Receiver autocomplete (#900)
* Extract receivers * Moved Match into Utils * Implemented autocomplete * Update bindata.go
This commit is contained in:
parent
dfbac123db
commit
f4c11751a9
|
@ -1,18 +1,19 @@
|
|||
module Alerts.Api exposing (..)
|
||||
|
||||
import Alerts.Types exposing (Alert, AlertGroup, Block, RouteOpts)
|
||||
import Alerts.Types exposing (Alert, Receiver)
|
||||
import Json.Decode as Json exposing (..)
|
||||
import Utils.Api exposing (iso8601Time)
|
||||
import Utils.Filter exposing (Filter, generateQueryString)
|
||||
import Utils.Types exposing (ApiData)
|
||||
import Regex
|
||||
|
||||
|
||||
fetchReceivers : String -> Cmd (ApiData (List String))
|
||||
fetchReceivers : String -> Cmd (ApiData (List Receiver))
|
||||
fetchReceivers apiUrl =
|
||||
Utils.Api.send
|
||||
(Utils.Api.get
|
||||
(apiUrl ++ "/receivers")
|
||||
(field "data" (list string))
|
||||
(field "data" (list (Json.map (\receiver -> Receiver receiver (Regex.escape receiver)) string)))
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
module Alerts.Types exposing (Alert, AlertGroup, Block, RouteOpts)
|
||||
module Alerts.Types exposing (Alert, Receiver)
|
||||
|
||||
import Utils.Types exposing (Labels)
|
||||
import Time exposing (Time)
|
||||
|
||||
|
||||
-- TODO: Revive inhibited field
|
||||
|
||||
|
||||
type alias Alert =
|
||||
{ annotations : Labels
|
||||
, labels : Labels
|
||||
|
@ -18,17 +15,7 @@ type alias Alert =
|
|||
}
|
||||
|
||||
|
||||
type alias AlertGroup =
|
||||
{ blocks : List Block
|
||||
, labels : Labels
|
||||
type alias Receiver =
|
||||
{ name : String
|
||||
, regex : String
|
||||
}
|
||||
|
||||
|
||||
type alias Block =
|
||||
{ alerts : List Alert
|
||||
, routeOpts : RouteOpts
|
||||
}
|
||||
|
||||
|
||||
type alias RouteOpts =
|
||||
{ receiver : String }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Types exposing (Model, Msg(..), Route(..))
|
||||
|
||||
import Alerts.Types exposing (AlertGroup, Alert)
|
||||
import Alerts.Types exposing (Alert)
|
||||
import Views.AlertList.Types as AlertList exposing (AlertListMsg)
|
||||
import Views.SilenceList.Types as SilenceList exposing (SilenceListMsg)
|
||||
import Views.SilenceView.Types as SilenceView exposing (SilenceViewMsg)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module Views.GroupBar.Match exposing (jaro, jaroWinkler, commonPrefix)
|
||||
module Utils.Match exposing (jaro, jaroWinkler, commonPrefix)
|
||||
|
||||
import Utils.List exposing (zip)
|
||||
import Char
|
|
@ -4,14 +4,13 @@ import Alerts.Types exposing (Alert)
|
|||
import Utils.Types exposing (ApiData(Initial))
|
||||
import Views.FilterBar.Types as FilterBar
|
||||
import Views.GroupBar.Types as GroupBar
|
||||
import Views.ReceiverBar.Types as ReceiverBar
|
||||
|
||||
|
||||
type AlertListMsg
|
||||
= AlertsFetched (ApiData (List Alert))
|
||||
| ReceiversFetched (ApiData (List String))
|
||||
| ToggleReceivers Bool
|
||||
| SelectReceiver (Maybe String)
|
||||
| FetchAlerts
|
||||
| MsgForReceiverBar ReceiverBar.Msg
|
||||
| MsgForFilterBar FilterBar.Msg
|
||||
| MsgForGroupBar GroupBar.Msg
|
||||
| ToggleSilenced Bool
|
||||
|
@ -26,8 +25,7 @@ type Tab
|
|||
|
||||
type alias Model =
|
||||
{ alerts : ApiData (List Alert)
|
||||
, receivers : List String
|
||||
, showRecievers : Bool
|
||||
, receiverBar : ReceiverBar.Model
|
||||
, groupBar : GroupBar.Model
|
||||
, filterBar : FilterBar.Model
|
||||
, tab : Tab
|
||||
|
@ -38,8 +36,7 @@ type alias Model =
|
|||
initAlertList : Model
|
||||
initAlertList =
|
||||
{ alerts = Initial
|
||||
, receivers = []
|
||||
, showRecievers = False
|
||||
, receiverBar = ReceiverBar.initReceiverBar
|
||||
, groupBar = GroupBar.initGroupBar
|
||||
, filterBar = FilterBar.initFilterBar
|
||||
, tab = FilterTab
|
||||
|
|
|
@ -7,14 +7,14 @@ import Utils.Filter exposing (Filter, parseFilter)
|
|||
import Utils.Types exposing (ApiData(Initial, Loading, Success, Failure))
|
||||
import Types exposing (Msg(MsgForAlertList, Noop))
|
||||
import Set
|
||||
import Regex
|
||||
import Navigation
|
||||
import Utils.Filter exposing (generateQueryString)
|
||||
import Views.GroupBar.Updates as GroupBar
|
||||
import Views.ReceiverBar.Updates as ReceiverBar
|
||||
|
||||
|
||||
update : AlertListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd Types.Msg )
|
||||
update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
|
||||
update msg ({ groupBar, filterBar, receiverBar } as model) filter apiUrl basePath =
|
||||
let
|
||||
alertsUrl =
|
||||
basePath ++ "#/alerts"
|
||||
|
@ -50,24 +50,10 @@ update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
|
|||
( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing }
|
||||
, Cmd.batch
|
||||
[ Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList)
|
||||
, Api.fetchReceivers apiUrl |> Cmd.map (ReceiversFetched >> MsgForAlertList)
|
||||
, ReceiverBar.fetchReceivers apiUrl |> Cmd.map (MsgForReceiverBar >> MsgForAlertList)
|
||||
]
|
||||
)
|
||||
|
||||
ReceiversFetched (Success receivers) ->
|
||||
( { model | receivers = receivers }, Cmd.none )
|
||||
|
||||
ReceiversFetched _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
ToggleReceivers show ->
|
||||
( { model | showRecievers = show }, Cmd.none )
|
||||
|
||||
SelectReceiver receiver ->
|
||||
( { model | showRecievers = False }
|
||||
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | receiver = Maybe.map Regex.escape receiver })
|
||||
)
|
||||
|
||||
ToggleSilenced showSilenced ->
|
||||
( model
|
||||
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced })
|
||||
|
@ -90,5 +76,12 @@ update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
|
|||
in
|
||||
( { model | groupBar = newGroupBar }, Cmd.map (MsgForGroupBar >> MsgForAlertList) cmd )
|
||||
|
||||
MsgForReceiverBar msg ->
|
||||
let
|
||||
( newReceiverBar, cmd ) =
|
||||
ReceiverBar.update alertsUrl filter msg receiverBar
|
||||
in
|
||||
( { model | receiverBar = newReceiverBar }, Cmd.map (MsgForReceiverBar >> MsgForAlertList) cmd )
|
||||
|
||||
SetActive maybeId ->
|
||||
( { model | activeId = maybeId }, Cmd.none )
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
module Views.AlertList.Views exposing (view)
|
||||
|
||||
import Alerts.Types exposing (Alert, AlertGroup, Block)
|
||||
import Alerts.Types exposing (Alert)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import Types exposing (Msg(Noop, CreateSilenceFromAlert, MsgForAlertList))
|
||||
import Utils.Filter exposing (Filter)
|
||||
import Views.FilterBar.Views as FilterBar
|
||||
import Views.ReceiverBar.Views as ReceiverBar
|
||||
import Utils.Types exposing (ApiData(Initial, Success, Loading, Failure), Labels)
|
||||
import Utils.Views
|
||||
import Utils.List
|
||||
|
@ -16,7 +17,6 @@ import Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..))
|
|||
import Types exposing (Msg(Noop, CreateSilenceFromAlert, MsgForAlertList))
|
||||
import Views.GroupBar.Views as GroupBar
|
||||
import Dict exposing (Dict)
|
||||
import Regex
|
||||
|
||||
|
||||
renderSilenced : Maybe Bool -> Html Msg
|
||||
|
@ -37,7 +37,7 @@ renderSilenced maybeShowSilenced =
|
|||
|
||||
|
||||
view : Model -> Filter -> Html Msg
|
||||
view { alerts, groupBar, filterBar, receivers, showRecievers, tab, activeId } filter =
|
||||
view { alerts, groupBar, filterBar, receiverBar, tab, activeId } filter =
|
||||
div []
|
||||
[ div
|
||||
[ class "card mb-5" ]
|
||||
|
@ -45,7 +45,7 @@ view { alerts, groupBar, filterBar, receivers, showRecievers, tab, activeId } fi
|
|||
[ ul [ class "nav nav-tabs card-header-tabs" ]
|
||||
[ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text "Filter" ]
|
||||
, Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ text "Group" ]
|
||||
, renderReceivers filter.receiver receivers showRecievers
|
||||
, ReceiverBar.view filter.receiver receiverBar |> Html.map (MsgForReceiverBar >> MsgForAlertList)
|
||||
, renderSilenced filter.showSilenced
|
||||
]
|
||||
]
|
||||
|
@ -74,12 +74,12 @@ view { alerts, groupBar, filterBar, receivers, showRecievers, tab, activeId } fi
|
|||
|
||||
|
||||
alertGroups : Maybe String -> Filter -> GroupBar.Model -> List Alert -> Html Msg
|
||||
alertGroups activeId filter groupBar alerts =
|
||||
alertGroups activeId filter { fields } alerts =
|
||||
let
|
||||
grouped =
|
||||
alerts
|
||||
|> Utils.List.groupBy
|
||||
(.labels >> List.filter (\( key, _ ) -> List.member key groupBar.fields))
|
||||
(.labels >> List.filter (\( key, _ ) -> List.member key fields))
|
||||
in
|
||||
grouped
|
||||
|> Dict.keys
|
||||
|
@ -91,6 +91,12 @@ alertGroups activeId filter groupBar alerts =
|
|||
(alertList activeId labels filter)
|
||||
(Dict.get labels grouped)
|
||||
)
|
||||
|> (\list ->
|
||||
if List.isEmpty list then
|
||||
[ Utils.Views.error "No alerts found" ]
|
||||
else
|
||||
list
|
||||
)
|
||||
|> div []
|
||||
|
||||
|
||||
|
@ -110,71 +116,5 @@ alertList activeId labels filter alerts =
|
|||
)
|
||||
labels
|
||||
)
|
||||
, if List.isEmpty alerts then
|
||||
div [] [ text "no alerts found" ]
|
||||
else
|
||||
ul [ class "list-group mb-4" ] (List.map (AlertView.view labels activeId) alerts)
|
||||
, ul [ class "list-group mb-4" ] (List.map (AlertView.view labels activeId) alerts)
|
||||
]
|
||||
|
||||
|
||||
renderReceivers : Maybe String -> List String -> Bool -> Html Msg
|
||||
renderReceivers receiver receivers opened =
|
||||
let
|
||||
autoCompleteClass =
|
||||
if opened then
|
||||
"show"
|
||||
else
|
||||
""
|
||||
|
||||
navLinkClass =
|
||||
if opened then
|
||||
"active"
|
||||
else
|
||||
""
|
||||
|
||||
-- Try to find the regex-escaped receiver in the list of unescaped receivers:
|
||||
unescapedReceiver =
|
||||
receivers
|
||||
|> List.filter (Regex.escape >> Just >> (==) receiver)
|
||||
|> List.map Just
|
||||
|> List.head
|
||||
|> Maybe.withDefault receiver
|
||||
in
|
||||
li
|
||||
[ class ("nav-item ml-auto autocomplete-menu " ++ autoCompleteClass)
|
||||
, onBlur (ToggleReceivers False |> MsgForAlertList)
|
||||
, tabindex 1
|
||||
, style
|
||||
[ ( "position", "relative" )
|
||||
, ( "outline", "none" )
|
||||
]
|
||||
]
|
||||
[ div
|
||||
[ onClick (ToggleReceivers (not opened) |> MsgForAlertList)
|
||||
, class "mt-1 mr-4"
|
||||
, style [ ( "cursor", "pointer" ) ]
|
||||
]
|
||||
[ text ("Receiver: " ++ Maybe.withDefault "All" unescapedReceiver) ]
|
||||
, receivers
|
||||
|> List.map Just
|
||||
|> (::) Nothing
|
||||
|> List.map (receiverField unescapedReceiver)
|
||||
|> div [ class "dropdown-menu dropdown-menu-right" ]
|
||||
]
|
||||
|
||||
|
||||
receiverField : Maybe String -> Maybe String -> Html Msg
|
||||
receiverField selected maybeReceiver =
|
||||
let
|
||||
attrs =
|
||||
if selected == maybeReceiver then
|
||||
[ class "dropdown-item active" ]
|
||||
else
|
||||
[ class "dropdown-item"
|
||||
, style [ ( "cursor", "pointer" ) ]
|
||||
, onClick (SelectReceiver maybeReceiver |> MsgForAlertList)
|
||||
]
|
||||
in
|
||||
div
|
||||
attrs
|
||||
[ text (Maybe.withDefault "All" maybeReceiver) ]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Views.GroupBar.Updates exposing (update, setFields)
|
||||
|
||||
import Views.GroupBar.Types exposing (Model, Msg(..))
|
||||
import Views.GroupBar.Match exposing (jaroWinkler)
|
||||
import Utils.Match exposing (jaroWinkler)
|
||||
import Task
|
||||
import Dom
|
||||
import Set
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
module Views.ReceiverBar.Types exposing (Model, Msg(..), initReceiverBar)
|
||||
|
||||
import Utils.Types exposing (ApiData(Initial))
|
||||
import Alerts.Types exposing (Receiver)
|
||||
|
||||
|
||||
type Msg
|
||||
= ReceiversFetched (ApiData (List Receiver))
|
||||
| UpdateReceiver String
|
||||
| EditReceivers
|
||||
| FilterByReceiver String
|
||||
| Select (Maybe Receiver)
|
||||
| ResultsHovered Bool
|
||||
| BlurReceiverField
|
||||
| Noop
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ receivers : List Receiver
|
||||
, matches : List Receiver
|
||||
, fieldText : String
|
||||
, selectedReceiver : Maybe Receiver
|
||||
, showReceivers : Bool
|
||||
, resultsHovered : Bool
|
||||
}
|
||||
|
||||
|
||||
initReceiverBar : Model
|
||||
initReceiverBar =
|
||||
{ receivers = []
|
||||
, matches = []
|
||||
, fieldText = ""
|
||||
, selectedReceiver = Nothing
|
||||
, showReceivers = False
|
||||
, resultsHovered = False
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
module Views.ReceiverBar.Updates exposing (update, fetchReceivers)
|
||||
|
||||
import Views.ReceiverBar.Types exposing (Model, Msg(..))
|
||||
import Utils.Types exposing (ApiData(Success))
|
||||
import Utils.Filter exposing (Filter, generateQueryString, stringifyGroup, parseGroup)
|
||||
import Navigation
|
||||
import Dom
|
||||
import Task
|
||||
import Alerts.Api as Api
|
||||
import Utils.Match exposing (jaroWinkler)
|
||||
|
||||
|
||||
update : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update url filter msg model =
|
||||
case msg of
|
||||
ReceiversFetched (Success receivers) ->
|
||||
( { model | receivers = receivers }, Cmd.none )
|
||||
|
||||
ReceiversFetched _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
EditReceivers ->
|
||||
( { model
|
||||
| showReceivers = True
|
||||
, fieldText = ""
|
||||
, matches =
|
||||
model.receivers
|
||||
|> List.take 10
|
||||
|> (::) { name = "All", regex = "" }
|
||||
, selectedReceiver = Nothing
|
||||
}
|
||||
, Dom.focus "receiver-field" |> Task.attempt (always Noop)
|
||||
)
|
||||
|
||||
ResultsHovered resultsHovered ->
|
||||
( { model | resultsHovered = resultsHovered }, Cmd.none )
|
||||
|
||||
UpdateReceiver receiver ->
|
||||
let
|
||||
matches =
|
||||
model.receivers
|
||||
|> List.sortBy (.name >> jaroWinkler receiver)
|
||||
|> List.reverse
|
||||
|> List.take 10
|
||||
|> (::) { name = "All", regex = "" }
|
||||
in
|
||||
( { model
|
||||
| fieldText = receiver
|
||||
, matches = matches
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
BlurReceiverField ->
|
||||
( { model | showReceivers = False }, Cmd.none )
|
||||
|
||||
Select maybeReceiver ->
|
||||
( { model | selectedReceiver = maybeReceiver }, Cmd.none )
|
||||
|
||||
FilterByReceiver regex ->
|
||||
( { model | showReceivers = False, resultsHovered = False }
|
||||
, Navigation.newUrl
|
||||
(url
|
||||
++ generateQueryString
|
||||
{ filter
|
||||
| receiver =
|
||||
if regex == "" then
|
||||
Nothing
|
||||
else
|
||||
Just regex
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Noop ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
fetchReceivers : String -> Cmd Msg
|
||||
fetchReceivers =
|
||||
Api.fetchReceivers >> Cmd.map ReceiversFetched
|
|
@ -0,0 +1,113 @@
|
|||
module Views.ReceiverBar.Views exposing (view)
|
||||
|
||||
import Html exposing (Html, li, div, text, input)
|
||||
import Html.Attributes exposing (class, style, tabindex, value, id)
|
||||
import Html.Events exposing (onBlur, onClick, onInput, onMouseEnter, onMouseLeave)
|
||||
import Views.ReceiverBar.Types exposing (Model, Msg(..))
|
||||
import Alerts.Types exposing (Receiver)
|
||||
import Utils.Keyboard exposing (keys, onKeyUp, onKeyDown)
|
||||
import Utils.List
|
||||
|
||||
|
||||
view : Maybe String -> Model -> Html Msg
|
||||
view maybeRegex model =
|
||||
if model.showReceivers || model.resultsHovered then
|
||||
viewDropdown model
|
||||
else
|
||||
viewResult maybeRegex model.receivers
|
||||
|
||||
|
||||
viewResult : Maybe String -> List Receiver -> Html Msg
|
||||
viewResult maybeRegex receivers =
|
||||
let
|
||||
unescapedReceiver =
|
||||
receivers
|
||||
|> List.filter (.regex >> Just >> (==) maybeRegex)
|
||||
|> List.map (.name >> Just)
|
||||
|> List.head
|
||||
|> Maybe.withDefault maybeRegex
|
||||
in
|
||||
li
|
||||
[ class ("nav-item ml-auto")
|
||||
, tabindex 1
|
||||
, style
|
||||
[ ( "position", "relative" )
|
||||
, ( "outline", "none" )
|
||||
]
|
||||
]
|
||||
[ div
|
||||
[ onClick EditReceivers
|
||||
, class "mt-1 mr-4"
|
||||
, style [ ( "cursor", "pointer" ) ]
|
||||
]
|
||||
[ text ("Receiver: " ++ Maybe.withDefault "All" unescapedReceiver) ]
|
||||
]
|
||||
|
||||
|
||||
viewDropdown : Model -> Html Msg
|
||||
viewDropdown { matches, fieldText, selectedReceiver } =
|
||||
let
|
||||
nextMatch =
|
||||
selectedReceiver
|
||||
|> Maybe.map (flip Utils.List.nextElem <| matches)
|
||||
|> Maybe.withDefault (List.head matches)
|
||||
|
||||
prevMatch =
|
||||
selectedReceiver
|
||||
|> Maybe.map (flip Utils.List.nextElem <| List.reverse matches)
|
||||
|> Maybe.withDefault (Utils.List.lastElem matches)
|
||||
|
||||
keyDown key =
|
||||
if key == keys.down then
|
||||
Select nextMatch
|
||||
else if key == keys.up then
|
||||
Select prevMatch
|
||||
else if key == keys.enter then
|
||||
selectedReceiver
|
||||
|> Maybe.map .regex
|
||||
|> Maybe.withDefault fieldText
|
||||
|> FilterByReceiver
|
||||
else
|
||||
Noop
|
||||
in
|
||||
li
|
||||
[ class ("nav-item ml-auto mr-4 autocomplete-menu show")
|
||||
, onMouseEnter (ResultsHovered True)
|
||||
, onMouseLeave (ResultsHovered False)
|
||||
, style
|
||||
[ ( "position", "relative" )
|
||||
, ( "outline", "none" )
|
||||
]
|
||||
]
|
||||
[ input
|
||||
[ id "receiver-field"
|
||||
, value fieldText
|
||||
, onBlur BlurReceiverField
|
||||
, onInput UpdateReceiver
|
||||
, onKeyDown keyDown
|
||||
, class "mr-4"
|
||||
, style
|
||||
[ ( "display", "block" )
|
||||
, ( "width", "100%" )
|
||||
]
|
||||
]
|
||||
[]
|
||||
, matches
|
||||
|> List.map (receiverField selectedReceiver)
|
||||
|> div [ class "dropdown-menu dropdown-menu-right" ]
|
||||
]
|
||||
|
||||
|
||||
receiverField : Maybe Receiver -> Receiver -> Html Msg
|
||||
receiverField selected receiver =
|
||||
let
|
||||
attrs =
|
||||
if selected == Just receiver then
|
||||
[ class "dropdown-item active" ]
|
||||
else
|
||||
[ class "dropdown-item"
|
||||
, style [ ( "cursor", "pointer" ) ]
|
||||
, onClick (FilterByReceiver receiver.regex)
|
||||
]
|
||||
in
|
||||
div attrs [ text receiver.name ]
|
|
@ -1,8 +1,8 @@
|
|||
module GroupBar exposing (..)
|
||||
module Match exposing (..)
|
||||
|
||||
import Test exposing (..)
|
||||
import Expect
|
||||
import Views.GroupBar.Match exposing (jaroWinkler, commonPrefix)
|
||||
import Utils.Match exposing (jaroWinkler, commonPrefix)
|
||||
|
||||
|
||||
testJaroWinkler : Test
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue