mirror of
https://github.com/prometheus/alertmanager
synced 2025-04-11 03:31:55 +00:00
Improve filter (#714)
* Update elm-tools/parser * Improve filter UI * Pressing backspace edits the last matcher * Put escape char back into the output * Allow editing the matcher * Update bindata.go * Const for key codes * Use qualified imports * Update bindata.go * Commented the backspacePressed attribute
This commit is contained in:
parent
c3850708c1
commit
88ec956973
File diff suppressed because one or more lines are too long
@ -9,10 +9,11 @@
|
|||||||
"exposed-modules": [],
|
"exposed-modules": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
||||||
|
"elm-lang/dom": "1.1.1 <= v < 2.0.0",
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
||||||
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
||||||
"elm-lang/navigation": "2.0.1 <= v < 3.0.0",
|
"elm-lang/navigation": "2.0.1 <= v < 3.0.0",
|
||||||
"elm-tools/parser": "1.0.2 <= v < 2.0.0",
|
"elm-tools/parser": "2.0.0 <= v < 3.0.0",
|
||||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
||||||
"jweir/elm-iso8601": "3.0.2 <= v < 4.0.0"
|
"jweir/elm-iso8601": "3.0.2 <= v < 4.0.0"
|
||||||
},
|
},
|
||||||
|
@ -24,6 +24,7 @@ import Types
|
|||||||
import Utils.Types exposing (..)
|
import Utils.Types exposing (..)
|
||||||
import Views.SilenceForm.Types exposing (initSilenceForm)
|
import Views.SilenceForm.Types exposing (initSilenceForm)
|
||||||
import Views.Status.Types exposing (StatusModel, initStatusModel)
|
import Views.Status.Types exposing (StatusModel, initStatusModel)
|
||||||
|
import Views.AlertList.Types exposing (initAlertList)
|
||||||
import Updates exposing (update)
|
import Updates exposing (update)
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ init location =
|
|||||||
nullFilter
|
nullFilter
|
||||||
|
|
||||||
( model, msg ) =
|
( model, msg ) =
|
||||||
update (urlUpdate location) (Model Loading Loading initSilenceForm Loading route filter 0 initStatusModel)
|
update (urlUpdate location) (Model Loading Loading initSilenceForm initAlertList route filter 0 initStatusModel)
|
||||||
in
|
in
|
||||||
model ! [ msg, Task.perform UpdateCurrentTime Time.now ]
|
model ! [ msg, Task.perform UpdateCurrentTime Time.now ]
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module Types exposing (Model, Msg(..), Route(..))
|
module Types exposing (Model, Msg(..), Route(..))
|
||||||
|
|
||||||
import Alerts.Types exposing (AlertGroup, Alert)
|
import Alerts.Types exposing (AlertGroup, Alert)
|
||||||
import Views.AlertList.Types exposing (AlertListMsg)
|
import Views.AlertList.Types as AlertList exposing (AlertListMsg)
|
||||||
import Views.SilenceList.Types exposing (SilenceListMsg)
|
import Views.SilenceList.Types exposing (SilenceListMsg)
|
||||||
import Views.Silence.Types exposing (SilenceMsg)
|
import Views.Silence.Types exposing (SilenceMsg)
|
||||||
import Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg)
|
import Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg)
|
||||||
@ -15,7 +15,7 @@ type alias Model =
|
|||||||
{ silences : ApiData (List Silence)
|
{ silences : ApiData (List Silence)
|
||||||
, silence : ApiData Silence
|
, silence : ApiData Silence
|
||||||
, silenceForm : SilenceForm.Model
|
, silenceForm : SilenceForm.Model
|
||||||
, alertGroups : ApiData (List AlertGroup)
|
, alertList : AlertList.Model
|
||||||
, route : Route
|
, route : Route
|
||||||
, filter : Filter
|
, filter : Filter
|
||||||
, currentTime : Time.Time
|
, currentTime : Time.Time
|
||||||
@ -40,8 +40,7 @@ type Msg
|
|||||||
| Noop
|
| Noop
|
||||||
| RedirectAlerts
|
| RedirectAlerts
|
||||||
| UpdateCurrentTime Time.Time
|
| UpdateCurrentTime Time.Time
|
||||||
| UpdateFilter Filter String
|
| UpdateFilter String
|
||||||
| AddLabel Msg Label
|
|
||||||
|
|
||||||
|
|
||||||
type Route
|
type Route
|
||||||
|
@ -25,7 +25,6 @@ import Views.SilenceList.Updates
|
|||||||
import Views.Status.Types exposing (StatusMsg(InitStatusView))
|
import Views.Status.Types exposing (StatusMsg(InitStatusView))
|
||||||
import Views.Status.Updates
|
import Views.Status.Updates
|
||||||
import String exposing (trim)
|
import String exposing (trim)
|
||||||
import Regex
|
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
@ -43,10 +42,10 @@ update msg model =
|
|||||||
|
|
||||||
NavigateToAlerts filter ->
|
NavigateToAlerts filter ->
|
||||||
let
|
let
|
||||||
( alertGroups, cmd ) =
|
( alertList, cmd ) =
|
||||||
Views.AlertList.Updates.update FetchAlertGroups model.alertGroups filter
|
Views.AlertList.Updates.update FetchAlertGroups model.alertList filter
|
||||||
in
|
in
|
||||||
( { model | alertGroups = alertGroups, route = AlertsRoute filter, filter = filter }, cmd )
|
( { model | alertList = alertList, route = AlertsRoute filter, filter = filter }, cmd )
|
||||||
|
|
||||||
NavigateToSilenceList filter ->
|
NavigateToSilenceList filter ->
|
||||||
let
|
let
|
||||||
@ -87,44 +86,18 @@ update msg model =
|
|||||||
RedirectAlerts ->
|
RedirectAlerts ->
|
||||||
( model, Navigation.newUrl "/#/alerts" )
|
( model, Navigation.newUrl "/#/alerts" )
|
||||||
|
|
||||||
UpdateFilter filter text ->
|
UpdateFilter text ->
|
||||||
let
|
let
|
||||||
t =
|
t =
|
||||||
if trim text == "" then
|
if trim text == "" then
|
||||||
Nothing
|
Nothing
|
||||||
else
|
else
|
||||||
Just text
|
Just text
|
||||||
in
|
|
||||||
( { model | filter = { filter | text = t } }, Cmd.none )
|
|
||||||
|
|
||||||
AddLabel msg ( key, value ) ->
|
prevFilter =
|
||||||
let
|
|
||||||
filter =
|
|
||||||
model.filter
|
model.filter
|
||||||
|
|
||||||
label =
|
|
||||||
key ++ "=" ++ toString value
|
|
||||||
|
|
||||||
labels =
|
|
||||||
Maybe.withDefault "" filter.text
|
|
||||||
|> Regex.replace Regex.All (Regex.regex "{|}") (\_ -> "")
|
|
||||||
|> Regex.split Regex.All (Regex.regex "\\s*,\\s*")
|
|
||||||
|> List.filter (String.isEmpty >> not)
|
|
||||||
|> (\labels ->
|
|
||||||
if List.member label labels then
|
|
||||||
labels
|
|
||||||
else
|
|
||||||
labels ++ [ label ]
|
|
||||||
)
|
|
||||||
|> String.join ", "
|
|
||||||
|
|
||||||
text =
|
|
||||||
if String.isEmpty labels then
|
|
||||||
Nothing
|
|
||||||
else
|
|
||||||
Just ("{" ++ labels ++ "}")
|
|
||||||
in
|
in
|
||||||
( { model | filter = { filter | text = text } }, Task.perform identity (Task.succeed msg) )
|
( { model | filter = { prevFilter | text = t } }, Cmd.none )
|
||||||
|
|
||||||
Noop ->
|
Noop ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
@ -137,10 +110,10 @@ update msg model =
|
|||||||
|
|
||||||
MsgForAlertList msg ->
|
MsgForAlertList msg ->
|
||||||
let
|
let
|
||||||
( alertGroups, cmd ) =
|
( alertList, cmd ) =
|
||||||
Views.AlertList.Updates.update msg model.alertGroups model.filter
|
Views.AlertList.Updates.update msg model.alertList model.filter
|
||||||
in
|
in
|
||||||
( { model | alertGroups = alertGroups }, cmd )
|
( { model | alertList = alertList }, cmd )
|
||||||
|
|
||||||
MsgForSilenceList msg ->
|
MsgForSilenceList msg ->
|
||||||
let
|
let
|
||||||
|
@ -15,7 +15,7 @@ parseDuration =
|
|||||||
durationParser : Parser Time.Time
|
durationParser : Parser Time.Time
|
||||||
durationParser =
|
durationParser =
|
||||||
Parser.succeed (List.foldr (+) 0)
|
Parser.succeed (List.foldr (+) 0)
|
||||||
|= Parser.zeroOrMore term
|
|= Parser.repeat Parser.zeroOrMore term
|
||||||
|. Parser.end
|
|. Parser.end
|
||||||
|
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ term =
|
|||||||
|> List.map (\( unit, ms ) -> Parser.succeed ms |. Parser.symbol unit)
|
|> List.map (\( unit, ms ) -> Parser.succeed ms |. Parser.symbol unit)
|
||||||
|> Parser.oneOf
|
|> Parser.oneOf
|
||||||
)
|
)
|
||||||
|. Parser.ignoreWhile ((==) ' ')
|
|. Parser.ignore Parser.zeroOrMore ((==) ' ')
|
||||||
|
|
||||||
|
|
||||||
durationFormat : Time.Time -> String
|
durationFormat : Time.Time -> String
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
module Utils.Filter exposing (..)
|
module Utils.Filter
|
||||||
|
exposing
|
||||||
|
( Matcher
|
||||||
|
, MatchOperator(..)
|
||||||
|
, generateQueryParam
|
||||||
|
, generateQueryString
|
||||||
|
, stringifyMatcher
|
||||||
|
, stringifyFilter
|
||||||
|
, parseFilter
|
||||||
|
, parseMatcher
|
||||||
|
)
|
||||||
|
|
||||||
import Utils.Types exposing (Filter)
|
import Utils.Types exposing (Filter)
|
||||||
import Http exposing (encodeUri)
|
import Http exposing (encodeUri)
|
||||||
|
import Parser exposing (Parser, (|.), (|=), zeroOrMore, ignore)
|
||||||
|
import Parser.LanguageKit as Parser exposing (Trailing(..))
|
||||||
|
import Char
|
||||||
|
import Set
|
||||||
|
|
||||||
|
|
||||||
generateQueryParam : String -> Maybe String -> Maybe String
|
generateQueryParam : String -> Maybe String -> Maybe String
|
||||||
@ -18,3 +32,136 @@ generateQueryString { receiver, showSilenced, text } =
|
|||||||
|> List.filterMap (uncurry generateQueryParam)
|
|> List.filterMap (uncurry generateQueryParam)
|
||||||
|> String.join "&"
|
|> String.join "&"
|
||||||
|> (++) "?"
|
|> (++) "?"
|
||||||
|
|
||||||
|
|
||||||
|
type alias Matcher =
|
||||||
|
{ key : String
|
||||||
|
, op : MatchOperator
|
||||||
|
, value : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type MatchOperator
|
||||||
|
= Eq
|
||||||
|
| NotEq
|
||||||
|
| RegexMatch
|
||||||
|
| NotRegexMatch
|
||||||
|
|
||||||
|
|
||||||
|
matchers : List ( String, MatchOperator )
|
||||||
|
matchers =
|
||||||
|
[ ( "=~", RegexMatch )
|
||||||
|
, ( "!~", NotRegexMatch )
|
||||||
|
, ( "=", Eq )
|
||||||
|
, ( "!=", NotEq )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
parseFilter : String -> Maybe (List Matcher)
|
||||||
|
parseFilter =
|
||||||
|
Parser.run filter
|
||||||
|
>> Result.toMaybe
|
||||||
|
|
||||||
|
|
||||||
|
parseMatcher : String -> Maybe Matcher
|
||||||
|
parseMatcher =
|
||||||
|
Parser.run matcher
|
||||||
|
>> Result.toMaybe
|
||||||
|
|
||||||
|
|
||||||
|
stringifyFilter : List Matcher -> String
|
||||||
|
stringifyFilter matchers =
|
||||||
|
case matchers of
|
||||||
|
[] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
list ->
|
||||||
|
(list
|
||||||
|
|> List.map stringifyMatcher
|
||||||
|
|> String.join ", "
|
||||||
|
|> (++) "{"
|
||||||
|
)
|
||||||
|
++ "}"
|
||||||
|
|
||||||
|
|
||||||
|
stringifyMatcher : Matcher -> String
|
||||||
|
stringifyMatcher { key, op, value } =
|
||||||
|
key
|
||||||
|
++ (matchers
|
||||||
|
|> List.filter (Tuple.second >> (==) op)
|
||||||
|
|> List.head
|
||||||
|
|> Maybe.map Tuple.first
|
||||||
|
|> Maybe.withDefault ""
|
||||||
|
)
|
||||||
|
++ "\""
|
||||||
|
++ value
|
||||||
|
++ "\""
|
||||||
|
|
||||||
|
|
||||||
|
filter : Parser (List Matcher)
|
||||||
|
filter =
|
||||||
|
Parser.succeed identity
|
||||||
|
|= Parser.record spaces item
|
||||||
|
|. Parser.end
|
||||||
|
|
||||||
|
|
||||||
|
matcher : Parser Matcher
|
||||||
|
matcher =
|
||||||
|
Parser.succeed identity
|
||||||
|
|. spaces
|
||||||
|
|= item
|
||||||
|
|. spaces
|
||||||
|
|. Parser.end
|
||||||
|
|
||||||
|
|
||||||
|
item : Parser Matcher
|
||||||
|
item =
|
||||||
|
Parser.succeed Matcher
|
||||||
|
|= Parser.variable isVarChar isVarChar Set.empty
|
||||||
|
|= (matchers
|
||||||
|
|> List.map
|
||||||
|
(\( keyword, matcher ) ->
|
||||||
|
Parser.succeed matcher
|
||||||
|
|. Parser.keyword keyword
|
||||||
|
)
|
||||||
|
|> Parser.oneOf
|
||||||
|
)
|
||||||
|
|= string '"'
|
||||||
|
|
||||||
|
|
||||||
|
spaces : Parser ()
|
||||||
|
spaces =
|
||||||
|
ignore zeroOrMore (\char -> char == ' ' || char == '\t')
|
||||||
|
|
||||||
|
|
||||||
|
string : Char -> Parser String
|
||||||
|
string separator =
|
||||||
|
Parser.succeed identity
|
||||||
|
|. Parser.symbol (String.fromChar separator)
|
||||||
|
|= stringContents separator
|
||||||
|
|. Parser.symbol (String.fromChar separator)
|
||||||
|
|
||||||
|
|
||||||
|
stringContents : Char -> Parser String
|
||||||
|
stringContents separator =
|
||||||
|
Parser.oneOf
|
||||||
|
[ Parser.succeed (++)
|
||||||
|
|= keepOne (\char -> char == '\\')
|
||||||
|
|= keepOne (\char -> True)
|
||||||
|
, Parser.keep Parser.oneOrMore (\char -> char /= separator && char /= '\\')
|
||||||
|
]
|
||||||
|
|> Parser.repeat Parser.oneOrMore
|
||||||
|
|> Parser.map (String.join "")
|
||||||
|
|
||||||
|
|
||||||
|
isVarChar : Char -> Bool
|
||||||
|
isVarChar char =
|
||||||
|
Char.isLower char
|
||||||
|
|| Char.isUpper char
|
||||||
|
|| (char == '_')
|
||||||
|
|| Char.isDigit char
|
||||||
|
|
||||||
|
|
||||||
|
keepOne : (Char -> Bool) -> Parser String
|
||||||
|
keepOne =
|
||||||
|
Parser.keep (Parser.Exactly 1)
|
||||||
|
@ -3,7 +3,6 @@ module Views exposing (..)
|
|||||||
import Html exposing (Html, text, div)
|
import Html exposing (Html, text, div)
|
||||||
import Html.Attributes exposing (class)
|
import Html.Attributes exposing (class)
|
||||||
import Types exposing (Msg(MsgForSilenceForm), Model, Route(..))
|
import Types exposing (Msg(MsgForSilenceForm), Model, Route(..))
|
||||||
import Utils.Types exposing (ApiResponse(..))
|
|
||||||
import Utils.Views exposing (error, loading)
|
import Utils.Views exposing (error, loading)
|
||||||
import Views.SilenceList.Views as SilenceList
|
import Views.SilenceList.Views as SilenceList
|
||||||
import Views.SilenceForm.Views as SilenceForm
|
import Views.SilenceForm.Views as SilenceForm
|
||||||
@ -33,15 +32,7 @@ currentView model =
|
|||||||
Silence.view model
|
Silence.view model
|
||||||
|
|
||||||
AlertsRoute filter ->
|
AlertsRoute filter ->
|
||||||
case model.alertGroups of
|
AlertList.view model.alertList filter
|
||||||
Success alertGroups ->
|
|
||||||
AlertList.view alertGroups model.filter (text "")
|
|
||||||
|
|
||||||
Loading ->
|
|
||||||
loading
|
|
||||||
|
|
||||||
Failure msg ->
|
|
||||||
AlertList.view [] model.filter (error msg)
|
|
||||||
|
|
||||||
SilenceListRoute route ->
|
SilenceListRoute route ->
|
||||||
SilenceList.view model.silences model.silence model.currentTime model.filter
|
SilenceList.view model.silences model.silence model.currentTime model.filter
|
||||||
|
@ -1,20 +1,151 @@
|
|||||||
module Views.AlertList.FilterBar exposing (view)
|
module Views.AlertList.FilterBar exposing (view)
|
||||||
|
|
||||||
import Html exposing (Html, div, span, input, text, button, i)
|
import Html exposing (Html, Attribute, div, span, input, text, button, i, small)
|
||||||
import Html.Attributes exposing (value, class)
|
import Html.Attributes exposing (value, class, style, disabled, id)
|
||||||
import Html.Events exposing (onClick, onInput)
|
import Html.Events exposing (onClick, onInput, on, keyCode)
|
||||||
|
import Utils.Filter exposing (Matcher)
|
||||||
|
import Views.AlertList.Types exposing (AlertListMsg(AddFilterMatcher, DeleteFilterMatcher, PressingBackspace, UpdateMatcherText))
|
||||||
|
import Types exposing (Msg(MsgForAlertList, Noop))
|
||||||
|
import Json.Decode as Json
|
||||||
|
|
||||||
|
|
||||||
view : String -> (String -> msg) -> msg -> Html msg
|
keys :
|
||||||
view filterText inputChangedMsg buttonClickedMsg =
|
{ backspace : Int
|
||||||
div [ class "input-group" ]
|
, enter : Int
|
||||||
[ span [ class "input-group-addon" ]
|
}
|
||||||
[ i [ class "fa fa-filter" ] []
|
keys =
|
||||||
|
{ backspace = 8
|
||||||
|
, enter = 13
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewMatcher : Matcher -> Html Msg
|
||||||
|
viewMatcher matcher =
|
||||||
|
div [ class "col col-auto", style [ ( "padding", "5px" ) ] ]
|
||||||
|
[ div [ class "btn-group" ]
|
||||||
|
[ button
|
||||||
|
[ class "btn btn-outline-info"
|
||||||
|
, onClick (DeleteFilterMatcher True matcher |> MsgForAlertList)
|
||||||
|
]
|
||||||
|
[ text <| Utils.Filter.stringifyMatcher matcher
|
||||||
|
]
|
||||||
|
, button
|
||||||
|
[ class "btn btn-outline-danger"
|
||||||
|
, onClick (DeleteFilterMatcher False matcher |> MsgForAlertList)
|
||||||
|
]
|
||||||
|
[ text "×" ]
|
||||||
]
|
]
|
||||||
, input
|
|
||||||
[ class "form-control", value filterText, onInput inputChangedMsg ]
|
|
||||||
[]
|
|
||||||
, span
|
|
||||||
[ class "input-group-btn" ]
|
|
||||||
[ button [ class "btn btn-primary", onClick buttonClickedMsg ] [ text "Filter" ] ]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
lastElem : List a -> Maybe a
|
||||||
|
lastElem =
|
||||||
|
List.foldl (Just >> always) Nothing
|
||||||
|
|
||||||
|
|
||||||
|
viewMatchers : List Matcher -> List (Html Msg)
|
||||||
|
viewMatchers matchers =
|
||||||
|
matchers
|
||||||
|
|> List.map viewMatcher
|
||||||
|
|
||||||
|
|
||||||
|
onKey : String -> Int -> Msg -> Attribute Msg
|
||||||
|
onKey event key msg =
|
||||||
|
on event
|
||||||
|
(Json.map
|
||||||
|
(\k ->
|
||||||
|
if k == key then
|
||||||
|
msg
|
||||||
|
else
|
||||||
|
Noop
|
||||||
|
)
|
||||||
|
keyCode
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
view : List Matcher -> String -> Bool -> Html Msg
|
||||||
|
view matchers matcherText backspacePressed =
|
||||||
|
let
|
||||||
|
className =
|
||||||
|
if matcherText == "" then
|
||||||
|
""
|
||||||
|
else
|
||||||
|
case maybeMatcher of
|
||||||
|
Just _ ->
|
||||||
|
"has-success"
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
"has-danger"
|
||||||
|
|
||||||
|
maybeMatcher =
|
||||||
|
Utils.Filter.parseMatcher matcherText
|
||||||
|
|
||||||
|
onKeydown =
|
||||||
|
onKey "keydown" keys.backspace <|
|
||||||
|
case ( matcherText, backspacePressed ) of
|
||||||
|
( "", True ) ->
|
||||||
|
Noop
|
||||||
|
|
||||||
|
( "", False ) ->
|
||||||
|
lastElem matchers
|
||||||
|
|> Maybe.map (DeleteFilterMatcher True >> MsgForAlertList)
|
||||||
|
|> Maybe.withDefault Noop
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
PressingBackspace True |> MsgForAlertList
|
||||||
|
|
||||||
|
onKeypress =
|
||||||
|
maybeMatcher
|
||||||
|
|> Maybe.map (AddFilterMatcher True >> MsgForAlertList)
|
||||||
|
|> Maybe.withDefault Noop
|
||||||
|
|> onKey "keypress" keys.enter
|
||||||
|
|
||||||
|
onKeyup =
|
||||||
|
onKey "keyup" keys.backspace (PressingBackspace False |> MsgForAlertList)
|
||||||
|
|
||||||
|
onClickAttr =
|
||||||
|
maybeMatcher
|
||||||
|
|> Maybe.map (AddFilterMatcher True >> MsgForAlertList)
|
||||||
|
|> Maybe.withDefault Noop
|
||||||
|
|> onClick
|
||||||
|
|
||||||
|
isDisabled =
|
||||||
|
maybeMatcher == Nothing
|
||||||
|
in
|
||||||
|
div
|
||||||
|
[ class "row no-gutters align-items-start" ]
|
||||||
|
(viewMatchers matchers
|
||||||
|
++ [ div
|
||||||
|
[ class ("col form-group " ++ className)
|
||||||
|
, style
|
||||||
|
[ ( "padding", "5px" )
|
||||||
|
, ( "min-width", "200px" )
|
||||||
|
, ( "max-width", "500px" )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ div [ class "input-group" ]
|
||||||
|
[ input
|
||||||
|
[ id "custom-matcher"
|
||||||
|
, class "form-control"
|
||||||
|
, value matcherText
|
||||||
|
, onKeydown
|
||||||
|
, onKeyup
|
||||||
|
, onKeypress
|
||||||
|
, onInput (UpdateMatcherText >> MsgForAlertList)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, span
|
||||||
|
[ class "input-group-btn" ]
|
||||||
|
[ button [ class "btn btn-primary", disabled isDisabled, onClickAttr ] [ text "Add" ] ]
|
||||||
|
]
|
||||||
|
, small [ class "form-text text-muted" ]
|
||||||
|
[ text "Custom matcher, e.g."
|
||||||
|
, button
|
||||||
|
[ class "btn btn-link btn-sm align-baseline"
|
||||||
|
, onClick (UpdateMatcherText "env=\"production\"" |> MsgForAlertList)
|
||||||
|
]
|
||||||
|
[ text "env=\"production\"" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@ -1,10 +1,40 @@
|
|||||||
module Views.AlertList.Types exposing (AlertListMsg(..))
|
module Views.AlertList.Types exposing (AlertListMsg(..), Model, initAlertList)
|
||||||
|
|
||||||
import Utils.Types exposing (ApiData, Filter)
|
import Utils.Types exposing (ApiData, Filter, ApiResponse(Loading))
|
||||||
import Alerts.Types exposing (Alert, AlertGroup)
|
import Alerts.Types exposing (Alert, AlertGroup)
|
||||||
|
import Utils.Filter
|
||||||
|
|
||||||
|
|
||||||
type AlertListMsg
|
type AlertListMsg
|
||||||
= AlertGroupsFetch (ApiData (List AlertGroup))
|
= AlertGroupsFetch (ApiData (List AlertGroup))
|
||||||
| FetchAlertGroups
|
| FetchAlertGroups
|
||||||
| FilterAlerts
|
| AddFilterMatcher Bool Utils.Filter.Matcher
|
||||||
|
| DeleteFilterMatcher Bool Utils.Filter.Matcher
|
||||||
|
| PressingBackspace Bool
|
||||||
|
| UpdateMatcherText String
|
||||||
|
|
||||||
|
|
||||||
|
{-| A note about the `backspacePressed` attribute:
|
||||||
|
|
||||||
|
Holding down the backspace removes (one by one) each last character in the input,
|
||||||
|
and the whole time sends multiple keyDown events. This is a guard so that if a user
|
||||||
|
holds down backspace to remove the text in the input, they won't accidentally hold
|
||||||
|
backspace too long and then delete the preceding matcher as well. So, once a user holds
|
||||||
|
backspace to clear an input, they have to then lift up the key and press it again to
|
||||||
|
proceed to deleting the next matcher.
|
||||||
|
-}
|
||||||
|
type alias Model =
|
||||||
|
{ alertGroups : ApiData (List AlertGroup)
|
||||||
|
, matchers : List Utils.Filter.Matcher
|
||||||
|
, backspacePressed : Bool
|
||||||
|
, matcherText : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initAlertList : Model
|
||||||
|
initAlertList =
|
||||||
|
{ alertGroups = Loading
|
||||||
|
, matchers = []
|
||||||
|
, backspacePressed = False
|
||||||
|
, matcherText = ""
|
||||||
|
}
|
||||||
|
@ -1,22 +1,74 @@
|
|||||||
module Views.AlertList.Updates exposing (..)
|
module Views.AlertList.Updates exposing (..)
|
||||||
|
|
||||||
import Alerts.Api as Api
|
import Alerts.Api as Api
|
||||||
import Views.AlertList.Types exposing (AlertListMsg(..))
|
import Views.AlertList.Types exposing (AlertListMsg(..), Model)
|
||||||
import Alerts.Types exposing (AlertGroup)
|
|
||||||
import Navigation
|
import Navigation
|
||||||
import Utils.Types exposing (ApiData, ApiResponse(..), Filter)
|
import Utils.Types exposing (ApiData, ApiResponse(Loading), Filter)
|
||||||
import Utils.Filter exposing (generateQueryString)
|
import Utils.Filter exposing (generateQueryString, stringifyFilter, parseFilter)
|
||||||
import Types exposing (Msg(MsgForAlertList))
|
import Types exposing (Msg(MsgForAlertList, Noop))
|
||||||
|
import Dom
|
||||||
|
import Task
|
||||||
|
|
||||||
|
|
||||||
update : AlertListMsg -> ApiData (List AlertGroup) -> Filter -> ( ApiData (List AlertGroup), Cmd Types.Msg )
|
immediatelyFilter : Filter -> Model -> ( Model, Cmd Types.Msg )
|
||||||
update msg groups filter =
|
immediatelyFilter filter model =
|
||||||
|
let
|
||||||
|
newFilter =
|
||||||
|
{ filter | text = Just (stringifyFilter model.matchers) }
|
||||||
|
in
|
||||||
|
( model
|
||||||
|
, Cmd.batch
|
||||||
|
[ Navigation.newUrl ("/#/alerts" ++ generateQueryString newFilter)
|
||||||
|
, Dom.focus "custom-matcher" |> Task.attempt (always Noop)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
update : AlertListMsg -> Model -> Filter -> ( Model, Cmd Types.Msg )
|
||||||
|
update msg model filter =
|
||||||
case msg of
|
case msg of
|
||||||
AlertGroupsFetch alertGroups ->
|
AlertGroupsFetch alertGroups ->
|
||||||
( alertGroups, Cmd.none )
|
( { model | alertGroups = alertGroups }, Cmd.none )
|
||||||
|
|
||||||
FetchAlertGroups ->
|
FetchAlertGroups ->
|
||||||
( groups, Api.alertGroups filter |> Cmd.map (AlertGroupsFetch >> MsgForAlertList) )
|
( { model
|
||||||
|
| matchers =
|
||||||
|
filter.text
|
||||||
|
|> Maybe.andThen parseFilter
|
||||||
|
|> Maybe.withDefault []
|
||||||
|
, alertGroups = Loading
|
||||||
|
}
|
||||||
|
, Api.alertGroups filter |> Cmd.map (AlertGroupsFetch >> MsgForAlertList)
|
||||||
|
)
|
||||||
|
|
||||||
FilterAlerts ->
|
AddFilterMatcher emptyMatcherText matcher ->
|
||||||
( groups, Navigation.newUrl ("/#/alerts" ++ generateQueryString filter) )
|
immediatelyFilter filter
|
||||||
|
{ model
|
||||||
|
| matchers =
|
||||||
|
if List.member matcher model.matchers then
|
||||||
|
model.matchers
|
||||||
|
else
|
||||||
|
model.matchers ++ [ matcher ]
|
||||||
|
, matcherText =
|
||||||
|
if emptyMatcherText then
|
||||||
|
""
|
||||||
|
else
|
||||||
|
model.matcherText
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteFilterMatcher setMatcherText matcher ->
|
||||||
|
immediatelyFilter filter
|
||||||
|
{ model
|
||||||
|
| matchers = List.filter ((/=) matcher) model.matchers
|
||||||
|
, matcherText =
|
||||||
|
if setMatcherText then
|
||||||
|
Utils.Filter.stringifyMatcher matcher
|
||||||
|
else
|
||||||
|
model.matcherText
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMatcherText value ->
|
||||||
|
( { model | matcherText = value }, Cmd.none )
|
||||||
|
|
||||||
|
PressingBackspace isPressed ->
|
||||||
|
( { model | backspacePressed = isPressed }, Cmd.none )
|
||||||
|
@ -1,44 +1,51 @@
|
|||||||
module Views.AlertList.Views exposing (view)
|
module Views.AlertList.Views exposing (view)
|
||||||
|
|
||||||
import Alerts.Types exposing (Alert, AlertGroup, Block)
|
import Alerts.Types exposing (Alert, AlertGroup, Block)
|
||||||
import Views.AlertList.Types exposing (AlertListMsg(FilterAlerts))
|
|
||||||
import Views.AlertList.Filter exposing (silenced, receiver, matchers)
|
import Views.AlertList.Filter exposing (silenced, receiver, matchers)
|
||||||
import Views.AlertList.FilterBar
|
import Views.AlertList.FilterBar
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Utils.Date
|
import Utils.Date
|
||||||
import Utils.Types exposing (Filter)
|
import Utils.Types exposing (ApiResponse(Success, Loading, Failure), Filter)
|
||||||
import Utils.Views exposing (..)
|
import Utils.Filter
|
||||||
import Types exposing (Msg(MsgForAlertList, Noop, CreateSilenceFromAlert, UpdateFilter, AddLabel))
|
import Utils.Views exposing (buttonLink, onClickMsgButton, listButton)
|
||||||
|
import Views.AlertList.Types exposing (AlertListMsg(AddFilterMatcher), Model)
|
||||||
|
import Types exposing (Msg(Noop, CreateSilenceFromAlert, MsgForAlertList))
|
||||||
|
|
||||||
|
|
||||||
view : List AlertGroup -> Filter -> Html Msg -> Html Msg
|
view : Model -> Filter -> Html Msg
|
||||||
view alertGroups filter errorHtml =
|
view { alertGroups, matchers, matcherText, backspacePressed } filter =
|
||||||
|
div []
|
||||||
|
[ Views.AlertList.FilterBar.view matchers matcherText backspacePressed
|
||||||
|
, case alertGroups of
|
||||||
|
Success groups ->
|
||||||
|
viewGroups groups filter
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
Utils.Views.loading
|
||||||
|
|
||||||
|
Failure msg ->
|
||||||
|
Utils.Views.error msg
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewGroups : List AlertGroup -> Filter -> Html Msg
|
||||||
|
viewGroups alertGroups filter =
|
||||||
let
|
let
|
||||||
filteredGroups =
|
filteredGroups =
|
||||||
receiver filter.receiver alertGroups
|
receiver filter.receiver alertGroups
|
||||||
|> silenced filter.showSilenced
|
|> silenced filter.showSilenced
|
||||||
|
|
||||||
filterText =
|
|
||||||
Maybe.withDefault "" filter.text
|
|
||||||
|
|
||||||
alertHtml =
|
|
||||||
if List.isEmpty filteredGroups then
|
|
||||||
div [ class "mt2" ] [ text "no alerts found found" ]
|
|
||||||
else
|
|
||||||
ul
|
|
||||||
[ classList
|
|
||||||
[ ( "list", True )
|
|
||||||
, ( "pa0", True )
|
|
||||||
]
|
|
||||||
]
|
|
||||||
(List.map alertGroupView filteredGroups)
|
|
||||||
in
|
in
|
||||||
div []
|
if List.isEmpty filteredGroups then
|
||||||
[ Views.AlertList.FilterBar.view filterText (Types.UpdateFilter filter) (MsgForAlertList FilterAlerts)
|
div [ class "mt2" ] [ text "no alerts found" ]
|
||||||
, errorHtml
|
else
|
||||||
, alertHtml
|
ul
|
||||||
]
|
[ classList
|
||||||
|
[ ( "list", True )
|
||||||
|
, ( "pa0", True )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
(List.map alertGroupView filteredGroups)
|
||||||
|
|
||||||
|
|
||||||
alertGroupView : AlertGroup -> Html Msg
|
alertGroupView : AlertGroup -> Html Msg
|
||||||
@ -80,10 +87,16 @@ alertView alert =
|
|||||||
|
|
||||||
|
|
||||||
labelButton : ( String, String ) -> Html Msg
|
labelButton : ( String, String ) -> Html Msg
|
||||||
labelButton (( key, value ) as label) =
|
labelButton ( key, value ) =
|
||||||
onClickMsgButton
|
onClickMsgButton
|
||||||
(key ++ "=" ++ value)
|
(key ++ "=" ++ value)
|
||||||
(AddLabel Noop label)
|
(AddFilterMatcher False
|
||||||
|
{ key = key
|
||||||
|
, op = Utils.Filter.Eq
|
||||||
|
, value = value
|
||||||
|
}
|
||||||
|
|> MsgForAlertList
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
alertHeader : ( String, String ) -> Html msg
|
alertHeader : ( String, String ) -> Html msg
|
||||||
|
@ -47,7 +47,7 @@ silences silences filter errorHtml =
|
|||||||
(List.map silenceList silences)
|
(List.map silenceList silences)
|
||||||
in
|
in
|
||||||
div []
|
div []
|
||||||
[ textField "Filter" filterText (UpdateFilter filter)
|
[ textField "Filter" filterText UpdateFilter
|
||||||
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", onClick (MsgForSilenceList FilterSilences) ] [ text "Filter Silences" ]
|
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", onClick (MsgForSilenceList FilterSilences) ] [ text "Filter Silences" ]
|
||||||
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", href "#/silences/new" ] [ text "New Silence" ]
|
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", href "#/silences/new" ] [ text "New Silence" ]
|
||||||
, errorHtml
|
, errorHtml
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user