From c47f1c0ccb09f41dc33fcca07cc440bfc0f4d220 Mon Sep 17 00:00:00 2001 From: Rok Garbas Date: Mon, 11 May 2020 21:56:10 +0200 Subject: [PATCH] Pagination of search results (#20) --- src/ElasticSearch.elm | 218 ++++++++++++++++++++++++++++++++++-------- src/Main.elm | 16 +++- src/Page/Options.elm | 11 ++- src/Page/Packages.elm | 24 ++++- src/Route.elm | 28 ++++-- 5 files changed, 240 insertions(+), 57 deletions(-) diff --git a/src/ElasticSearch.elm b/src/ElasticSearch.elm index 4ea676b..5c3d195 100644 --- a/src/ElasticSearch.elm +++ b/src/ElasticSearch.elm @@ -7,7 +7,6 @@ module ElasticSearch exposing , decodeResult , init , makeRequest - , showLoadingOnQuery , update , view ) @@ -17,23 +16,34 @@ import Browser.Navigation import Html exposing ( Html + , a , button , div + , em , form , h1 , input + , li + , p + , pre , text + , ul ) import Html.Attributes exposing ( class + , classList + , href , type_ , value ) import Html.Events exposing - ( onInput + ( custom + , onClick + , onInput , onSubmit + , preventDefaultOn ) import Http import Json.Decode @@ -46,6 +56,8 @@ type alias Model a = { query : Maybe String , result : RemoteData.WebData (Result a) , showDetailsFor : Maybe String + , from : Int + , size : Int } @@ -78,11 +90,15 @@ type alias ResultItem a = init : Maybe String -> Maybe String + -> Maybe Int + -> Maybe Int -> ( Model a, Cmd msg ) -init query showDetailsFor = +init query showDetailsFor from size = ( { query = query , result = RemoteData.NotAsked , showDetailsFor = showDetailsFor + , from = Maybe.withDefault 0 from + , size = Maybe.withDefault 15 size } , Cmd.none ) @@ -95,7 +111,8 @@ init query showDetailsFor = type Msg a - = QueryInput String + = NoOp + | QueryInput String | QuerySubmit | QueryResponse (RemoteData.WebData (Result a)) | ShowDetails String @@ -109,6 +126,11 @@ update : -> ( Model a, Cmd (Msg a) ) update path navKey msg model = case msg of + NoOp -> + ( model + , Cmd.none + ) + QueryInput query -> ( { model | query = Just query } , Cmd.none @@ -116,7 +138,11 @@ update path navKey msg model = QuerySubmit -> ( model - , createUrl path model.query model.showDetailsFor + , createUrl path + model.query + model.showDetailsFor + 0 + model.size |> Browser.Navigation.pushUrl navKey ) @@ -135,27 +161,23 @@ update path navKey msg model = else Just selected ) + model.from + model.size |> Browser.Navigation.pushUrl navKey ) -showLoadingOnQuery : Model a -> Model a -showLoadingOnQuery model = - -- TODO: use this - { model - | result = - case model.query of - Just query -> - RemoteData.Loading - - Nothing -> - RemoteData.NotAsked - } - - -createUrl : String -> Maybe String -> Maybe String -> String -createUrl path query showDetailsFor = - [] +createUrl : + String + -> Maybe String + -> Maybe String + -> Int + -> Int + -> String +createUrl path query showDetailsFor from size = + [ Url.Builder.int "from" from + , Url.Builder.int "size" size + ] |> List.append (query |> Maybe.map @@ -181,14 +203,15 @@ createUrl path query showDetailsFor = view : - { title : String } + String + -> String -> Model a -> (Maybe String -> Result a -> Html b) -> (Msg a -> b) -> Html b -view options model viewSuccess outMsg = +view path title model viewSuccess outMsg = div [ class "search-page" ] - [ h1 [ class "page-header" ] [ text options.title ] + [ h1 [ class "page-header" ] [ text title ] , div [ class "search-input" ] [ form [ onSubmit (outMsg QuerySubmit) ] [ div [ class "input-append" ] @@ -200,15 +223,6 @@ view options model viewSuccess outMsg = [] , div [ class "btn-group" ] [ button [ class "btn" ] [ text "Search" ] - - --TODO: and option to select the right channel+version+evaluation - --, button [ class "btn" ] [ text "Loading ..." ] - --, div [ class "popover bottom" ] - -- [ div [ class "arrow" ] [] - -- , div [ class "popover-title" ] [ text "Select options" ] - -- , div [ class "popover-content" ] - -- [ p [] [ text "Sed posuere consectetur est at lobortis. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum." ] ] - -- ] ] ] ] @@ -221,7 +235,30 @@ view options model viewSuccess outMsg = div [] [ text "Loading" ] RemoteData.Success result -> - viewSuccess model.showDetailsFor result + div [] + [ p [] + [ em [] + [ text + ("Showing results " + ++ String.fromInt model.from + ++ "-" + ++ String.fromInt + (if model.from + model.size > result.hits.total.value then + result.hits.total.value + + else + model.from + model.size + ) + ++ " of " + ++ String.fromInt result.hits.total.value + ++ "." + ) + ] + ] + , viewPager outMsg model result path + , viewSuccess model.showDetailsFor result + , viewPager outMsg model result path + ] RemoteData.Failure error -> div [] @@ -232,6 +269,97 @@ view options model viewSuccess outMsg = ] +viewPager : + (Msg a -> b) + -> Model a + -> Result a + -> String + -> Html b +viewPager outMsg model result path = + ul [ class "pager" ] + [ li + [ classList + [ ( "disabled", model.from == 0 ) + ] + ] + [ a + [ if model.from == 0 then + href "#disabled" + + else + href <| + createUrl + path + model.query + model.showDetailsFor + 0 + model.size + ] + [ text "First" ] + ] + , li + [ classList + [ ( "disabled", model.from == 0 ) + ] + ] + [ a + [ href <| + if model.from - model.size < 0 then + "#disabled" + + else + createUrl + path + model.query + model.showDetailsFor + (model.from - model.size) + model.size + ] + [ text "Previous" ] + ] + , li + [ classList + [ ( "disabled", model.from + model.size >= result.hits.total.value ) + ] + ] + [ a + [ href <| + if model.from + model.size >= result.hits.total.value then + "#disabled" + + else + createUrl + path + model.query + model.showDetailsFor + (model.from + model.size) + model.size + ] + [ text "Next" ] + ] + , li + [ classList + [ ( "disabled", model.from + model.size >= result.hits.total.value ) + ] + ] + [ a + [ href <| + if model.from + model.size >= result.hits.total.value then + "#disabled" + + else + createUrl + path + model.query + model.showDetailsFor + ((result.hits.total.value // model.size) * model.size) + model.size + ] + [ text "Last" ] + ] + ] + + -- API @@ -243,8 +371,13 @@ type alias Options = } -makeRequestBody : String -> String -> Http.Body -makeRequestBody field query = +makeRequestBody : + String + -> String + -> Int + -> Int + -> Http.Body +makeRequestBody field query from size = let stringIn name value = [ ( name, Json.Encode.string value ) ] @@ -254,6 +387,7 @@ makeRequestBody field query = in -- Prefix Query -- { + -- "" -- "query": { -- "prefix": { -- "user": { @@ -285,6 +419,10 @@ makeRequestBody field query = |> objectIn field |> objectIn "wildcard" |> objectIn "query" + |> List.append + [ ( "from", Json.Encode.int from ) + , ( "size", Json.Encode.int size ) + ] |> Json.Encode.object |> Http.jsonBody @@ -295,15 +433,17 @@ makeRequest : -> Json.Decode.Decoder a -> Options -> String + -> Int + -> Int -> Cmd (Msg a) -makeRequest field index decodeResultItemSource options query = +makeRequest field index decodeResultItemSource options query from size = Http.riskyRequest { method = "POST" , headers = [ Http.header "Authorization" ("Basic " ++ Base64.encode (options.username ++ ":" ++ options.password)) ] , url = options.url ++ "/" ++ index ++ "/_search" - , body = makeRequestBody field query + , body = makeRequestBody field query from size , expect = Http.expectJson (RemoteData.fromResult >> QueryResponse) diff --git a/src/Main.elm b/src/Main.elm index 2ce0992..33b019a 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -117,6 +117,8 @@ submitQuery old ( new, cmd ) = , makeRequest new.elasticsearch (Maybe.withDefault "" newModel.query) + newModel.from + newModel.size |> Cmd.map msg ] ) @@ -170,13 +172,13 @@ changeRouteTo model url = -- on the home page ( newModel, Browser.Navigation.pushUrl newModel.navKey "/packages" ) - Just (Route.Packages query showDetailsFor) -> - Page.Packages.init query showDetailsFor + Just (Route.Packages query showDetailsFor from size) -> + Page.Packages.init query showDetailsFor from size |> updateWith Packages PackagesMsg newModel |> submitQuery newModel - Just (Route.Options query showDetailsFor) -> - Page.Options.init query showDetailsFor + Just (Route.Options query showDetailsFor from size) -> + Page.Options.init query showDetailsFor from size |> updateWith Options OptionsMsg newModel |> submitQuery newModel @@ -188,7 +190,11 @@ update msg model = case urlRequest of Browser.Internal url -> ( model - , Browser.Navigation.pushUrl model.navKey <| Url.toString url + , if url.fragment == Just "disabled" then + Cmd.none + + else + Browser.Navigation.pushUrl model.navKey <| Url.toString url ) Browser.External href -> diff --git a/src/Page/Options.elm b/src/Page/Options.elm index cb82dd4..05d758f 100644 --- a/src/Page/Options.elm +++ b/src/Page/Options.elm @@ -65,6 +65,8 @@ type alias ResultItemSource = init : Maybe String -> Maybe String + -> Maybe Int + -> Maybe Int -> ( Model, Cmd Msg ) init = ElasticSearch.init @@ -96,7 +98,8 @@ update navKey msg model = view : Model -> Html Msg view model = ElasticSearch.view - { title = "Search NixOS options" } + "options" + "Search NixOS options" model viewSuccess SearchMsg @@ -208,14 +211,18 @@ viewResultItemDetails item = makeRequest : ElasticSearch.Options -> String + -> Int + -> Int -> Cmd Msg -makeRequest options query = +makeRequest options query from size = ElasticSearch.makeRequest "option_name" "nixos-unstable-options" decodeResultItemSource options query + from + size |> Cmd.map SearchMsg diff --git a/src/Page/Packages.elm b/src/Page/Packages.elm index 4089569..e9ec289 100644 --- a/src/Page/Packages.elm +++ b/src/Page/Packages.elm @@ -74,13 +74,15 @@ type alias ResultPackageLicense = type alias ResultPackageMaintainer = { name : String , email : String - , github : String + , github : Maybe String } init : Maybe String -> Maybe String + -> Maybe Int + -> Maybe Int -> ( Model, Cmd Msg ) init = ElasticSearch.init @@ -112,7 +114,8 @@ update navKey msg model = view : Model -> Html Msg view model = ElasticSearch.view - { title = "Search NixOS packages" } + "packages" + "Search NixOS packages" model viewSuccess SearchMsg @@ -238,7 +241,14 @@ viewResultItemDetails item = showMaintainer maintainer = li [] [ a - [ href <| "https://github.com/" ++ maintainer.github ] + [ href <| + case maintainer.github of + Just github -> + "https://github.com/" ++ github + + Nothing -> + "#" + ] [ text <| maintainer.name ++ " <" ++ maintainer.email ++ ">" ] ] in @@ -269,14 +279,18 @@ viewResultItemDetails item = makeRequest : ElasticSearch.Options -> String + -> Int + -> Int -> Cmd Msg -makeRequest options query = +makeRequest options query from size = ElasticSearch.makeRequest "attr_name" "nixos-unstable-packages" decodeResultItemSource options query + from + size |> Cmd.map SearchMsg @@ -311,4 +325,4 @@ decodeResultPackageMaintainer = Json.Decode.map3 ResultPackageMaintainer (Json.Decode.field "name" Json.Decode.string) (Json.Decode.field "email" Json.Decode.string) - (Json.Decode.field "github" Json.Decode.string) + (Json.Decode.field "github" (Json.Decode.nullable Json.Decode.string)) diff --git a/src/Route.elm b/src/Route.elm index 1e3080f..7d49714 100644 --- a/src/Route.elm +++ b/src/Route.elm @@ -15,8 +15,8 @@ import Url.Parser.Query type Route = NotFound | Home - | Packages (Maybe String) (Maybe String) - | Options (Maybe String) (Maybe String) + | Packages (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) + | Options (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) parser : Url.Parser.Parser (Route -> msg) msg @@ -33,12 +33,16 @@ parser = (Url.Parser.s "packages" Url.Parser.Query.string "query" Url.Parser.Query.string "showDetailsFor" + Url.Parser.Query.int "from" + Url.Parser.Query.int "size" ) , Url.Parser.map Options (Url.Parser.s "options" Url.Parser.Query.string "query" Url.Parser.Query.string "showDetailsFor" + Url.Parser.Query.int "from" + Url.Parser.Query.int "size" ) ] @@ -88,8 +92,20 @@ routeToPieces page = NotFound -> ( [ "not-found" ], [] ) - Packages query showDetailsFor -> - ( [ "packages" ], [ query, showDetailsFor ] ) + Packages query showDetailsFor from size -> + ( [ "packages" ] + , [ query + , showDetailsFor + , Maybe.map String.fromInt from + , Maybe.map String.fromInt size + ] + ) - Options query showDetailsFor -> - ( [ "options" ], [ query, showDetailsFor ] ) + Options query showDetailsFor from size -> + ( [ "options" ] + , [ query + , showDetailsFor + , Maybe.map String.fromInt from + , Maybe.map String.fromInt size + ] + )