somehow in a working starte with bugs and missing features

This commit is contained in:
Rok Garbas 2020-05-08 15:24:58 +02:00
parent f0318458d8
commit 387d70eaa3
Failed to generate hash of commit
9 changed files with 1015 additions and 503 deletions

View file

@ -54,7 +54,7 @@ def get_packages(evaluation):
check=True,
)
packages = json.loads(result.stdout).items()
packages = list(packages)[:10]
packages = list(packages)
def gen():
for attr_name, data in packages:
@ -123,7 +123,7 @@ def get_options(evaluation):
if os.path.exists(options_file):
with open(options_file) as f:
options = json.load(f).items()
options = list(options)[:10]
options = list(options)
def gen():
for name, option in options:

315
src/ElasticSearch.elm Normal file
View file

@ -0,0 +1,315 @@
module ElasticSearch exposing
( Model
, Msg(..)
, Options
, Result
, ResultItem
, decodeResult
, init
, makeRequest
, showLoadingOnQuery
, update
, view
)
import Base64
import Browser.Navigation
import Html
exposing
( Html
, button
, div
, form
, h1
, input
, text
)
import Html.Attributes
exposing
( class
, type_
, value
)
import Html.Events
exposing
( onInput
, onSubmit
)
import Http
import Json.Decode
import Json.Encode
import RemoteData
import Url.Builder
type alias Model a =
{ query : Maybe String
, result : RemoteData.WebData (Result a)
, showDetailsFor : Maybe String
}
type alias Result a =
{ hits : ResultHits a
}
type alias ResultHits a =
{ total : ResultHitsTotal
, max_score : Maybe Float
, hits : List (ResultItem a)
}
type alias ResultHitsTotal =
{ value : Int
, relation : String -- TODO: this should probably be Enum
}
type alias ResultItem a =
{ index : String
, id : String
, score : Float
, source : a
}
init :
Maybe String
-> Maybe String
-> ( Model a, Cmd msg )
init query showDetailsFor =
( { query = query
, result = RemoteData.NotAsked
, showDetailsFor = showDetailsFor
}
, Cmd.none
)
-- ---------------------------
-- UPDATE
-- ---------------------------
type Msg a
= QueryInput String
| QuerySubmit
| QueryResponse (RemoteData.WebData (Result a))
| ShowDetails String
update :
String
-> Browser.Navigation.Key
-> Msg a
-> Model a
-> ( Model a, Cmd (Msg a) )
update path navKey msg model =
case msg of
QueryInput query ->
( { model | query = Just query }
, Cmd.none
)
QuerySubmit ->
( model
, createUrl path model.query model.showDetailsFor
|> Browser.Navigation.pushUrl navKey
)
QueryResponse result ->
( { model | result = result }
, Cmd.none
)
ShowDetails selected ->
( { model | showDetailsFor = Just selected }
, Cmd.none
)
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 =
[]
|> List.append
(query
|> Maybe.map
(\x ->
[ Url.Builder.string "query" x ]
)
|> Maybe.withDefault []
)
|> List.append
(showDetailsFor
|> Maybe.map
(\x ->
[ Url.Builder.string "showDetailsFor" x
]
)
|> Maybe.withDefault []
)
|> Url.Builder.absolute [ path ]
-- VIEW
view :
{ title : String }
-> Model a
-> (Maybe String -> Result a -> Html b)
-> (Msg a -> b)
-> Html b
view options model viewSuccess outMsg =
div [ class "search-page" ]
[ h1 [ class "page-header" ] [ text options.title ]
, div [ class "search-input" ]
[ form [ onSubmit (outMsg QuerySubmit) ]
[ div [ class "input-append" ]
[ input
[ type_ "text"
, onInput (\x -> outMsg (QueryInput x))
, value <| Maybe.withDefault "" model.query
]
[]
, 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." ] ]
-- ]
]
]
]
]
, case model.result of
RemoteData.NotAsked ->
div [] [ text "NotAsked" ]
RemoteData.Loading ->
div [] [ text "Loading" ]
RemoteData.Success result ->
viewSuccess model.showDetailsFor result
RemoteData.Failure error ->
div []
[ text "Error!"
--, pre [] [ text (Debug.toString error) ]
]
]
-- API
type alias Options =
{ url : String
, username : String
, password : String
}
makeRequestBody : String -> String -> Http.Body
makeRequestBody field query =
let
stringIn name value =
[ ( name, Json.Encode.string value ) ]
objectIn name object =
[ ( name, Json.Encode.object object ) ]
in
-- I'm not sure we need fuziness
--, ( "fuzziness", Json.Encode.int 1 )
query
|> stringIn "query"
|> objectIn field
|> objectIn "match"
|> objectIn "query"
|> Json.Encode.object
|> Http.jsonBody
makeRequest :
String
-> String
-> Json.Decode.Decoder a
-> Options
-> String
-> Cmd (Msg a)
makeRequest field index decodeResultItemSource options query =
Http.riskyRequest
{ method = "POST"
, headers =
[ Http.header "Authorization" ("Basic " ++ Base64.encode (options.username ++ ":" ++ options.password))
]
, url = options.url ++ "/" ++ index ++ "/_search"
, body = makeRequestBody field query
, expect =
Http.expectJson
(RemoteData.fromResult >> QueryResponse)
(decodeResult decodeResultItemSource)
, timeout = Nothing
, tracker = Nothing
}
-- JSON
decodeResult :
Json.Decode.Decoder a
-> Json.Decode.Decoder (Result a)
decodeResult decodeResultItemSource =
Json.Decode.map Result
(Json.Decode.field "hits" (decodeResultHits decodeResultItemSource))
decodeResultHits : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultHits a)
decodeResultHits decodeResultItemSource =
Json.Decode.map3 ResultHits
(Json.Decode.field "total" decodeResultHitsTotal)
(Json.Decode.field "max_score" (Json.Decode.nullable Json.Decode.float))
(Json.Decode.field "hits" (Json.Decode.list (decodeResultItem decodeResultItemSource)))
decodeResultHitsTotal : Json.Decode.Decoder ResultHitsTotal
decodeResultHitsTotal =
Json.Decode.map2 ResultHitsTotal
(Json.Decode.field "value" Json.Decode.int)
(Json.Decode.field "relation" Json.Decode.string)
decodeResultItem : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultItem a)
decodeResultItem decodeResultItemSource =
Json.Decode.map4 ResultItem
(Json.Decode.field "_index" Json.Decode.string)
(Json.Decode.field "_id" Json.Decode.string)
(Json.Decode.field "_score" Json.Decode.float)
(Json.Decode.field "_source" decodeResultItemSource)

