add style to the search result

This commit is contained in:
Rok Garbas 2020-04-10 10:13:50 +02:00
parent f19216fe5a
commit 40bd247ee9
Failed to generate hash of commit
4 changed files with 286 additions and 117 deletions

View file

@ -54,6 +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]
def gen(): def gen():
for attr_name, data in packages: for attr_name, data in packages:
@ -87,7 +88,14 @@ def get_packages(evaluation):
) )
for maintainer in data["meta"].get("maintainers", []) for maintainer in data["meta"].get("maintainers", [])
] ]
platforms = [
type(platform) == str
and platform
or None
for platform in data["meta"].get("platforms", [])
]
yield dict( yield dict(
id=attr_name,
attr_name=attr_name, attr_name=attr_name,
name=data["pname"], name=data["pname"],
version=data["version"], version=data["version"],
@ -95,6 +103,7 @@ def get_packages(evaluation):
longDescription=data["meta"].get("longDescription", ""), longDescription=data["meta"].get("longDescription", ""),
license=licenses, license=licenses,
maintainers=maintainers, maintainers=maintainers,
platforms=[i for i in platforms if i],
position=position, position=position,
homepage=data["meta"].get("homepage"), homepage=data["meta"].get("homepage"),
) )
@ -114,6 +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]
def gen(): def gen():
for name, option in options: for name, option in options:
@ -123,6 +133,7 @@ def get_options(evaluation):
example.get("_type") == "literalExample": example.get("_type") == "literalExample":
example = str(example["text"]) example = str(example["text"])
yield dict( yield dict(
id=name,
option_name=name, option_name=name,
description=option.get("description"), description=option.get("description"),
type=option.get("type"), type=option.get("type"),
@ -133,6 +144,7 @@ def get_options(evaluation):
return len(options), gen return len(options), gen
def recreate_index(es, channel): def recreate_index(es, channel):
if es.indices.exists(f"{channel}-packages"): if es.indices.exists(f"{channel}-packages"):
es.indices.delete(index=f"{channel}-packages") es.indices.delete(index=f"{channel}-packages")
@ -162,6 +174,7 @@ def recreate_index(es, channel):
github=dict(type="text"), github=dict(type="text"),
), ),
), ),
platforms=dict(type="keyword"),
position=dict(type="text"), position=dict(type="text"),
homepage=dict(type="text"), homepage=dict(type="text"),
), ),

View file

