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, check=True,
) )
packages = json.loads(result.stdout).items() packages = json.loads(result.stdout).items()
packages = list(packages)[:10] packages = list(packages)
def gen(): def gen():
for attr_name, data in packages: for attr_name, data in packages:
@ -123,7 +123,7 @@ def get_options(evaluation):
if os.path.exists(options_file): if os.path.exists(options_file):
with open(options_file) as f: with open(options_file) as f:
options = json.load(f).items() options = json.load(f).items()
options = list(options)[:10] options = list(options)
def gen(): def gen():
for name, option in options: 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) module Main exposing (main)
import Base64 --exposing (UrlRequest(..))
import Browser exposing (UrlRequest(..))
import Browser.Navigation as Nav exposing (Key) import Browser
import Browser.Navigation
import ElasticSearch
import Html import Html
exposing exposing
( Html ( Html
, a , a
, button
, div , div
, footer , footer
, form
, h1
, header , header
, img , img
, input
, li , li
, p
, pre
, table
, tbody
, td
, text , text
, th
, thead
, tr
, ul , ul
) )
import Html.Attributes import Html.Attributes
exposing exposing
( class ( class
, colspan , classList
, href , href
, src , src
, type_
, value
) )
import Html.Events import Page.Home
exposing import Page.Options
( onClick import Page.Packages
, onInput import RemoteData
, onSubmit import Route
) import Url
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
-- ---------------------------
-- MODEL -- MODEL
-- ---------------------------
type alias Flags = type alias Flags =
@ -71,403 +44,174 @@ type alias Flags =
type alias Model = type alias Model =
{ key : Key { navKey : Browser.Navigation.Key
, elasticsearchUrl : String , url : Url.Url
, elasticsearchUsername : String , elasticsearch : ElasticSearch.Options
, elasticsearchPassword : String
, page : Page , page : Page
} }
type alias SearchModel =
{ query : Maybe String
, result : R.WebData SearchResult
, showDetailsFor : Maybe String
}
type Page type Page
= SearchPage SearchModel = NotFound
| Home Page.Home.Model
| Packages Page.Packages.Model
| Options Page.Options.Model
init :
--| PackagePage SearchModel Flags
--| MaintainerPage SearchModel -> Url.Url
-> Browser.Navigation.Key
-> ( Model, Cmd Msg )
type alias SearchResult = init flags url navKey =
{ 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 =
let let
model = model =
{ key = key { navKey = navKey
, elasticsearchUrl = flags.elasticsearchUrl , url = url
, elasticsearchUsername = flags.elasticsearchUsername , elasticsearch =
, elasticsearchPassword = flags.elasticsearchPassword ElasticSearch.Options
, page = UrlParser.parse urlParser url |> Maybe.withDefault emptySearch flags.elasticsearchUrl
flags.elasticsearchUsername
flags.elasticsearchPassword
, page = NotFound
} }
in in
( model changeRouteTo model url
, 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
-- ---------------------------
-- 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 -- UPDATE
-- ---------------------------
type Msg type Msg
= OnUrlRequest UrlRequest = ChangedUrl Url.Url
| OnUrlChange Url | ClickedLink Browser.UrlRequest
| SearchPageInput String | HomeMsg Page.Home.Msg
| SearchQuerySubmit | PackagesMsg Page.Packages.Msg
| SearchQueryResponse (R.WebData SearchResult) | OptionsMsg Page.Options.Msg
| SearchShowPackageDetails String
decodeResult : D.Decoder SearchResult updateWith :
decodeResult = (subModel -> Page)
D.map SearchResult -> (subMsg -> Msg)
(D.field "hits" decodeResultHits) -> 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 submitQuery :
decodeResultHits = Model
D.map3 SearchResultHits -> ( Model, Cmd Msg )
(D.field "total" decodeResultHitsTotal) -> ( Model, Cmd Msg )
(D.field "max_score" (D.nullable D.float)) submitQuery old ( new, cmd ) =
(D.field "hits" (D.list decodeResultItem)) let
triggerSearch oldModel newModel msg makeRequest =
if (oldModel.query == newModel.query) && RemoteData.isSuccess oldModel.result then
( new, cmd )
else
decodeResultHitsTotal : D.Decoder SearchResultHitsTotal ( new
decodeResultHitsTotal = , Cmd.batch
D.map2 SearchResultHitsTotal [ cmd
(D.field "value" D.int) , Page.Packages.makeRequest
(D.field "relation" D.string) new.elasticsearch
(Maybe.withDefault "" newModel.query)
|> Cmd.map PackagesMsg
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
] ]
)
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 )
decodeResultPackage : D.Decoder SearchResultPackage changeRouteTo : Model -> Url.Url -> ( Model, Cmd Msg )
decodeResultPackage = changeRouteTo model url =
D.succeed SearchResultPackage let
|> DP.required "attr_name" D.string newModel =
|> DP.required "name" D.string { model | url = url }
|> 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)
maybeRoute =
Route.fromUrl url
in
case maybeRoute of
Nothing ->
( { newModel
| page = NotFound
}
, Cmd.none
)
decodeResultPackageLicense : D.Decoder SearchResultPackageLicense Just Route.NotFound ->
decodeResultPackageLicense = ( { newModel
D.map2 SearchResultPackageLicense | page = NotFound
(D.field "fullName" (D.nullable D.string)) }
(D.field "url" (D.nullable D.string)) , Cmd.none
)
Just Route.Home ->
-- Always redirect to /packages until we have something to show
-- on the home page
( newModel, Browser.Navigation.pushUrl newModel.navKey "/packages" )
decodeResultPackageMaintainer : D.Decoder SearchResultPackageMaintainer Just (Route.Packages query showDetailsFor) ->
decodeResultPackageMaintainer = Page.Packages.init query showDetailsFor
D.map3 SearchResultPackageMaintainer |> updateWith Packages PackagesMsg newModel
(D.field "name" D.string) |> submitQuery newModel
(D.field "email" D.string)
(D.field "github" D.string)
Just (Route.Options query showDetailsFor) ->
decodeResultOption : D.Decoder SearchResultOption Page.Options.init query showDetailsFor
decodeResultOption = |> updateWith Options OptionsMsg newModel
D.map6 SearchResultOption |> submitQuery newModel
(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 : Msg -> Model -> ( Model, Cmd Msg )
update message model = update msg model =
case message of case ( msg, model.page ) of
OnUrlRequest urlRequest -> ( ClickedLink urlRequest, _ ) ->
( model, handleUrlRequest model.key urlRequest ) case urlRequest of
Browser.Internal url ->
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 }
}
, Cmd.none
)
SearchQuerySubmit ->
case model.page of
SearchPage searchModel ->
( model ( model
, Nav.pushUrl model.key <| createSearchUrl searchModel , Browser.Navigation.pushUrl model.navKey <| Url.toString url
) )
SearchQueryResponse result -> Browser.External href ->
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 ( model
, Nav.pushUrl model.key <| createSearchUrl newSearchModel , Browser.Navigation.load href
) )
( ChangedUrl url, _ ) ->
changeRouteTo model url
createSearchUrl : SearchModel -> String ( HomeMsg subMsg, Home subModel ) ->
createSearchUrl model = Page.Home.update subMsg subModel
[] |> updateWith Home HomeMsg model
|> List.append
(model.query ( PackagesMsg subMsg, Packages subModel ) ->
|> Maybe.map Page.Packages.update model.navKey subMsg subModel
(\query -> |> updateWith Packages PackagesMsg model
[ UrlBuilder.string "query" query ]
) ( OptionsMsg subMsg, Options subModel ) ->
|> Maybe.withDefault [] Page.Options.update model.navKey subMsg subModel
) |> updateWith Options OptionsMsg model
|> List.append
(model.showDetailsFor ( _, _ ) ->
|> Maybe.map -- Disregard messages that arrived for the wrong page.
(\x -> ( model, Cmd.none )
[ UrlBuilder.string "showDetailsFor" x
]
)
|> Maybe.withDefault []
)
|> UrlBuilder.absolute [ "search" ]
-- ---------------------------
-- VIEW -- VIEW
-- ---------------------------
view : Model -> Html Msg view : Model -> Html Msg
@ -480,134 +224,79 @@ view model =
[ a [ class "brand", href "https://search.nixos.org" ] [ a [ class "brand", href "https://search.nixos.org" ]
[ img [ src "https://nixos.org/logo/nix-wiki.png", class "logo" ] [] [ img [ src "https://nixos.org/logo/nix-wiki.png", class "logo" ] []
] ]
, viewNavigation model.url
] ]
] ]
] ]
] ]
, div [ class "container main" ] , div [ class "container main" ]
[ case model.page of [ viewPage model
SearchPage searchModel ->
searchPage searchModel
, footer [] [] , footer [] []
] ]
] ]
searchPage : SearchModel -> Html Msg viewNavigation : Url.Url -> Html Msg
searchPage model = viewNavigation url =
div [ class "search-page" ] ul [ class "nav" ]
[ h1 [ class "page-header" ] [ text "Search for packages and options" ] (List.map
, div [ class "search-input" ] (viewNavigationItem url)
[ form [ onSubmit SearchQuerySubmit ] [ ( "/packages", "Packages" )
[ div [ class "input-append" ] , ( "/options", "Options" )
[ 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." ] ]
-- ]
]
]
]
]
, 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 viewNavigationItem :
searchPageResult showDetailsFor result = Url.Url
div [ class "search-result" ] -> ( String, String )
[ table [ class "table table-hover" ] -> Html Msg
[ thead [] viewNavigationItem url ( path, title ) =
[ tr [] li
[ th [] [ text "Attribute name" ] [ classList [ ( "active", path == url.path ) ] ]
, th [] [ text "Name" ] [ a [ href path ] [ text title ] ]
, th [] [ text "Version" ]
, th [] [ text "Description" ]
]
]
, tbody [] <| List.concatMap (searchPageResultItem showDetailsFor) result.hits
]
]
searchPageResultItem : Maybe String -> SearchResultItem -> List (Html Msg) viewPage : Model -> Html Msg
searchPageResultItem showDetailsFor item = viewPage model =
case item.source of case model.page of
Package package -> NotFound ->
let div [] [ text "Not Found" ]
packageDetails =
if Just item.id == showDetailsFor then
[ td [ colspan 4 ]
[]
]
else Home _ ->
[] div [] [ text "Welcome" ]
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
Option option -> Packages packagesModel ->
[ tr Html.map (\m -> PackagesMsg m) <| Page.Packages.view packagesModel
[]
[-- td [] [ text option.option_name ] Options optionsModel ->
--, td [] [ text option.name ] Html.map (\m -> OptionsMsg m) <| Page.Options.view optionsModel
--, td [] [ text option.version ]
--, td [] [ text option.description ]
]
] -- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- ---------------------------
-- MAIN -- MAIN
-- ---------------------------
main : Program Flags Model Msg main : Program Flags Model Msg
main = main =
Browser.application Browser.application
{ init = init { init = init
, onUrlRequest = ClickedLink
, onUrlChange = ChangedUrl
, subscriptions = subscriptions
, update = update , update = update
, view = , view =
\m -> \m ->
{ title = "NixOS Search" { title = "NixOS Search"
, body = [ view m ] , 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'; 'use strict';
require("./styles.scss"); require("./index.scss");
const {Elm} = require('./Main'); const {Elm} = require('./Main');

View file

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