View file

@ -1,66 +1,39 @@
module Main exposing (main)
import Base64
import Browser exposing (UrlRequest(..))
import Browser.Navigation as Nav exposing (Key)
--exposing (UrlRequest(..))
import Browser
import Browser.Navigation
import ElasticSearch
import Html
exposing
( Html
, a
, button
, div
, footer
, form
, h1
, header
, img
, input
, li
, p
, pre
, table
, tbody
, td
, text
, th
, thead
, tr
, ul
)
import Html.Attributes
exposing
( class
, colspan
, classList
, href
, src
, type_
, value
)
import Html.Events
exposing
( onClick
, onInput
, onSubmit
)
import Http
import Json.Decode as D
import Json.Decode.Pipeline as DP
import Json.Encode as E
import RemoteData as R
import Url exposing (Url)
import Url.Builder as UrlBuilder
import Url.Parser as UrlParser
exposing
( (<?>)
, Parser
)
import Url.Parser.Query as UrlParserQuery
import Page.Home
import Page.Options
import Page.Packages
import RemoteData
import Route
import Url
-- ---------------------------
-- MODEL
-- ---------------------------
type alias Flags =
@ -71,403 +44,174 @@ type alias Flags =
type alias Model =
{ key : Key
, elasticsearchUrl : String
, elasticsearchUsername : String
, elasticsearchPassword : String
{ navKey : Browser.Navigation.Key
, url : Url.Url
, elasticsearch : ElasticSearch.Options
, page : Page
}
type alias SearchModel =
{ query : Maybe String
, result : R.WebData SearchResult
, showDetailsFor : Maybe String
}
type Page
= SearchPage SearchModel
= NotFound
| Home Page.Home.Model
| Packages Page.Packages.Model
| Options Page.Options.Model
--| PackagePage SearchModel
--| MaintainerPage SearchModel
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
= Package SearchResultPackage
| Option SearchResultOption
type alias SearchResultPackage =
{ attr_name : String
, name : String
, version : String
, description : Maybe String
, longDescription : Maybe String
, licenses : List SearchResultPackageLicense
, maintainers : List SearchResultPackageMaintainer
, platforms : List String
, position : Maybe String
, homepage : Maybe String
}
type alias SearchResultOption =
{ option_name : String
, description : String
, type_ : String
, default : String
, example : String
, source : String
}
type alias SearchResultPackageLicense =
{ fullName : Maybe String
, url : Maybe String
}
type alias SearchResultPackageMaintainer =
{ name : String
, email : String
, github : String
}
emptySearch : Page
emptySearch =
SearchPage
{ query = Nothing
, result = R.NotAsked
, showDetailsFor = Nothing
}
init : Flags -> Url -> Key -> ( Model, Cmd Msg )
init flags url key =
init :
Flags
-> Url.Url
-> Browser.Navigation.Key
-> ( Model, Cmd Msg )
init flags url navKey =
let
model =
{ key = key
, elasticsearchUrl = flags.elasticsearchUrl
, elasticsearchUsername = flags.elasticsearchUsername
, elasticsearchPassword = flags.elasticsearchPassword
, page = UrlParser.parse urlParser url |> Maybe.withDefault emptySearch
{ navKey = navKey
, url = url
, elasticsearch =
ElasticSearch.Options
flags.elasticsearchUrl
flags.elasticsearchUsername
flags.elasticsearchPassword
, page = NotFound
}
in
( model
, initPageCmd model model
)
initPageCmd : Model -> Model -> Cmd Msg
initPageCmd oldModel model =
let
makeRequest query =
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
[ ( "attr_name"
, E.object
[ ( "query", E.string query )
-- I'm not sure we need fuziness
--, ( "fuzziness", E.int 1 )
]
)
]
)
]
)
]
, expect = Http.expectJson (R.fromResult >> SearchQueryResponse) decodeResult
, timeout = Nothing
, tracker = Nothing
}
in
case oldModel.page of
SearchPage oldSearchModel ->
case model.page of
SearchPage searchModel ->
if (oldSearchModel.query == searchModel.query) && R.isSuccess oldSearchModel.result then
Cmd.none
else
searchModel.query
|> Maybe.map makeRequest
|> Maybe.withDefault Cmd.none
changeRouteTo model url
-- ---------------------------
-- 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
[ UrlParser.map
(\query showDetailsFor ->
SearchPage
{ query = query
, result = R.NotAsked
, showDetailsFor = showDetailsFor
}
)
(UrlParser.s "search" <?> UrlParserQuery.string "query" <?> UrlParserQuery.string "showDetailsFor")
]
-- ---------------------------
-- UPDATE
-- ---------------------------
type Msg
= OnUrlRequest UrlRequest
| OnUrlChange Url
| SearchPageInput String
| SearchQuerySubmit
| SearchQueryResponse (R.WebData SearchResult)
| SearchShowPackageDetails String
= ChangedUrl Url.Url
| ClickedLink Browser.UrlRequest
| HomeMsg Page.Home.Msg
| PackagesMsg Page.Packages.Msg
| OptionsMsg Page.Options.Msg
decodeResult : D.Decoder SearchResult
decodeResult =
D.map SearchResult
(D.field "hits" decodeResultHits)
updateWith :
(subModel -> Page)
-> (subMsg -> Msg)
-> Model
-> ( subModel, Cmd subMsg )
-> ( Model, Cmd Msg )
updateWith toPage toMsg model ( subModel, subCmd ) =
( { model | page = toPage subModel }
, Cmd.map toMsg subCmd
)
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))
submitQuery :
Model
-> ( Model, Cmd Msg )
-> ( Model, Cmd Msg )
submitQuery old ( new, cmd ) =
let
triggerSearch oldModel newModel msg makeRequest =
if (oldModel.query == newModel.query) && RemoteData.isSuccess oldModel.result then
( new, cmd )
else
( new
, Cmd.batch
[ cmd
, Page.Packages.makeRequest
new.elasticsearch
(Maybe.withDefault "" newModel.query)
|> Cmd.map PackagesMsg
]
)
in
case ( old.page, new.page ) of
( Packages oldModel, Packages newModel ) ->
triggerSearch oldModel newModel PackagesMsg Page.Packages.makeRequest
( Options oldModel, Options newModel ) ->
triggerSearch oldModel newModel OptionsMsg Page.Options.makeRequest
( _, _ ) ->
( new, cmd )
decodeResultHitsTotal : D.Decoder SearchResultHitsTotal
decodeResultHitsTotal =
D.map2 SearchResultHitsTotal
(D.field "value" D.int)
(D.field "relation" D.string)
changeRouteTo : Model -> Url.Url -> ( Model, Cmd Msg )
changeRouteTo model url =
let
newModel =
{ model | url = url }
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 "platforms" (D.list D.string)
|> 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.nullable 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)
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
OnUrlRequest urlRequest ->
( model, handleUrlRequest model.key urlRequest )
OnUrlChange url ->
let
newModel =
{ model | page = UrlParser.parse urlParser url |> Maybe.withDefault model.page }
newPage =
case newModel.page of
SearchPage searchModel ->
SearchPage
{ searchModel
| result =
case searchModel.query of
Just query ->
R.Loading
Nothing ->
R.NotAsked
}
newNewModel =
{ newModel | page = newPage }
in
( newNewModel
, initPageCmd newModel newNewModel
)
SearchPageInput query ->
( { model
| page =
case model.page of
SearchPage searchModel ->
SearchPage { searchModel | query = Just query }
maybeRoute =
Route.fromUrl url
in
case maybeRoute of
Nothing ->
( { newModel
| page = NotFound
}
, Cmd.none
)
SearchQuerySubmit ->
case model.page of
SearchPage searchModel ->
( model
, Nav.pushUrl model.key <| createSearchUrl searchModel
)
SearchQueryResponse result ->
case model.page of
SearchPage searchModel ->
let
newPage =
SearchPage { searchModel | result = result }
in
( { model | page = newPage }
, Cmd.none
)
SearchShowPackageDetails showDetailsFor ->
case model.page of
SearchPage searchModel ->
let
newSearchModel =
{ searchModel
| showDetailsFor =
if searchModel.showDetailsFor == Just showDetailsFor then
Nothing
else
Just showDetailsFor
}
in
( model
, Nav.pushUrl model.key <| createSearchUrl newSearchModel
)
createSearchUrl : SearchModel -> String
createSearchUrl model =
[]
|> List.append
(model.query
|> Maybe.map
(\query ->
[ UrlBuilder.string "query" query ]
)
|> Maybe.withDefault []
Just Route.NotFound ->
( { newModel
| page = NotFound
}
, Cmd.none
)
|> List.append
(model.showDetailsFor
|> Maybe.map
(\x ->
[ UrlBuilder.string "showDetailsFor" x
]
Just Route.Home ->
-- Always redirect to /packages until we have something to show
-- on the home page
( newModel, Browser.Navigation.pushUrl newModel.navKey "/packages" )
Just (Route.Packages query showDetailsFor) ->
Page.Packages.init query showDetailsFor
|> updateWith Packages PackagesMsg newModel
|> submitQuery newModel
Just (Route.Options query showDetailsFor) ->
Page.Options.init query showDetailsFor
|> updateWith Options OptionsMsg newModel
|> submitQuery newModel
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model.page ) of
( ClickedLink urlRequest, _ ) ->
case urlRequest of
Browser.Internal url ->
( model
, Browser.Navigation.pushUrl model.navKey <| Url.toString url
)
|> Maybe.withDefault []
)
|> UrlBuilder.absolute [ "search" ]
Browser.External href ->
( model
, Browser.Navigation.load href
)
( ChangedUrl url, _ ) ->
changeRouteTo model url
( HomeMsg subMsg, Home subModel ) ->
Page.Home.update subMsg subModel
|> updateWith Home HomeMsg model
( PackagesMsg subMsg, Packages subModel ) ->
Page.Packages.update model.navKey subMsg subModel
|> updateWith Packages PackagesMsg model
( OptionsMsg subMsg, Options subModel ) ->
Page.Options.update model.navKey subMsg subModel
|> updateWith Options OptionsMsg model
( _, _ ) ->
-- Disregard messages that arrived for the wrong page.
( model, Cmd.none )
-- ---------------------------
-- VIEW
-- ---------------------------
view : Model -> Html Msg
@ -480,134 +224,79 @@ view model =
[ a [ class "brand", href "https://search.nixos.org" ]
[ img [ src "https://nixos.org/logo/nix-wiki.png", class "logo" ] []
]
, viewNavigation model.url
]
]
]
]
, div [ class "container main" ]
[ case model.page of
SearchPage searchModel ->
searchPage searchModel
[ viewPage model
, footer [] []
]
]
searchPage : SearchModel -> Html Msg
searchPage model =
div [ class "search-page" ]
[ h1 [ class "page-header" ] [ text "Search for packages and options" ]
, div [ class "search-input" ]
[ form [ onSubmit SearchQuerySubmit ]
[ div [ class "input-append" ]
[ input
[ type_ "text"
, onInput SearchPageInput
, value <| Maybe.withDefault "" model.query
]
[]
, 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." ] ]
-- ]
]
]
]
viewNavigation : Url.Url -> Html Msg
viewNavigation url =
ul [ class "nav" ]
(List.map
(viewNavigationItem url)
[ ( "/packages", "Packages" )
, ( "/options", "Options" )
]
, case model.result of
R.NotAsked ->
div [] [ text "NotAsked" ]
R.Loading ->
div [] [ text "Loading" ]
R.Success result ->
searchPageResult model.showDetailsFor result.hits
R.Failure error ->
div []
[ text "Error!"
--, pre [] [ text (Debug.toString error) ]
]
]
)
searchPageResult : Maybe String -> SearchResultHits -> Html Msg
searchPageResult showDetailsFor result =
div [ class "search-result" ]
[ table [ class "table table-hover" ]
[ thead []
[ tr []
[ th [] [ text "Attribute name" ]
, th [] [ text "Name" ]
, th [] [ text "Version" ]
, th [] [ text "Description" ]
]
]
, tbody [] <| List.concatMap (searchPageResultItem showDetailsFor) result.hits
]
]
viewNavigationItem :
Url.Url
-> ( String, String )
-> Html Msg
viewNavigationItem url ( path, title ) =
li
[ classList [ ( "active", path == url.path ) ] ]
[ a [ href path ] [ text title ] ]
searchPageResultItem : Maybe String -> SearchResultItem -> List (Html Msg)
searchPageResultItem showDetailsFor item =
case item.source of
Package package ->
let
packageDetails =
if Just item.id == showDetailsFor then
[ td [ colspan 4 ]
[]
]
viewPage : Model -> Html Msg
viewPage model =
case model.page of
NotFound ->
div [] [ text "Not Found" ]
else
[]
in
[ tr [ onClick <| SearchShowPackageDetails item.id ]
[ td [] [ text package.attr_name ]
, td [] [ text package.name ]
, td [] [ text package.version ]
, td [] [ text <| Maybe.withDefault "" package.description ]
]
]
++ packageDetails
Home _ ->
div [] [ text "Welcome" ]
Option option ->
[ tr
[]
[-- td [] [ text option.option_name ]
--, td [] [ text option.name ]
--, td [] [ text option.version ]
--, td [] [ text option.description ]
]
]
Packages packagesModel ->
Html.map (\m -> PackagesMsg m) <| Page.Packages.view packagesModel
Options optionsModel ->
Html.map (\m -> OptionsMsg m) <| Page.Options.view optionsModel
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- ---------------------------
-- MAIN
-- ---------------------------
main : Program Flags Model Msg
main =
Browser.application
{ init = init
, onUrlRequest = ClickedLink
, onUrlChange = ChangedUrl
, subscriptions = subscriptions
, update = update
, view =
\m ->
{ title = "NixOS Search"
, body = [ view m ]
}
, subscriptions = \_ -> Sub.none
, onUrlRequest = OnUrlRequest
, onUrlChange = OnUrlChange
}