@ -6,19 +6,33 @@ import Browser.Navigation as Nav exposing (Key)
import Html import Html
exposing exposing
( Html ( Html
, a
, button , button
, div , div
, footer
, form
, h1 , h1
, header , header
, img
, input , input
, li , li
, p
, pre , pre
, table
, tbody
, td
, text , text
, th
, thead
, tr
, ul , ul
) )
import Html.Attributes import Html.Attributes
exposing exposing
( class ( class
, colspan
, href
, src
, type_ , type_
, value , value
) )
@ -26,6 +40,7 @@ import Html.Events
exposing exposing
( onClick ( onClick
, onInput , onInput
, onSubmit
) )
import Http import Http
import Json.Decode as D import Json.Decode as D
@ -33,6 +48,7 @@ import Json.Decode.Pipeline as DP
import Json.Encode as E import Json.Encode as E
import RemoteData as R import RemoteData as R
import Url exposing (Url) import Url exposing (Url)
import Url.Builder as UrlBuilder
import Url.Parser as UrlParser import Url.Parser as UrlParser
exposing exposing
( (<?>) ( (<?>)
@ -64,8 +80,9 @@ type alias Model =
type alias SearchModel = type alias SearchModel =
{ query : String { query : Maybe String
, result : R.WebData SearchResult , result : R.WebData SearchResult
, showDetailsFor : Maybe String
} }
@ -117,6 +134,7 @@ type alias SearchResultPackage =
, longDescription : Maybe String , longDescription : Maybe String
, licenses : List SearchResultPackageLicense , licenses : List SearchResultPackageLicense
, maintainers : List SearchResultPackageMaintainer , maintainers : List SearchResultPackageMaintainer
, platforms : List String
, position : Maybe String , position : Maybe String
, homepage : Maybe String , homepage : Maybe String
} }
@ -133,7 +151,7 @@ type alias SearchResultOption =
type alias SearchResultPackageLicense = type alias SearchResultPackageLicense =
{ fullName : String { fullName : Maybe String
, url : Maybe String , url : Maybe String
} }
@ -147,21 +165,77 @@ type alias SearchResultPackageMaintainer =
emptySearch : Page emptySearch : Page
emptySearch = emptySearch =
SearchPage { query = "", result = R.NotAsked } SearchPage
{ query = Nothing
, result = R.NotAsked
, showDetailsFor = Nothing
}
init : Flags -> Url -> Key -> ( Model, Cmd Msg ) init : Flags -> Url -> Key -> ( Model, Cmd Msg )
init flags url key = init flags url key =
( { key = key let
model =
{ key = key
, elasticsearchUrl = flags.elasticsearchUrl , elasticsearchUrl = flags.elasticsearchUrl
, elasticsearchUsername = flags.elasticsearchUsername , elasticsearchUsername = flags.elasticsearchUsername
, elasticsearchPassword = flags.elasticsearchPassword , elasticsearchPassword = flags.elasticsearchPassword
, page = UrlParser.parse urlParser url |> Maybe.withDefault emptySearch , page = UrlParser.parse urlParser url |> Maybe.withDefault emptySearch
} }
, Cmd.none 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
-- --------------------------- -- ---------------------------
-- URL Parsing and Routing -- URL Parsing and Routing
@ -182,13 +256,14 @@ urlParser : Parser (Page -> msg) msg
urlParser = urlParser =
UrlParser.oneOf UrlParser.oneOf
[ UrlParser.map [ UrlParser.map
(\q -> (\query showDetailsFor ->
SearchPage SearchPage
{ query = q |> Maybe.withDefault "" { query = query
, result = R.NotAsked , result = R.NotAsked
, showDetailsFor = showDetailsFor
} }
) )
(UrlParser.s "search" <?> UrlParserQuery.string "query") (UrlParser.s "search" <?> UrlParserQuery.string "query" <?> UrlParserQuery.string "showDetailsFor")
] ]
@ -204,6 +279,7 @@ type Msg
| SearchPageInput String | SearchPageInput String
| SearchQuerySubmit | SearchQuerySubmit
| SearchQueryResponse (R.WebData SearchResult) | SearchQueryResponse (R.WebData SearchResult)
| SearchShowPackageDetails String
decodeResult : D.Decoder SearchResult decodeResult : D.Decoder SearchResult
@ -255,6 +331,7 @@ decodeResultPackage =
|> DP.required "longDescription" (D.nullable D.string) |> DP.required "longDescription" (D.nullable D.string)
|> DP.required "license" (D.list decodeResultPackageLicense) |> DP.required "license" (D.list decodeResultPackageLicense)
|> DP.required "maintainers" (D.list decodeResultPackageMaintainer) |> DP.required "maintainers" (D.list decodeResultPackageMaintainer)
|> DP.required "platforms" (D.list D.string)
|> DP.required "position" (D.nullable D.string) |> DP.required "position" (D.nullable D.string)
|> DP.required "homepage" (D.nullable D.string) |> DP.required "homepage" (D.nullable D.string)
@ -262,7 +339,7 @@ decodeResultPackage =
decodeResultPackageLicense : D.Decoder SearchResultPackageLicense decodeResultPackageLicense : D.Decoder SearchResultPackageLicense
decodeResultPackageLicense = decodeResultPackageLicense =
D.map2 SearchResultPackageLicense D.map2 SearchResultPackageLicense
(D.field "fullName" D.string) (D.field "fullName" (D.nullable D.string))
(D.field "url" (D.nullable D.string)) (D.field "url" (D.nullable D.string))
@ -285,44 +362,6 @@ decodeResultOption =
(D.field "source" D.string) (D.field "source" D.string)
initPage : Model -> Cmd Msg
initPage model =
case model.page of
SearchPage searchModel ->
if searchModel.query == "" then
Cmd.none
else
Http.riskyRequest
{ method = "POST"
, headers =
[ Http.header "Authorization" ("Basic " ++ Base64.encode (model.elasticsearchUsername ++ ":" ++ model.elasticsearchPassword))
]
, url = model.elasticsearchUrl ++ "/nixos-unstable-packages/_search"
, body =
Http.jsonBody <|
E.object
[ ( "query"
, E.object
[ ( "match"
, E.object
[ ( "name"
, E.object
[ ( "query", E.string searchModel.query )
, ( "fuzziness", E.int 1 )
]
)
]
)
]
)
]
, expect = Http.expectJson (R.fromResult >> SearchQueryResponse) decodeResult
, timeout = Nothing
, tracker = Nothing
}
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> Model -> ( Model, Cmd Msg )
update message model = update message model =
case message of case message of
@ -340,18 +379,19 @@ update message model =
SearchPage SearchPage
{ searchModel { searchModel
| result = | result =
if searchModel.query == "" then case searchModel.query of
R.NotAsked Just query ->
else
R.Loading R.Loading
Nothing ->
R.NotAsked
} }
newNewModel = newNewModel =
{ newModel | page = newPage } { newModel | page = newPage }
in in
( newNewModel ( newNewModel
, initPage newNewModel , initPageCmd newModel newNewModel
) )
SearchPageInput query -> SearchPageInput query ->
@ -359,7 +399,7 @@ update message model =
| page = | page =
case model.page of case model.page of
SearchPage searchModel -> SearchPage searchModel ->
SearchPage { searchModel | query = query } SearchPage { searchModel | query = Just query }
} }
, Cmd.none , Cmd.none
) )
@ -368,7 +408,7 @@ update message model =
case model.page of case model.page of
SearchPage searchModel -> SearchPage searchModel ->
( model ( model
, Nav.pushUrl model.key <| "/search?query=" ++ searchModel.query , Nav.pushUrl model.key <| createSearchUrl searchModel
) )
SearchQueryResponse result -> SearchQueryResponse result ->
@ -382,6 +422,47 @@ update message model =
, Cmd.none , 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
|> Maybe.map
(\x ->
[ UrlBuilder.string "showDetailsFor" x
]
)
|> Maybe.withDefault []
)
|> UrlBuilder.absolute [ "search" ]
-- --------------------------- -- ---------------------------
@ -391,27 +472,54 @@ update message model =
view : Model -> Html Msg view : Model -> Html Msg
view model = view model =
div [ class "container" ] div []
[ header [] [ header []
[ h1 [] [ text "NixOS Search" ] [ div [ class "navbar navbar-static-top" ]
[ div [ class "navbar-inner" ]
[ div [ class "container" ]
[ a [ class "brand", href "https://search.nixos.org" ]
[ img [ src "https://nixos.org/logo/nix-wiki.png", class "logo" ] []
] ]
, case model.page of ]
]
]
]
, div [ class "container main" ]
[ case model.page of
SearchPage searchModel -> SearchPage searchModel ->
searchPage searchModel searchPage searchModel
, footer [] []
]
] ]
searchPage : SearchModel -> Html Msg searchPage : SearchModel -> Html Msg
searchPage model = searchPage model =
div [] div [ class "search-page" ]
[ div [] [ h1 [ class "page-header" ] [ text "Search for packages and options" ]
, div [ class "search-input" ]
[ form [ onSubmit SearchQuerySubmit ]
[ div [ class "input-append" ]
[ input [ input
[ type_ "text" [ type_ "text"
, onInput SearchPageInput , onInput SearchPageInput
, value model.query , value <| Maybe.withDefault "" model.query
] ]
[] []
, button [ onClick SearchQuerySubmit ] [ text "Search" ] , 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 , case model.result of
R.NotAsked -> R.NotAsked ->
@ -421,26 +529,60 @@ searchPage model =
div [] [ text "Loading" ] div [] [ text "Loading" ]
R.Success result -> R.Success result ->
ul [] (searchPageResult result.hits) searchPageResult model.showDetailsFor result.hits
R.Failure error -> R.Failure error ->
div [] [ text "Error!", pre [] [ text (Debug.toString error) ] ] div [] [ text "Error!", pre [] [ text (Debug.toString error) ] ]
] ]
searchPageResult : SearchResultHits -> List (Html Msg) searchPageResult : Maybe String -> SearchResultHits -> Html Msg
searchPageResult result = searchPageResult showDetailsFor result =
List.map searchPageResultItem result.hits 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
]
]
searchPageResultItem : SearchResultItem -> Html Msg searchPageResultItem : Maybe String -> SearchResultItem -> List (Html Msg)
searchPageResultItem item = searchPageResultItem showDetailsFor item =
-- case item.source of case item.source of
-- Package package -> Package package ->
-- li [] [ text package.attr_name ] let
-- Option option -> packageDetails =
-- li [] [ text option.option_name ] if Just item.id == showDetailsFor then
li [] [ text <| Debug.toString item ] [ td [ colspan 4 ] [ text "works!" ] ]
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
Option option ->
[ tr
[]
[-- td [] [ text option.option_name ]
--, td [] [ text option.name ]
--, td [] [ text option.version ]
--, td [] [ text option.description ]
]
]

View file

@ -1,11 +1,28 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Elm hotloading dev environment</title> <title>Elm hotloading dev environment</title>
</head>
<body>
</body> <script type="text/javascript" src="https://nixos.org/js/jquery.min.js"></script>
<script type="text/javascript" src="https://nixos.org/js/jquery-ui.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="text/javascript" src="https://nixos.org/bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://nixos.org/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://nixos.org/bootstrap/css/bootstrap-responsive.min.css" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
<link rel="shortcut icon" type="image/png" href="https://nixos.org/favicon.png" />
</head>
<body>
</body>
</html> </html>

View file

@ -1,30 +1,27 @@
@import '~purecss/build/pure.css';
body { header .navbar a.brand {
background-color: #ddd; line-height: 1.5em;
margin-top: 20px; img.logo {
} height: 1.5em;
.container { margin-right: 0.5em;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
header {
display: flex;
align-items: center;
img {
width: 60px;
margin-right: 30px;
} }
} }
button { .search-page {
margin-right: 15px; .search-input {
} text-align: center;
.input-append input {
.logo { font-size: 24px;
background: url('images/logo.png'); height: 40px;
width: 60px; }
height: 60px; .input-append button {
background-size: cover; font-size: 24px;
height: 50px;
}
}
.search-result {
tbody tr {
cursor: pointer;
}
}
} }