somehow in a working starte with bugs and missing features
This commit is contained in:
parent
f0318458d8
commit
387d70eaa3
|
@ -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
315
src/ElasticSearch.elm
Normal 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)
|
685
src/Main.elm
685
src/Main.elm
|
@ -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
|
||||||
|
( 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
|
changeRouteTo : Model -> Url.Url -> ( Model, Cmd Msg )
|
||||||
decodeResultHitsTotal =
|
changeRouteTo model url =
|
||||||
D.map2 SearchResultHitsTotal
|
let
|
||||||
(D.field "value" D.int)
|
newModel =
|
||||||
(D.field "relation" D.string)
|
{ model | url = url }
|
||||||
|
|
||||||
|
maybeRoute =
|
||||||
decodeResultItem : D.Decoder SearchResultItem
|
Route.fromUrl url
|
||||||
decodeResultItem =
|
in
|
||||||
D.map4 SearchResultItem
|
case maybeRoute of
|
||||||
(D.field "_index" D.string)
|
Nothing ->
|
||||||
(D.field "_id" D.string)
|
( { newModel
|
||||||
(D.field "_score" D.float)
|
| page = NotFound
|
||||||
(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 }
|
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchQuerySubmit ->
|
Just Route.NotFound ->
|
||||||
case model.page of
|
( { newModel
|
||||||
SearchPage searchModel ->
|
| page = NotFound
|
||||||
( model
|
}
|
||||||
, Nav.pushUrl model.key <| createSearchUrl searchModel
|
, Cmd.none
|
||||||
)
|
|
||||||
|
|
||||||
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 []
|
|
||||||
)
|
)
|
||||||
|> List.append
|
|
||||||
(model.showDetailsFor
|
Just Route.Home ->
|
||||||
|> Maybe.map
|
-- Always redirect to /packages until we have something to show
|
||||||
(\x ->
|
-- on the home page
|
||||||
[ UrlBuilder.string "showDetailsFor" x
|
( 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 []
|
|
||||||
)
|
Browser.External href ->
|
||||||
|> UrlBuilder.absolute [ "search" ]
|
( 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
|
||||||
-- ---------------------------
|
|
||||||
|
|
||||||
|
|
||||||
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
28
src/Page/Home.elm
Normal 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
169
src/Page/Options.elm
Normal 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
211
src/Page/Packages.elm
Normal 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
95
src/Route.elm
Normal 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 ] )
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
require("./styles.scss");
|
require("./index.scss");
|
||||||
|
|
||||||
const {Elm} = require('./Main');
|
const {Elm} = require('./Main');
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
|
|
||||||
header .navbar a.brand {
|
header .navbar {
|
||||||
line-height: 1.5em;
|
a.brand {
|
||||||
|
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 {
|
Loading…
Reference in a new issue