28
src/Page/Home.elm Normal file
View file

@ -0,0 +1,28 @@
module Page.Home exposing (Model, Msg, init, update, view)
import Html exposing (Html, text, div )
-- MODEL
type alias Model = ()
init : (Model, Cmd Msg)
init =
((), Cmd.none)
-- UPDATE
type Msg = NoOp
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
(model, Cmd.none)
-- VIEW
view : Model -> Html Msg
view model =
div [] [text "Home"]

169
src/Page/Options.elm Normal file
View file

@ -0,0 +1,169 @@
module Page.Options exposing
( Model
, Msg
, decodeResultItemSource
, init
, makeRequest
, update
, view
)
import Browser.Navigation
import ElasticSearch
import Html
exposing
( Html
, div
, table
, tbody
, td
, text
, th
, thead
, tr
)
import Html.Attributes
exposing
( class
, colspan
)
import Html.Events
exposing
( onClick
)
import Json.Decode
-- MODEL
type alias Model =
ElasticSearch.Model ResultItemSource
type alias ResultItemSource =
{ option_name : String
, description : String
, type_ : String
, default : String
, example : String
, source : String
}
init :
Maybe String
-> Maybe String
-> ( Model, Cmd Msg )
init =
ElasticSearch.init
-- UPDATE
type Msg
= SearchMsg (ElasticSearch.Msg ResultItemSource)
update : Browser.Navigation.Key -> Msg -> Model -> ( Model, Cmd Msg )
update navKey msg model =
case msg of
SearchMsg subMsg ->
let
( newModel, newCmd ) =
ElasticSearch.update "options" navKey subMsg model
in
( newModel, Cmd.map SearchMsg newCmd )
-- VIEW
view : Model -> Html Msg
view model =
ElasticSearch.view
{ title = "Search NixOS options" }
model
viewSuccess
SearchMsg
viewSuccess :
Maybe String
-> ElasticSearch.Result ResultItemSource
-> Html Msg
viewSuccess showDetailsFor result =
div [ class "search-result" ]
[ table [ class "table table-hover" ]
[ thead []
[ tr []
[ th [] [ text "Option name" ]
]
]
, tbody
[]
(List.concatMap
(viewResultItem showDetailsFor)
result.hits.hits
)
]
]
viewResultItem :
Maybe String
-> ElasticSearch.ResultItem ResultItemSource
-> List (Html Msg)
viewResultItem showDetailsFor item =
let
packageDetails =
if Just item.id == showDetailsFor then
[ td [ colspan 1 ]
[ text "This are details!" ]
]
else
[]
in
tr [ onClick (SearchMsg (ElasticSearch.ShowDetails item.id)) ]
[ td [] [ text item.source.option_name ]
]
:: packageDetails
-- API
makeRequest :
ElasticSearch.Options
-> String
-> Cmd Msg
makeRequest options query =
ElasticSearch.makeRequest
"option_name"
-- TODO: add support for different channels
"nixos-unstable-options"
decodeResultItemSource
options
query
|> Cmd.map SearchMsg
-- JSON
decodeResultItemSource : Json.Decode.Decoder ResultItemSource
decodeResultItemSource =
Json.Decode.map6 ResultItemSource
(Json.Decode.field "option_name" Json.Decode.string)
(Json.Decode.field "description" Json.Decode.string)
(Json.Decode.field "type" Json.Decode.string)
(Json.Decode.field "default" Json.Decode.string)
(Json.Decode.field "example" Json.Decode.string)
(Json.Decode.field "source" Json.Decode.string)

