Receiver autocomplete (#900)

* Extract receivers

* Moved Match into Utils

* Implemented autocomplete

* Update bindata.go
This commit is contained in:
Andrey Kuzmin 2017-07-11 11:25:03 +02:00 committed by GitHub
parent dfbac123db
commit f4c11751a9
13 changed files with 272 additions and 124 deletions

View File

@ -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)))
)

View File

@ -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 }

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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) ]

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 ]

View File

@ -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