2020-03-31 03:22:27 +00:00
|
|
|
module Main exposing (main)
|
2020-03-28 04:09:01 +00:00
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
import Base64
|
2020-03-28 04:09:01 +00:00
|
|
|
import Browser exposing (UrlRequest(..))
|
|
|
|
import Browser.Navigation as Nav exposing (Key)
|
2020-03-31 03:22:27 +00:00
|
|
|
import Html
|
|
|
|
exposing
|
|
|
|
( Html
|
|
|
|
, button
|
|
|
|
, div
|
|
|
|
, h1
|
|
|
|
, header
|
|
|
|
, input
|
|
|
|
, li
|
2020-04-07 05:05:50 +00:00
|
|
|
, pre
|
2020-03-31 03:22:27 +00:00
|
|
|
, text
|
|
|
|
, ul
|
|
|
|
)
|
|
|
|
import Html.Attributes
|
|
|
|
exposing
|
|
|
|
( class
|
|
|
|
, type_
|
|
|
|
, value
|
|
|
|
)
|
|
|
|
import Html.Events
|
|
|
|
exposing
|
|
|
|
( onClick
|
|
|
|
, onInput
|
|
|
|
)
|
2020-04-07 05:05:50 +00:00
|
|
|
import Http
|
|
|
|
import Json.Decode as D
|
|
|
|
import Json.Decode.Pipeline as DP
|
|
|
|
import Json.Encode as E
|
|
|
|
import RemoteData as R
|
2020-03-28 04:09:01 +00:00
|
|
|
import Url exposing (Url)
|
2020-03-31 03:22:27 +00:00
|
|
|
import Url.Parser as UrlParser
|
|
|
|
exposing
|
|
|
|
( (<?>)
|
|
|
|
, Parser
|
|
|
|
)
|
2020-03-31 00:59:06 +00:00
|
|
|
import Url.Parser.Query as UrlParserQuery
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- MODEL
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
type alias Flags =
|
|
|
|
{ elasticsearchUrl : String
|
|
|
|
, elasticsearchUsername : String
|
|
|
|
, elasticsearchPassword : String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-28 04:09:01 +00:00
|
|
|
type alias Model =
|
|
|
|
{ key : Key
|
2020-04-07 05:05:50 +00:00
|
|
|
, elasticsearchUrl : String
|
|
|
|
, elasticsearchUsername : String
|
|
|
|
, elasticsearchPassword : String
|
2020-03-28 04:09:01 +00:00
|
|
|
, page : Page
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-31 00:59:06 +00:00
|
|
|
type alias SearchModel =
|
|
|
|
{ query : String
|
2020-04-07 05:05:50 +00:00
|
|
|
, result : R.WebData SearchResult
|
2020-03-31 00:59:06 +00:00
|
|
|
}
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
type Page
|
2020-04-07 05:05:50 +00:00
|
|
|
= SearchPage SearchModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
--| PackagePage SearchModel
|
|
|
|
--| MaintainerPage SearchModel
|
2020-03-31 00:59:06 +00:00
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
type alias SearchResult =
|
|
|
|
{ hits : SearchResultHits
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultHits =
|
|
|
|
{ total : SearchResultHitsTotal
|
|
|
|
, max_score : Maybe Float
|
|
|
|
, hits : List SearchResultItem
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultHitsTotal =
|
|
|
|
{ value : Int
|
|
|
|
, relation : String -- TODO: this should probably be Enum
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultItem =
|
|
|
|
{ index : String
|
|
|
|
, id : String
|
|
|
|
, score : Float
|
|
|
|
, source : SearchResultItemSource
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SearchResultItemSource
|
2020-03-31 03:22:27 +00:00
|
|
|
= Package SearchResultPackage
|
|
|
|
| Option SearchResultOption
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultPackage =
|
2020-04-07 05:05:50 +00:00
|
|
|
{ attr_name : String
|
2020-03-31 03:22:27 +00:00
|
|
|
, name : String
|
|
|
|
, version : String
|
2020-04-07 05:05:50 +00:00
|
|
|
, description : Maybe String
|
|
|
|
, longDescription : Maybe String
|
|
|
|
, licenses : List SearchResultPackageLicense
|
|
|
|
, maintainers : List SearchResultPackageMaintainer
|
|
|
|
, position : Maybe String
|
|
|
|
, homepage : Maybe String
|
2020-03-31 03:22:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultOption =
|
|
|
|
{ option_name : String
|
|
|
|
, description : String
|
|
|
|
, type_ : String
|
|
|
|
, default : String
|
|
|
|
, example : String
|
|
|
|
, source : String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultPackageLicense =
|
|
|
|
{ fullName : String
|
2020-04-07 05:05:50 +00:00
|
|
|
, url : Maybe String
|
2020-03-31 03:22:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchResultPackageMaintainer =
|
|
|
|
{ name : String
|
|
|
|
, email : String
|
|
|
|
, github : String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emptySearch : Page
|
2020-03-31 00:59:06 +00:00
|
|
|
emptySearch =
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage { query = "", result = R.NotAsked }
|
2020-03-31 00:59:06 +00:00
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
init : Flags -> Url -> Key -> ( Model, Cmd Msg )
|
|
|
|
init flags url key =
|
2020-03-31 00:59:06 +00:00
|
|
|
( { key = key
|
2020-04-07 05:05:50 +00:00
|
|
|
, elasticsearchUrl = flags.elasticsearchUrl
|
|
|
|
, elasticsearchUsername = flags.elasticsearchUsername
|
|
|
|
, elasticsearchPassword = flags.elasticsearchPassword
|
2020-03-31 00:59:06 +00:00
|
|
|
, page = UrlParser.parse urlParser url |> Maybe.withDefault emptySearch
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- URL Parsing and Routing
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
|
|
|
handleUrlRequest : Key -> UrlRequest -> Cmd msg
|
|
|
|
handleUrlRequest key urlRequest =
|
|
|
|
case urlRequest of
|
|
|
|
Internal url ->
|
|
|
|
Nav.pushUrl key (Url.toString url)
|
|
|
|
|
|
|
|
External url ->
|
|
|
|
Nav.load url
|
|
|
|
|
|
|
|
|
|
|
|
urlParser : Parser (Page -> msg) msg
|
|
|
|
urlParser =
|
|
|
|
UrlParser.oneOf
|
2020-03-31 00:59:06 +00:00
|
|
|
[ UrlParser.map
|
|
|
|
(\q ->
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage
|
2020-03-31 00:59:06 +00:00
|
|
|
{ query = q |> Maybe.withDefault ""
|
2020-04-07 05:05:50 +00:00
|
|
|
, result = R.NotAsked
|
2020-03-31 00:59:06 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
(UrlParser.s "search" <?> UrlParserQuery.string "query")
|
2020-03-28 04:09:01 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- UPDATE
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
|
|
|
type Msg
|
|
|
|
= OnUrlRequest UrlRequest
|
|
|
|
| OnUrlChange Url
|
2020-03-31 00:59:06 +00:00
|
|
|
| SearchPageInput String
|
|
|
|
| SearchQuerySubmit
|
2020-04-07 05:05:50 +00:00
|
|
|
| SearchQueryResponse (R.WebData SearchResult)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResult : D.Decoder SearchResult
|
|
|
|
decodeResult =
|
|
|
|
D.map SearchResult
|
|
|
|
(D.field "hits" decodeResultHits)
|
2020-03-31 00:59:06 +00:00
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
decodeResultHits : D.Decoder SearchResultHits
|
|
|
|
decodeResultHits =
|
|
|
|
D.map3 SearchResultHits
|
|
|
|
(D.field "total" decodeResultHitsTotal)
|
|
|
|
(D.field "max_score" (D.nullable D.float))
|
|
|
|
(D.field "hits" (D.list decodeResultItem))
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultHitsTotal : D.Decoder SearchResultHitsTotal
|
|
|
|
decodeResultHitsTotal =
|
|
|
|
D.map2 SearchResultHitsTotal
|
|
|
|
(D.field "value" D.int)
|
|
|
|
(D.field "relation" D.string)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultItem : D.Decoder SearchResultItem
|
|
|
|
decodeResultItem =
|
|
|
|
D.map4 SearchResultItem
|
|
|
|
(D.field "_index" D.string)
|
|
|
|
(D.field "_id" D.string)
|
|
|
|
(D.field "_score" D.float)
|
|
|
|
(D.field "_source" decodeResultItemSource)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultItemSource : D.Decoder SearchResultItemSource
|
|
|
|
decodeResultItemSource =
|
|
|
|
D.oneOf
|
|
|
|
[ D.map Package decodeResultPackage
|
|
|
|
|
|
|
|
--, D.map Option decodeResultOption
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultPackage : D.Decoder SearchResultPackage
|
|
|
|
decodeResultPackage =
|
|
|
|
D.succeed SearchResultPackage
|
|
|
|
|> DP.required "attr_name" D.string
|
|
|
|
|> DP.required "name" D.string
|
|
|
|
|> DP.required "version" D.string
|
|
|
|
|> DP.required "description" (D.nullable D.string)
|
|
|
|
|> DP.required "longDescription" (D.nullable D.string)
|
|
|
|
|> DP.required "license" (D.list decodeResultPackageLicense)
|
|
|
|
|> DP.required "maintainers" (D.list decodeResultPackageMaintainer)
|
|
|
|
|> DP.required "position" (D.nullable D.string)
|
|
|
|
|> DP.required "homepage" (D.nullable D.string)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultPackageLicense : D.Decoder SearchResultPackageLicense
|
|
|
|
decodeResultPackageLicense =
|
|
|
|
D.map2 SearchResultPackageLicense
|
|
|
|
(D.field "fullName" D.string)
|
|
|
|
(D.field "url" (D.nullable D.string))
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultPackageMaintainer : D.Decoder SearchResultPackageMaintainer
|
|
|
|
decodeResultPackageMaintainer =
|
|
|
|
D.map3 SearchResultPackageMaintainer
|
|
|
|
(D.field "name" D.string)
|
|
|
|
(D.field "email" D.string)
|
|
|
|
(D.field "github" D.string)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultOption : D.Decoder SearchResultOption
|
|
|
|
decodeResultOption =
|
|
|
|
D.map6 SearchResultOption
|
|
|
|
(D.field "option_name" D.string)
|
|
|
|
(D.field "description" D.string)
|
|
|
|
(D.field "type" D.string)
|
|
|
|
(D.field "default" D.string)
|
|
|
|
(D.field "example" D.string)
|
|
|
|
(D.field "source" D.string)
|
|
|
|
|
|
|
|
|
|
|
|
initPage : Model -> Cmd Msg
|
|
|
|
initPage model =
|
|
|
|
case model.page of
|
|
|
|
SearchPage searchModel ->
|
|
|
|
if searchModel.query == "" then
|
|
|
|
Cmd.none
|
|
|
|
|
|
|
|
else
|
|
|
|
Http.riskyRequest
|
|
|
|
{ method = "POST"
|
|
|
|
, headers =
|
|
|
|
[ Http.header "Authorization" ("Basic " ++ Base64.encode (model.elasticsearchUsername ++ ":" ++ model.elasticsearchPassword))
|
|
|
|
]
|
|
|
|
, url = model.elasticsearchUrl ++ "/nixos-unstable-packages/_search"
|
|
|
|
, body =
|
|
|
|
Http.jsonBody <|
|
|
|
|
E.object
|
|
|
|
[ ( "query"
|
|
|
|
, E.object
|
|
|
|
[ ( "match"
|
|
|
|
, E.object
|
|
|
|
[ ( "name"
|
|
|
|
, E.object
|
|
|
|
[ ( "query", E.string searchModel.query )
|
|
|
|
, ( "fuzziness", E.int 1 )
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
, expect = Http.expectJson (R.fromResult >> SearchQueryResponse) decodeResult
|
|
|
|
, timeout = Nothing
|
|
|
|
, tracker = Nothing
|
|
|
|
}
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
|
|
update message model =
|
|
|
|
case message of
|
|
|
|
OnUrlRequest urlRequest ->
|
|
|
|
( model, handleUrlRequest model.key urlRequest )
|
|
|
|
|
|
|
|
OnUrlChange url ->
|
|
|
|
let
|
2020-03-31 00:59:06 +00:00
|
|
|
newModel =
|
|
|
|
{ model | page = UrlParser.parse urlParser url |> Maybe.withDefault model.page }
|
2020-03-31 03:22:27 +00:00
|
|
|
|
|
|
|
newPage =
|
2020-03-31 00:59:06 +00:00
|
|
|
case newModel.page of
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage searchModel ->
|
|
|
|
SearchPage
|
2020-03-31 00:59:06 +00:00
|
|
|
{ searchModel
|
2020-04-07 05:05:50 +00:00
|
|
|
| result =
|
2020-03-31 00:59:06 +00:00
|
|
|
if searchModel.query == "" then
|
2020-04-07 05:05:50 +00:00
|
|
|
R.NotAsked
|
2020-03-31 00:59:06 +00:00
|
|
|
|
|
|
|
else
|
2020-04-07 05:05:50 +00:00
|
|
|
R.Loading
|
2020-03-31 00:59:06 +00:00
|
|
|
}
|
2020-04-07 05:05:50 +00:00
|
|
|
|
|
|
|
newNewModel =
|
|
|
|
{ newModel | page = newPage }
|
2020-03-31 03:22:27 +00:00
|
|
|
in
|
2020-04-07 05:05:50 +00:00
|
|
|
( newNewModel
|
|
|
|
, initPage newNewModel
|
2020-03-28 04:09:01 +00:00
|
|
|
)
|
|
|
|
|
2020-03-31 00:59:06 +00:00
|
|
|
SearchPageInput query ->
|
|
|
|
( { model
|
|
|
|
| page =
|
|
|
|
case model.page of
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage searchModel ->
|
|
|
|
SearchPage { searchModel | query = query }
|
2020-03-31 00:59:06 +00:00
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
2020-03-28 04:09:01 +00:00
|
|
|
|
2020-03-31 00:59:06 +00:00
|
|
|
SearchQuerySubmit ->
|
|
|
|
case model.page of
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage searchModel ->
|
2020-03-31 00:59:06 +00:00
|
|
|
( model
|
|
|
|
, Nav.pushUrl model.key <| "/search?query=" ++ searchModel.query
|
|
|
|
)
|
2020-03-28 04:09:01 +00:00
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchQueryResponse result ->
|
|
|
|
case model.page of
|
|
|
|
SearchPage searchModel ->
|
|
|
|
let
|
|
|
|
newPage =
|
|
|
|
SearchPage { searchModel | result = result }
|
|
|
|
in
|
|
|
|
( { model | page = newPage }
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- VIEW
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
|
|
|
view : Model -> Html Msg
|
|
|
|
view model =
|
|
|
|
div [ class "container" ]
|
|
|
|
[ header []
|
2020-03-31 00:59:06 +00:00
|
|
|
[ h1 [] [ text "NixOS Search" ]
|
2020-03-28 04:09:01 +00:00
|
|
|
]
|
|
|
|
, case model.page of
|
2020-04-07 05:05:50 +00:00
|
|
|
SearchPage searchModel ->
|
2020-03-31 00:59:06 +00:00
|
|
|
searchPage searchModel
|
2020-03-28 04:09:01 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-03-31 00:59:06 +00:00
|
|
|
searchPage : SearchModel -> Html Msg
|
|
|
|
searchPage model =
|
|
|
|
div []
|
|
|
|
[ div []
|
|
|
|
[ input
|
|
|
|
[ type_ "text"
|
|
|
|
, onInput SearchPageInput
|
|
|
|
, value model.query
|
|
|
|
]
|
|
|
|
[]
|
|
|
|
, button [ onClick SearchQuerySubmit ] [ text "Search" ]
|
2020-03-28 04:09:01 +00:00
|
|
|
]
|
2020-04-07 05:05:50 +00:00
|
|
|
, case model.result of
|
|
|
|
R.NotAsked ->
|
|
|
|
div [] [ text "NotAsked" ]
|
|
|
|
|
|
|
|
R.Loading ->
|
|
|
|
div [] [ text "Loading" ]
|
|
|
|
|
|
|
|
R.Success result ->
|
|
|
|
ul [] (searchPageResult result.hits)
|
|
|
|
|
|
|
|
R.Failure error ->
|
|
|
|
div [] [ text "Error!", pre [] [ text (Debug.toString error) ] ]
|
2020-03-28 04:09:01 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
searchPageResult : SearchResultHits -> List (Html Msg)
|
2020-03-31 03:22:27 +00:00
|
|
|
searchPageResult result =
|
2020-04-07 05:05:50 +00:00
|
|
|
List.map searchPageResultItem result.hits
|
|
|
|
|
2020-03-31 03:22:27 +00:00
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
searchPageResultItem : SearchResultItem -> Html Msg
|
|
|
|
searchPageResultItem item =
|
|
|
|
-- case item.source of
|
|
|
|
-- Package package ->
|
|
|
|
-- li [] [ text package.attr_name ]
|
|
|
|
-- Option option ->
|
|
|
|
-- li [] [ text option.option_name ]
|
|
|
|
li [] [ text <| Debug.toString item ]
|
2020-03-28 04:09:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- MAIN
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
2020-04-07 05:05:50 +00:00
|
|
|
main : Program Flags Model Msg
|
2020-03-28 04:09:01 +00:00
|
|
|
main =
|
|
|
|
Browser.application
|
|
|
|
{ init = init
|
|
|
|
, update = update
|
|
|
|
, view =
|
|
|
|
\m ->
|
2020-03-31 00:59:06 +00:00
|
|
|
{ title = "NixOS Search"
|
2020-03-28 04:09:01 +00:00
|
|
|
, body = [ view m ]
|
|
|
|
}
|
|
|
|
, subscriptions = \_ -> Sub.none
|
|
|
|
, onUrlRequest = OnUrlRequest
|
|
|
|
, onUrlChange = OnUrlChange
|
|
|
|
}
|