211
src/Page/Packages.elm Normal file
View file

@ -0,0 +1,211 @@
module Page.Packages exposing
( Model
, Msg
, decodeResultItemSource
, init
, makeRequest
, update
, view
)
import Browser.Navigation
import ElasticSearch
import Html
exposing
( Html
, div
, table
, tbody
, td
, text
, th
, thead
, tr
)
import Html.Attributes
exposing
( class
, colspan
)
import Html.Events
exposing
( onClick
)
import Json.Decode
import Json.Decode.Pipeline
-- MODEL
type alias Model =
ElasticSearch.Model ResultItemSource
type alias ResultItemSource =
{ attr_name : String
, name : String
, version : String
, description : Maybe String
, longDescription : Maybe String
, licenses : List ResultPackageLicense
, maintainers : List ResultPackageMaintainer
, platforms : List String
, position : Maybe String
, homepage : Maybe String
}
type alias ResultPackageLicense =
{ fullName : Maybe String
, url : Maybe String
}
type alias ResultPackageMaintainer =
{ name : String
, email : String
, github : String
}
init :
Maybe String
-> Maybe String
-> ( Model, Cmd Msg )
init =
ElasticSearch.init
-- UPDATE
type Msg
= SearchMsg (ElasticSearch.Msg ResultItemSource)
update : Browser.Navigation.Key -> Msg -> Model -> ( Model, Cmd Msg )
update navKey msg model =
case msg of
SearchMsg subMsg ->
let
( newModel, newCmd ) =
ElasticSearch.update "packages" navKey subMsg model
in
( newModel, Cmd.map SearchMsg newCmd )
-- VIEW
view : Model -> Html Msg
view model =
ElasticSearch.view
{ title = "Search NixOS packages" }
model
viewSuccess
SearchMsg
viewSuccess :
Maybe String
-> ElasticSearch.Result ResultItemSource
-> Html Msg
viewSuccess showDetailsFor result =
div [ class "search-result" ]
[ table [ class "table table-hover" ]
[ thead []
[ tr []
[ th [] [ text "Attribute name" ]
, th [] [ text "Name" ]
, th [] [ text "Version" ]
, th [] [ text "Description" ]
]
]
, tbody
[]
(List.concatMap
(viewResultItem showDetailsFor)
result.hits.hits
)
]
]
viewResultItem :
Maybe String
-> ElasticSearch.ResultItem ResultItemSource
-> List (Html Msg)
viewResultItem showDetailsFor item =
let
packageDetails =
if Just item.id == showDetailsFor then
[ td [ colspan 4 ]
[ text "This are details!" ]
]
else
[]
in
tr [ onClick (SearchMsg (ElasticSearch.ShowDetails item.id)) ]
[ td [] [ text item.source.attr_name ]
, td [] [ text item.source.name ]
, td [] [ text item.source.version ]
, td [] [ text <| Maybe.withDefault "" item.source.description ]
]
:: packageDetails
-- API
makeRequest :
ElasticSearch.Options
-> String
-> Cmd Msg
makeRequest options query =
ElasticSearch.makeRequest
"attr_name"
"nixos-unstable-packages"
decodeResultItemSource
options
query
|> Cmd.map SearchMsg
-- JSON
decodeResultItemSource : Json.Decode.Decoder ResultItemSource
decodeResultItemSource =
Json.Decode.succeed ResultItemSource
|> Json.Decode.Pipeline.required "attr_name" Json.Decode.string
|> Json.Decode.Pipeline.required "name" Json.Decode.string
|> Json.Decode.Pipeline.required "version" Json.Decode.string
|> Json.Decode.Pipeline.required "description" (Json.Decode.nullable Json.Decode.string)
|> Json.Decode.Pipeline.required "longDescription" (Json.Decode.nullable Json.Decode.string)
|> Json.Decode.Pipeline.required "license" (Json.Decode.list decodeResultPackageLicense)
|> Json.Decode.Pipeline.required "maintainers" (Json.Decode.list decodeResultPackageMaintainer)
|> Json.Decode.Pipeline.required "platforms" (Json.Decode.list Json.Decode.string)
|> Json.Decode.Pipeline.required "position" (Json.Decode.nullable Json.Decode.string)
|> Json.Decode.Pipeline.required "homepage" (Json.Decode.nullable Json.Decode.string)
decodeResultPackageLicense : Json.Decode.Decoder ResultPackageLicense
decodeResultPackageLicense =
Json.Decode.map2 ResultPackageLicense
(Json.Decode.field "fullName" (Json.Decode.nullable Json.Decode.string))
(Json.Decode.field "url" (Json.Decode.nullable Json.Decode.string))
decodeResultPackageMaintainer : Json.Decode.Decoder ResultPackageMaintainer
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)

95
src/Route.elm Normal file
View file

@ -0,0 +1,95 @@
module Route exposing (Route(..), fromUrl, href, replaceUrl)
import Browser.Navigation
import Html
import Html.Attributes
import Url
import Url.Parser exposing ((<?>))
import Url.Parser.Query
-- ROUTING
type Route
= NotFound
| Home
| Packages (Maybe String) (Maybe String)
| Options (Maybe String) (Maybe String)
parser : Url.Parser.Parser (Route -> msg) msg
parser =
Url.Parser.oneOf
[ Url.Parser.map
Home
Url.Parser.top
, Url.Parser.map
NotFound
(Url.Parser.s "not-found")
, Url.Parser.map
Packages
(Url.Parser.s "packages"
<?> Url.Parser.Query.string "query"
<?> Url.Parser.Query.string "showDetailsFor"
)
, Url.Parser.map
Options
(Url.Parser.s "options"
<?> Url.Parser.Query.string "query"
<?> Url.Parser.Query.string "showDetailsFor"
)
]
-- PUBLIC HELPERS
href : Route -> Html.Attribute msg
href targetRoute =
Html.Attributes.href (routeToString targetRoute)
replaceUrl : Browser.Navigation.Key -> Route -> Cmd msg
replaceUrl navKey route =
Browser.Navigation.replaceUrl navKey (routeToString route)
fromUrl : Url.Url -> Maybe Route
fromUrl url =
-- The RealWorld spec treats the fragment like a path.
-- This makes it *literally* the path, so we can proceed
-- with parsing as if it had been a normal path all along.
--{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
Url.Parser.parse parser url
-- INTERNAL
routeToString : Route -> String
routeToString page =
let
( path, query ) =
routeToPieces page
in
"/" ++ String.join "/" path ++ "?" ++ String.join "&" (List.filterMap Basics.identity query)
routeToPieces : Route -> ( List String, List (Maybe String) )
routeToPieces page =
case page of
Home ->
( [], [] )
NotFound ->
( [ "not-found" ], [] )
Packages query showDetailsFor ->
( [ "packages" ], [ query, showDetailsFor ] )
Options query showDetailsFor ->
( [ "options" ], [ query, showDetailsFor ] )

View file

@ -1,6 +1,6 @@
'use strict';
require("./styles.scss");
require("./index.scss");
const {Elm} = require('./Main');

View file

@ -1,10 +1,15 @@
header .navbar a.brand {
line-height: 1.5em;
header .navbar {
a.brand {
line-height: 1.5em;
}
img.logo {
height: 1.5em;
margin-right: 0.5em;
}
ul.nav > li > a {
line-height: 2.5em;
}
}
.search-page {