Adds faceted search (#261)

This commit is contained in:
Rok Garbas 2021-01-23 17:25:43 +01:00 committed by GitHub
parent 1c1a3ca7d1
commit 5bb94c9c92
Failed to generate hash of commit
13 changed files with 1659 additions and 773 deletions

View file

@ -1 +1 @@
17
18

View file

@ -31,6 +31,8 @@ pkgs.stdenv.mkDerivation {
name = "${package.name}-${package.version}";
src = pkgs.lib.cleanSource ./.;
preferLocalBuild = true;
buildInputs =
[
yarnPkg

View file

@ -13,12 +13,13 @@ mkPoetryApplication {
'';
});
});
preferLocalBuild = true;
nativeBuildInputs = with pkgs; [
poetry
fd
entr
nixStable
];
#doCheck = false;
checkPhase = ''
export PYTHONPATH=$PWD:$PYTHONPATH
black --diff --check import_scripts/ tests/
@ -27,8 +28,13 @@ mkPoetryApplication {
pytest -vv tests/
'';
postInstall = ''
wrapProgram $out/bin/import-channel --set INDEX_SCHEMA_VERSION "${version}"
wrapProgram $out/bin/channel-diff --set INDEX_SCHEMA_VERSION "${version}"
wrapProgram $out/bin/import-channel \
--set INDEX_SCHEMA_VERSION "${version}" \
--prefix PATH : "${pkgs.nixStable}/bin"
wrapProgram $out/bin/channel-diff \
--set INDEX_SCHEMA_VERSION "${version}" \
--prefix PATH : "${pkgs.nixStable}/bin"
'';
shellHook = ''
cd import-scripts/

View file

@ -36,6 +36,7 @@ CHANNELS = {
"20.03": "nixos/20.03/nixos-20.03.",
"20.09": "nixos/20.09/nixos-20.09.",
}
ALLOWED_PLATFORMS = ["x86_64-linux", "aarch64-linux", "x86_64-darwin", "i686-linux"]
ANALYSIS = {
"normalizer": {
"lowercase": {"type": "custom", "char_filter": [], "filter": ["lowercase"]}
@ -88,42 +89,34 @@ MAPPING = {
},
"package_attr_name": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_attr_name_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_attr_name_query": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_attr_name_query_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_attr_set": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_attr_set_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_pname": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_pname_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"package_pversion": {"type": "keyword"},
@ -151,6 +144,7 @@ MAPPING = {
"type": "nested",
"properties": {"fullName": {"type": "text"}, "url": {"type": "text"}},
},
"package_license_set": {"type": "keyword"},
"package_maintainers": {
"type": "nested",
"properties": {
@ -159,6 +153,7 @@ MAPPING = {
"github": {"type": "text"},
},
},
"package_maintainers_set": {"type": "keyword"},
"package_platforms": {"type": "keyword"},
"package_position": {"type": "text"},
"package_homepage": {"type": "keyword"},
@ -166,22 +161,18 @@ MAPPING = {
# Options fields
"option_name": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"option_name_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"option_name_query": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"option_name_query_reverse": {
"type": "keyword",
"normalizer": "lowercase",
"fields": {"edge": {"type": "text", "analyzer": "edge"}},
},
"option_description": {
@ -396,7 +387,7 @@ def remove_attr_set(name):
@backoff.on_exception(backoff.expo, subprocess.CalledProcessError)
def get_packages_raw(evaluation):
logger.debug(
f"get_packages: Retrieving list of packages for '{evaluation['git_revision']}' revision"
f"get_packages_raw: Retrieving list of packages for '{evaluation['git_revision']}' revision"
)
result = subprocess.run(
shlex.split(
@ -417,7 +408,7 @@ def get_packages(evaluation, evaluation_builds):
licenses = data["meta"].get("license")
if licenses:
if type(licenses) == str:
licenses = [dict(fullName=licenses)]
licenses = [dict(fullName=licenses, url=None)]
elif type(licenses) == dict:
licenses = [licenses]
licenses = [
@ -427,24 +418,27 @@ def get_packages(evaluation, evaluation_builds):
for license in licenses
]
else:
licenses = []
licenses = [dict(fullName="No license", url=None)]
maintainers = get_maintainer(data["meta"].get("maintainers", []))
if len(maintainers) == 0:
maintainers = [dict(name="No maintainers", email=None, github=None)]
platforms = [
type(platform) == str and platform or None
platform
for platform in data["meta"].get("platforms", [])
if type(platform) == str and platform in ALLOWED_PLATFORMS
]
attr_set = None
attr_set = "No package set"
if "." in attr_name:
attr_set = attr_name.split(".")[0]
maybe_attr_set = attr_name.split(".")[0]
if (
not attr_set.endswith("Packages")
and not attr_set.endswith("Plugins")
and not attr_set.endswith("Extensions")
maybe_attr_set.endswith("Packages")
or maybe_attr_set.endswith("Plugins")
or maybe_attr_set.endswith("Extensions")
):
attr_set = None
attr_set = maybe_attr_set
hydra = None
if data["name"] in evaluation_builds:
@ -492,8 +486,10 @@ def get_packages(evaluation, evaluation_builds):
package_longDescription=package_longDescription,
package_longDescription_reverse=field_reverse(package_longDescription),
package_license=licenses,
package_license_set=[i["fullName"] for i in licenses],
package_maintainers=maintainers,
package_platforms=[i for i in platforms if i],
package_maintainers_set=[i["name"] for i in maintainers if i["name"]],
package_platforms=platforms,
package_position=position,
package_homepage=data["meta"].get("homepage"),
package_system=data["system"],

View file

@ -32,7 +32,6 @@ import Page.Packages
import Route
import Search
import Url
import Url.Builder
@ -129,6 +128,7 @@ attemptQuery (( model, _ ) as pair) =
(Maybe.withDefault "" searchModel.query)
searchModel.from
searchModel.size
searchModel.buckets
searchModel.sort
]
)
@ -365,7 +365,7 @@ viewNavigation route =
f searchArgs
_ ->
f <| Route.SearchArgs Nothing Nothing Nothing Nothing Nothing Nothing
f <| Route.SearchArgs Nothing Nothing Nothing Nothing Nothing Nothing Nothing
in
li [] [ a [ href "https://nixos.org" ] [ text "Back to nixos.org" ] ]
:: List.map

View file

@ -14,24 +14,17 @@ import Html
( Html
, a
, code
, dd
, div
, dl
, dt
, li
, pre
, span
, table
, tbody
, td
, strong
, text
, th
, thead
, tr
, ul
)
import Html.Attributes
exposing
( class
, colspan
, classList
, href
, target
)
@ -42,8 +35,6 @@ import Html.Events
import Html.Parser
import Html.Parser.Util
import Json.Decode
import Json.Encode
import Regex
import Route
import Search
@ -53,7 +44,7 @@ import Search
type alias Model =
Search.Model ResultItemSource
Search.Model ResultItemSource ResultAggregations
type alias ResultItemSource =
@ -66,6 +57,16 @@ type alias ResultItemSource =
}
type alias ResultAggregations =
{ all : AggregationsAll
}
type alias AggregationsAll =
{ doc_count : Int
}
init : Route.SearchArgs -> Maybe Model -> ( Model, Cmd Msg )
init searchArgs model =
let
@ -82,7 +83,7 @@ init searchArgs model =
type Msg
= SearchMsg (Search.Msg ResultItemSource)
= SearchMsg (Search.Msg ResultItemSource ResultAggregations)
update :
@ -111,125 +112,62 @@ update navKey msg model =
view : Model -> Html Msg
view model =
Search.view { toRoute = Route.Options, categoryName = "options" }
"Search NixOS options"
[ text "Search more than "
, strong [] [ text "10 000 options" ]
]
model
viewSuccess
viewBuckets
SearchMsg
viewBuckets :
Maybe String
-> Search.SearchResult ResultItemSource ResultAggregations
-> List (Html Msg)
viewBuckets _ _ =
[]
viewSuccess :
String
-> Bool
-> Maybe String
-> Search.SearchResult ResultItemSource
-> List (Search.ResultItem ResultItemSource)
-> Html Msg
viewSuccess channel show result =
div [ class "search-result" ]
[ table [ class "table table-hover" ]
[ thead []
[ tr []
[ th [] [ text "Option name" ]
]
]
, tbody
[]
(List.concatMap
(viewResultItem channel show)
result.hits.hits
)
]
]
viewSuccess channel showNixOSDetails show hits =
ul []
(List.map
(viewResultItem channel showNixOSDetails show)
hits
)
viewResultItem :
String
-> Bool
-> Maybe String
-> Search.ResultItem ResultItemSource
-> List (Html Msg)
viewResultItem channel show item =
let
packageDetails =
if Just item.source.name == show then
[ td [ colspan 1 ] [ viewResultItemDetails channel item ]
]
else
[]
open =
SearchMsg (Search.ShowDetails item.source.name)
in
[]
-- DEBUG: |> List.append
-- DEBUG: [ tr []
-- DEBUG: [ td [ colspan 1 ]
-- DEBUG: [ div [] [ text <| "score: " ++ String.fromFloat (Maybe.withDefault 0 item.score) ]
-- DEBUG: , div []
-- DEBUG: [ text <|
-- DEBUG: "matched queries: "
-- DEBUG: , ul []
-- DEBUG: (item.matched_queries
-- DEBUG: |> Maybe.withDefault []
-- DEBUG: |> List.sort
-- DEBUG: |> List.map (\q -> li [] [ text q ])
-- DEBUG: )
-- DEBUG: ]
-- DEBUG: ]
-- DEBUG: ]
-- DEBUG: ]
|> List.append
(tr
[ onClick open
, Search.elementId item.source.name
]
[ td []
[ Html.button
[ class "search-result-button"
, Html.Events.custom "click" <|
Json.Decode.succeed
{ message = open
, stopPropagation = True
, preventDefault = True
}
]
[ text item.source.name
]
]
]
:: packageDetails
)
viewResultItemDetails :
String
-> Search.ResultItem ResultItemSource
-> Html Msg
viewResultItemDetails channel item =
viewResultItem channel _ show item =
let
default =
"Not given"
asText value =
span [] <|
showHtml value =
div [] <|
case Html.Parser.run value of
Ok nodes ->
Html.Parser.Util.toVirtualDom nodes
Err e ->
Err _ ->
[]
default =
"Not given"
asPre value =
pre [] [ text value ]
asCode value =
code [] [ text value ]
asPreCode value =
div [] [ pre [] [ code [] [ text value ] ] ]
encodeHtml value =
value
|> String.replace "<" "&lt;"
|> String.replace ">" "&gt;"
div [] [ pre [] [ code [ class "code-block" ] [ text value ] ] ]
githubUrlPrefix branch =
"https://github.com/NixOS/nixpkgs/blob/" ++ branch ++ "/"
@ -248,19 +186,11 @@ viewResultItemDetails channel item =
[ href <| githubUrlPrefix channelDetails.branch ++ (value |> String.replace ":" "#L")
, target "_blank"
]
[ text <| value ]
[ text value ]
Nothing ->
text <| cleanPosition value
wrapped wrapWith value =
case value of
"" ->
wrapWith <| "\"" ++ value ++ "\""
_ ->
wrapWith value
withEmpty wrapWith maybe =
case maybe of
Nothing ->
@ -271,21 +201,56 @@ viewResultItemDetails channel item =
Just value ->
wrapWith value
wrapped wrapWith value =
case value of
"" ->
wrapWith <| "\"" ++ value ++ "\""
_ ->
wrapWith value
showDetails =
if Just item.source.name == show then
div [ Html.Attributes.map SearchMsg Search.trapClick ]
[ div [] [ text "Default value" ]
, div [] [ withEmpty (wrapped asPreCode) item.source.default ]
, div [] [ text "Type" ]
, div [] [ withEmpty asPre item.source.type_ ]
, div [] [ text "Example" ]
, div [] [ withEmpty (wrapped asPreCode) item.source.example ]
, div [] [ text "Declared in" ]
, div [] [ withEmpty asGithubLink item.source.source ]
]
|> Just
else
Nothing
toggle =
SearchMsg (Search.ShowDetails item.source.name)
isOpen =
Just item.source.name == show
in
dl [ class "dl-horizontal" ]
[ dt [] [ text "Name" ]
, dd [] [ withEmpty asText (Just (encodeHtml item.source.name)) ]
, dt [] [ text "Description" ]
, dd [] [ withEmpty asText item.source.description ]
, dt [] [ text "Default value" ]
, dd [] [ withEmpty (wrapped asPreCode) item.source.default ]
, dt [] [ text "Type" ]
, dd [] [ withEmpty asPre item.source.type_ ]
, dt [] [ text "Example value" ]
, dd [] [ withEmpty (wrapped asPreCode) item.source.example ]
, dt [] [ text "Declared in" ]
, dd [] [ withEmpty asGithubLink item.source.source ]
li
[ class "option"
, classList [ ( "opened", isOpen ) ]
, Search.elementId item.source.name
]
<|
List.filterMap identity
[ Just <|
Html.button
[ class "search-result-button"
, onClick toggle
]
[ text item.source.name ]
, Maybe.map showHtml item.source.description
, Just <|
Search.showMoreButton toggle isOpen
, showDetails
]
@ -298,9 +263,10 @@ makeRequest :
-> String
-> Int
-> Int
-> Maybe String
-> Search.Sort
-> Cmd Msg
makeRequest options channel query from size sort =
makeRequest options channel query from size _ sort =
Search.makeRequest
(Search.makeRequestBody
(String.trim query)
@ -309,6 +275,8 @@ makeRequest options channel query from size sort =
sort
"option"
"option_name"
[]
[]
[ ( "option_name", 6.0 )
, ( "option_name_query", 3.0 )
, ( "option_description", 1.0 )
@ -316,6 +284,7 @@ makeRequest options channel query from size sort =
)
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ channel)
decodeResultItemSource
decodeResultAggregations
options
Search.QueryResponse
(Just "query-options")
@ -335,3 +304,15 @@ decodeResultItemSource =
(Json.Decode.field "option_default" (Json.Decode.nullable Json.Decode.string))
(Json.Decode.field "option_example" (Json.Decode.nullable Json.Decode.string))
(Json.Decode.field "option_source" (Json.Decode.nullable Json.Decode.string))
decodeResultAggregations : Json.Decode.Decoder ResultAggregations
decodeResultAggregations =
Json.Decode.map ResultAggregations
(Json.Decode.field "all" decodeResultAggregationsAll)
decodeResultAggregationsAll : Json.Decode.Decoder AggregationsAll
decodeResultAggregationsAll =
Json.Decode.map AggregationsAll
(Json.Decode.field "doc_count" Json.Decode.int)

View file

@ -13,39 +13,33 @@ import Html
exposing
( Html
, a
, code
, dd
, div
, dl
, dt
, em
, h4
, li
, p
, pre
, table
, tbody
, td
, span
, strong
, text
, th
, thead
, tr
, ul
)
import Html.Attributes
exposing
( class
, colspan
, classList
, href
, id
, target
)
import Html.Events
exposing
( onClick
)
import Html.Events exposing (onClick)
import Json.Decode
import Json.Decode.Pipeline
import Json.Encode
import Regex
import Route
import Search
import Utils
@ -53,7 +47,7 @@ import Search
type alias Model =
Search.Model ResultItemSource
Search.Model ResultItemSource ResultAggregations
type alias ResultItemSource =
@ -103,6 +97,51 @@ type alias ResultPackageHydraPath =
}
type alias ResultAggregations =
{ all : Aggregations
, package_platforms : Search.Aggregation
, package_attr_set : Search.Aggregation
, package_maintainers_set : Search.Aggregation
, package_license_set : Search.Aggregation
}
type alias Aggregations =
{ doc_count : Int
, package_platforms : Search.Aggregation
, package_attr_set : Search.Aggregation
, package_maintainers_set : Search.Aggregation
, package_license_set : Search.Aggregation
}
type alias Buckets =
{ packageSets : List String
, licenses : List String
, maintainers : List String
, platforms : List String
}
emptyBuckets : Buckets
emptyBuckets =
{ packageSets = []
, licenses = []
, maintainers = []
, platforms = []
}
initBuckets :
Maybe String
-> Buckets
initBuckets bucketsAsString =
bucketsAsString
|> Maybe.map (Json.Decode.decodeString decodeBuckets)
|> Maybe.andThen Result.toMaybe
|> Maybe.withDefault emptyBuckets
init : Route.SearchArgs -> Maybe Model -> ( Model, Cmd Msg )
init searchArgs model =
let
@ -119,7 +158,7 @@ init searchArgs model =
type Msg
= SearchMsg (Search.Msg ResultItemSource)
= SearchMsg (Search.Msg ResultItemSource ResultAggregations)
update :
@ -148,259 +187,369 @@ update navKey msg model =
view : Model -> Html Msg
view model =
Search.view { toRoute = Route.Packages, categoryName = "packages" }
"Search NixOS packages"
[ text "Search more than "
, strong [] [ text "80 000 packages" ]
]
model
viewSuccess
viewBuckets
SearchMsg
viewBuckets :
Maybe String
-> Search.SearchResult ResultItemSource ResultAggregations
-> List (Html Msg)
viewBuckets bucketsAsString result =
let
initialBuckets =
initBuckets bucketsAsString
selectedBucket =
initialBuckets
createBucketsMsg getBucket mergeBuckets value =
value
|> Utils.toggleList (getBucket initialBuckets)
|> mergeBuckets initialBuckets
|> encodeBuckets
|> Json.Encode.encode 0
|> Search.BucketsChange
|> SearchMsg
sortBuckets items =
items
|> List.sortBy .doc_count
|> List.reverse
in
[]
|> viewBucket
"Package sets"
(result.aggregations.package_attr_set.buckets |> sortBuckets)
(createBucketsMsg .packageSets (\s v -> { s | packageSets = v }))
selectedBucket.packageSets
|> viewBucket
"Licenses"
(result.aggregations.package_license_set.buckets |> sortBuckets)
(createBucketsMsg .licenses (\s v -> { s | licenses = v }))
selectedBucket.licenses
|> viewBucket
"Maintainers"
(result.aggregations.package_maintainers_set.buckets |> sortBuckets)
(createBucketsMsg .maintainers (\s v -> { s | maintainers = v }))
selectedBucket.maintainers
|> viewBucket
"Platforms"
(result.aggregations.package_platforms.buckets |> sortBuckets)
(createBucketsMsg .platforms (\s v -> { s | platforms = v }))
selectedBucket.platforms
viewBucket :
String
-> List Search.AggregationsBucketItem
-> (String -> Msg)
-> List String
-> List (Html Msg)
-> List (Html Msg)
viewBucket title buckets searchMsgFor selectedBucket sets =
List.append
sets
(if List.isEmpty buckets then
[]
else
[ li []
[ ul []
(List.append
[ li [ class "header" ] [ text title ] ]
(List.map
(\bucket ->
li []
[ a
[ href "#"
, onClick <| searchMsgFor bucket.key
, classList
[ ( "selected"
, List.member bucket.key selectedBucket
)
]
]
[ span [] [ text bucket.key ]
, span [] [ span [ class "badge" ] [ text <| String.fromInt bucket.doc_count ] ]
]
]
)
buckets
)
)
]
]
)
viewSuccess :
String
-> Bool
-> Maybe String
-> Search.SearchResult ResultItemSource
-> List (Search.ResultItem ResultItemSource)
-> Html Msg
viewSuccess channel show 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 channel show)
result.hits.hits
)
]
]
viewSuccess channel showNixOSDetails show hits =
ul []
(List.map
(viewResultItem channel showNixOSDetails show)
hits
)
viewResultItem :
String
-> Bool
-> Maybe String
-> Search.ResultItem ResultItemSource
-> List (Html Msg)
viewResultItem channel show item =
let
packageDetails =
if Just item.source.attr_name == show then
[ td [ colspan 4 ] [ viewResultItemDetails channel item ]
]
else
[]
open =
SearchMsg (Search.ShowDetails item.source.attr_name)
in
[]
-- DEBUG: |> List.append
-- DEBUG: [ tr []
-- DEBUG: [ td [ colspan 4 ]
-- DEBUG: [ div []
-- DEBUG: [ text <|
-- DEBUG: "score: "
-- DEBUG: ++ (item.score
-- DEBUG: |> Maybe.map String.fromFloat
-- DEBUG: |> Maybe.withDefault "No score"
-- DEBUG: )
-- DEBUG: ]
-- DEBUG: , div []
-- DEBUG: [ text <|
-- DEBUG: "matched queries: "
-- DEBUG: , ul []
-- DEBUG: (item.matched_queries
-- DEBUG: |> Maybe.withDefault []
-- DEBUG: |> List.sort
-- DEBUG: |> List.map (\q -> li [] [ text q ])
-- DEBUG: )
-- DEBUG: ]
-- DEBUG: ]
-- DEBUG: ]
-- DEBUG: ]
|> List.append
(tr
[ onClick open
, Search.elementId item.source.attr_name
]
[ td []
[ Html.button
[ class "search-result-button"
, Html.Events.custom "click" <|
Json.Decode.succeed
{ message = open
, stopPropagation = True
, preventDefault = True
}
]
[ text item.source.attr_name ]
]
, td [] [ text item.source.pname ]
, td [] [ text item.source.pversion ]
, td [] [ text <| Maybe.withDefault "" item.source.description ]
]
:: packageDetails
)
viewResultItemDetails :
String
-> Search.ResultItem ResultItemSource
-> Html Msg
viewResultItemDetails channel item =
viewResultItem channel showNixOSDetails show item =
let
default =
"Not specified"
asText =
text
asLink value =
a [ href value ] [ text value ]
githubUrlPrefix branch =
"https://github.com/NixOS/nixpkgs/blob/" ++ branch ++ "/"
cleanPosition =
Regex.fromString "^[0-9a-f]+\\.tar\\.gz\\/"
|> Maybe.withDefault Regex.never
>> (\reg -> Regex.replace reg (\_ -> ""))
asGithubLink value =
case Search.channelDetailsFromId channel of
Just channelDetails ->
a
[ href <| githubUrlPrefix channelDetails.branch ++ (value |> String.replace ":" "#L" |> cleanPosition)
, target "_blank"
]
[ text <| cleanPosition value ]
createGithubUrl branch value =
let
uri =
value
|> String.replace ":" "#L"
|> cleanPosition
in
"https://github.com/NixOS/nixpkgs/blob/" ++ branch ++ "/" ++ uri
Nothing ->
text <| cleanPosition value
mainPlatforms platform =
List.member platform
[ "x86_64-linux"
, "aarch64-linux"
, "x86_64-darwin"
, "i686-linux"
createShortDetailsItem title url =
a
[ href url
, target "_blank"
]
[ text title ]
getHydraDetailsForPlatform hydra platform =
hydra
|> Maybe.andThen
(\hydras ->
hydras
|> List.filter (\x -> x.platform == platform)
shortPackageDetails =
ul []
((item.source.position
|> Maybe.map
(\position ->
case Search.channelDetailsFromId channel of
Nothing ->
[]
Just channelDetails ->
[ li [ trapClick ]
[ createShortDetailsItem
"Source"
(createGithubUrl channelDetails.branch position)
]
]
)
|> Maybe.withDefault []
)
|> List.append
(item.source.homepage
|> List.head
)
|> Maybe.map
(\x ->
[ li [ trapClick ]
[ createShortDetailsItem "Homepage" x ]
]
)
|> Maybe.withDefault []
)
|> List.append
(item.source.licenses
|> List.filterMap
(\license ->
case ( license.fullName, license.url ) of
( Nothing, Nothing ) ->
Nothing
showPlatforms hydra platforms =
platforms
|> List.filter mainPlatforms
|> List.map (showPlatform hydra)
( Just fullName, Nothing ) ->
Just (text fullName)
showPlatform hydra platform =
case
( getHydraDetailsForPlatform hydra platform
, Search.channelDetailsFromId channel
( Nothing, Just url ) ->
Just (createShortDetailsItem "Unknown" url)
( Just fullName, Just url ) ->
Just (createShortDetailsItem fullName url)
)
|> List.intersperse (text ", ")
|> (\x -> [ li [] (List.append [ text "Licenses: " ] x) ])
)
|> List.append
[ text "Name: "
, li [] [ text item.source.pname ]
, text "Version: "
, li [] [ text item.source.pversion ]
]
)
of
( Just hydraDetails, _ ) ->
a
[ href <| "https://hydra.nixos.org/build/" ++ String.fromInt hydraDetails.build_id
]
[ text platform
]
( Nothing, Just channelDetails ) ->
a
[ href <| "https://hydra.nixos.org/job/" ++ channelDetails.jobset ++ "/nixpkgs." ++ item.source.attr_name ++ "." ++ platform
]
[ text platform
]
( _, _ ) ->
text platform
showLicence license =
case ( license.fullName, license.url ) of
( Nothing, Nothing ) ->
text default
( Just fullName, Nothing ) ->
text fullName
( Nothing, Just url ) ->
a [ href url ] [ text default ]
( Just fullName, Just url ) ->
a [ href url ] [ text fullName ]
showMaintainer maintainer =
a
[ href <|
case maintainer.github of
Just github ->
"https://github.com/" ++ github
li []
[ a
[ href <|
case maintainer.github of
Just github ->
"https://github.com/" ++ github
Nothing ->
"#"
Nothing ->
"#"
]
[ text <| Maybe.withDefault "" maintainer.name ++ " <" ++ Maybe.withDefault "" maintainer.email ++ ">" ]
]
[ text <| Maybe.withDefault "" maintainer.name ++ " <" ++ Maybe.withDefault "" maintainer.email ++ ">" ]
asPre value =
pre [] [ text value ]
showPlatform platform =
case Search.channelDetailsFromId channel of
Just channelDetails ->
let
url =
"https://hydra.nixos.org/job/" ++ channelDetails.jobset ++ "/nixpkgs." ++ item.source.attr_name ++ "." ++ platform
in
li []
[ a
[ href url
]
[ text platform ]
]
asCode value =
code [] [ text value ]
asList list =
case list of
[] ->
asPre default
_ ->
ul [ class "inline" ] <| List.map (\i -> li [] [ i ]) list
withEmpty wrapWith maybe =
case maybe of
Nothing ->
asPre default
li [] [ text platform ]
Just "" ->
asPre default
maintainersAndPlatforms =
[ div []
[ div []
(List.append [ h4 [] [ text "Maintainers" ] ]
(if List.isEmpty item.source.maintainers then
[ p [] [ text "This package has no maintainers." ] ]
Just value ->
wrapWith value
else
[ ul [] (List.map showMaintainer item.source.maintainers) ]
)
)
, div []
(List.append [ h4 [] [ text "Platforms" ] ]
(if List.isEmpty item.source.platforms then
[ p [] [ text "This package is not available on any platform." ] ]
else
[ ul [] (List.map showPlatform item.source.platforms) ]
)
)
]
]
longerPackageDetails =
if Just item.source.attr_name == show then
[ div [ trapClick ]
(maintainersAndPlatforms
|> List.append
(item.source.longDescription
|> Maybe.map (\desc -> [ p [] [ text desc ] ])
|> Maybe.withDefault []
)
|> List.append
[ div []
[ h4 []
[ text "How to install "
, em [] [ text item.source.attr_name ]
, text "?"
]
, ul [ class "nav nav-tabs" ]
[ li
[ classList
[ ( "active", showNixOSDetails )
, ( "pull-right", True )
]
]
[ a
[ href "#"
, Search.onClickStop <|
SearchMsg <|
Search.ShowNixOSDetails True
]
[ text "On NixOS" ]
]
, li
[ classList
[ ( "active", not showNixOSDetails )
, ( "pull-right", True )
]
]
[ a
[ href "#"
, Search.onClickStop <|
SearchMsg <|
Search.ShowNixOSDetails False
]
[ text "On non-NixOS" ]
]
]
, div
[ class "tab-content" ]
[ div
[ classList
[ ( "active", not showNixOSDetails )
]
, class "tab-pane"
, id "package-details-nixpkgs"
]
[ pre [ class "code-block" ]
[ text "nix-env -iA nixpkgs."
, strong [] [ text item.source.attr_name ]
]
]
, div
[ classList
[ ( "tab-pane", True )
, ( "active", showNixOSDetails )
]
]
[ pre [ class "code-block" ]
[ text <| "nix-env -iA nixos."
, strong [] [ text item.source.attr_name ]
]
]
]
]
]
)
]
else
[]
toggle =
SearchMsg (Search.ShowDetails item.source.attr_name)
trapClick =
Html.Attributes.map SearchMsg Search.trapClick
isOpen =
Just item.source.attr_name == show
in
dl [ class "dl-horizontal" ]
[ dt [] [ text "Attribute Name" ]
, dd [] [ withEmpty asText (Just item.source.attr_name) ]
, dt [] [ text "Name" ]
, dd [] [ withEmpty asText (Just item.source.pname) ]
, dt [] [ text "Install command" ]
, dd [] [ withEmpty asCode (Just ("nix-env -iA nixos." ++ item.source.attr_name)) ]
, dt [] [ text "Nix expression" ]
, dd [] [ withEmpty asGithubLink item.source.position ]
, dt [] [ text "Platforms" ]
, dd [] [ asList (showPlatforms item.source.hydra item.source.platforms) ]
, dt [] [ text "Homepage" ]
, dd [] <| List.intersperse (Html.text ", ") <| List.map asLink item.source.homepage
, dt [] [ text "Licenses" ]
, dd [] [ asList (List.map showLicence item.source.licenses) ]
, dt [] [ text "Maintainers" ]
, dd [] [ asList (List.map showMaintainer item.source.maintainers) ]
, dt [] [ text "Description" ]
, dd [] [ withEmpty asText item.source.description ]
, dt [] [ text "Long description" ]
, dd [] [ withEmpty asText item.source.longDescription ]
li
[ class "package"
, classList [ ( "opened", isOpen ) ]
, Search.elementId item.source.attr_name
]
([]
|> List.append longerPackageDetails
|> List.append
[ Html.button
[ class "search-result-button"
, onClick toggle
]
[ text item.source.attr_name ]
, div [] [ text <| Maybe.withDefault "" item.source.description ]
, shortPackageDetails
, Search.showMoreButton toggle isOpen
]
)
@ -413,9 +562,60 @@ makeRequest :
-> String
-> Int
-> Int
-> Maybe String
-> Search.Sort
-> Cmd Msg
makeRequest options channel query from size sort =
makeRequest options channel query from size maybeBuckets sort =
let
currentBuckets =
initBuckets maybeBuckets
aggregations =
[ ( "package_attr_set", currentBuckets.packageSets )
, ( "package_license_set", currentBuckets.licenses )
, ( "package_maintainers_set", currentBuckets.maintainers )
, ( "package_platforms", currentBuckets.platforms )
]
filterByBucket field value =
[ ( "term"
, Json.Encode.object
[ ( field
, Json.Encode.object
[ ( "value", Json.Encode.string value )
, ( "_name", Json.Encode.string <| "filter_bucket_" ++ field )
]
)
]
)
]
filterByBuckets =
[ ( "bool"
, Json.Encode.object
[ ( "must"
, Json.Encode.list Json.Encode.object
(List.map
(\( aggregation, buckets ) ->
[ ( "bool"
, Json.Encode.object
[ ( "should"
, Json.Encode.list Json.Encode.object <|
List.map
(filterByBucket aggregation)
buckets
)
]
)
]
)
aggregations
)
)
]
)
]
in
Search.makeRequest
(Search.makeRequestBody
(String.trim query)
@ -424,6 +624,12 @@ makeRequest options channel query from size sort =
sort
"package"
"package_attr_name"
[ "package_attr_set"
, "package_license_set"
, "package_maintainers_set"
, "package_platforms"
]
filterByBuckets
[ ( "package_attr_name", 9.0 )
, ( "package_pname", 6.0 )
, ( "package_attr_name_query", 4.0 )
@ -433,6 +639,7 @@ makeRequest options channel query from size sort =
)
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ channel)
decodeResultItemSource
decodeResultAggregations
options
Search.QueryResponse
(Just "query-packages")
@ -443,20 +650,25 @@ makeRequest options channel query from size sort =
-- JSON
decodeHomepage : Json.Decode.Decoder (List String)
decodeHomepage =
Json.Decode.oneOf
-- null becomes [] (empty list)
[ Json.Decode.null []
-- "foo" becomes ["foo"]
, Json.Decode.map List.singleton Json.Decode.string
-- arrays are decoded to list as expected
, Json.Decode.list Json.Decode.string
encodeBuckets : Buckets -> Json.Encode.Value
encodeBuckets options =
Json.Encode.object
[ ( "package_attr_set", Json.Encode.list Json.Encode.string options.packageSets )
, ( "package_license_set", Json.Encode.list Json.Encode.string options.licenses )
, ( "package_maintainers_set", Json.Encode.list Json.Encode.string options.maintainers )
, ( "package_platforms", Json.Encode.list Json.Encode.string options.platforms )
]
decodeBuckets : Json.Decode.Decoder Buckets
decodeBuckets =
Json.Decode.map4 Buckets
(Json.Decode.field "package_attr_set" (Json.Decode.list Json.Decode.string))
(Json.Decode.field "package_license_set" (Json.Decode.list Json.Decode.string))
(Json.Decode.field "package_maintainers_set" (Json.Decode.list Json.Decode.string))
(Json.Decode.field "package_platforms" (Json.Decode.list Json.Decode.string))
decodeResultItemSource : Json.Decode.Decoder ResultItemSource
decodeResultItemSource =
Json.Decode.succeed ResultItemSource
@ -474,6 +686,20 @@ decodeResultItemSource =
|> Json.Decode.Pipeline.required "package_hydra" (Json.Decode.nullable (Json.Decode.list decodeResultPackageHydra))
decodeHomepage : Json.Decode.Decoder (List String)
decodeHomepage =
Json.Decode.oneOf
-- null becomes [] (empty list)
[ Json.Decode.null []
-- "foo" becomes ["foo"]
, Json.Decode.map List.singleton Json.Decode.string
-- arrays are decoded to list as expected
, Json.Decode.list Json.Decode.string
]
decodeResultPackageLicense : Json.Decode.Decoder ResultPackageLicense
decodeResultPackageLicense =
Json.Decode.map2 ResultPackageLicense
@ -507,3 +733,23 @@ decodeResultPackageHydraPath =
Json.Decode.map2 ResultPackageHydraPath
(Json.Decode.field "output" Json.Decode.string)
(Json.Decode.field "path" Json.Decode.string)
decodeResultAggregations : Json.Decode.Decoder ResultAggregations
decodeResultAggregations =
Json.Decode.map5 ResultAggregations
(Json.Decode.field "all" decodeAggregations)
(Json.Decode.field "package_platforms" Search.decodeAggregation)
(Json.Decode.field "package_attr_set" Search.decodeAggregation)
(Json.Decode.field "package_maintainers_set" Search.decodeAggregation)
(Json.Decode.field "package_license_set" Search.decodeAggregation)
decodeAggregations : Json.Decode.Decoder Aggregations
decodeAggregations =
Json.Decode.map5 Aggregations
(Json.Decode.field "doc_count" Json.Decode.int)
(Json.Decode.field "package_platforms" Search.decodeAggregation)
(Json.Decode.field "package_attr_set" Search.decodeAggregation)
(Json.Decode.field "package_maintainers_set" Search.decodeAggregation)
(Json.Decode.field "package_license_set" Search.decodeAggregation)

View file

@ -28,6 +28,7 @@ type alias SearchArgs =
, show : Maybe String
, from : Maybe Int
, size : Maybe Int
, buckets : Maybe String
-- TODO: embed sort type
, sort : Maybe String
@ -53,6 +54,7 @@ searchQueryParser url =
<?> Url.Parser.Query.string "show"
<?> Url.Parser.Query.int "from"
<?> Url.Parser.Query.int "size"
<?> Url.Parser.Query.string "buckets"
<?> Url.Parser.Query.string "sort"
@ -63,6 +65,7 @@ searchArgsToUrl args =
, Maybe.map (Url.Builder.string "show") args.show
, Maybe.map (Url.Builder.int "from") args.from
, Maybe.map (Url.Builder.int "size") args.size
, Maybe.map (Url.Builder.string "buckets") args.buckets
, Maybe.map (Url.Builder.string "sort") args.sort
]
, Maybe.map (Tuple.pair "query") args.query

File diff suppressed because it is too large Load diff

13
src/Utils.elm Normal file
View file

@ -0,0 +1,13 @@
module Utils exposing (toggleList)
toggleList :
List a
-> a
-> List a
toggleList list item =
if List.member item list then
List.filter (\x -> x /= item) list
else
List.append list [ item ]

View file

@ -20,11 +20,12 @@
<link rel="search" type="application/opensearchdescription+xml" title="NixOS packages"
href="/desc-search-packages.xml">
<link rel="search" type="application/opensearchdescription+xml" title="NixOS options"
href="/desc-search-options.xml">
<link rel="search" type="application/opensearchdescription+xml" title="NixOS options" href="/desc-search-options.xml">
</head>
<body>
<script src="https://nixos.org/js/jquery.min.js"></script>
<script src="https://nixos.org/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

View file

@ -11,4 +11,4 @@ Elm.Main.init({
elasticsearchUsername : process.env.ELASTICSEARCH_USERNAME || 'z3ZFJ6y2mR',
elasticsearchPassword : process.env.ELASTICSEARCH_PASSWORD || 'ds8CEvALPf9pui7XG'
}
});
});

View file

@ -1,7 +1,66 @@
/* ------------------------------------------------------------------------- */
/* -- Utils ---------------------------------------------------------------- */
/* ------------------------------------------------------------------------- */
.terminal() {
background: #333;
color: #fff;
margin: 0;
&:before {
content: "$ "
}
}
.search-result-item() {
.result-item-show-more-wrapper {
text-align: center;
}
// show longer details link
.result-item-show-more {
margin: 0 auto;
display: none;
text-align: center;
text-decoration: none;
line-height: 1.5em;
color: #666;
background: #FFF;
padding: 0 1em;
position: relative;
top: 0.75em;
outline: none;
}
&.opened,
&:hover {
padding-bottom: 0;
.result-item-show-more {
display: inline-block;
padding-top: 0.5em;
}
}
}
/* ------------------------------------------------------------------------- */
/* -- Layout --------------------------------------------------------------- */
/* ------------------------------------------------------------------------- */
body {
position: relative;
min-height: 100vh;
overflow-y: scroll;
overflow-y: auto;
& > div:first-child {
position: relative;
min-height: 100vh;
}
}
.code-block {
display: block;
cursor: text;
}
#content {
@ -30,91 +89,394 @@ header .navbar.navbar-static-top {
}
}
.search-input {
.input-append {
input {
font-size: 18px;
height: 40px;
width: 25em;
}
div.loader {
display: none;
z-index: 100;
position: absolute;
margin: 0;
top: 37px;
right: 125px;
font-size: 6px;
color: #999999;
}
button {
font-size: 24px;
height: 50px;
min-width: 4em;
// Search seatch
.search-page {
&.not-asked {
& > h1 {
margin-top: 2.5em;
margin-bottom: 0.8em;
}
}
form > p > strong {
vertical-align: middle;
font-size: 1.2em;
margin-left: 0.2em;
}
}
.search-result {
tbody > tr {
cursor: pointer;
}
tbody > td > dl > dd > ul.inline {
margin: 0;
li {
margin: 0;
padding: 0;
}
li::after {
content: ", ";
padding-right: 0.5em;
}
li:last-child::after {
content: "";
// Search section title title
& > h1 {
font-weight: normal;
font-size: 2.3em;
&:before {
content: "\2315";
display: inline-block;
font-size: 1.5em;
margin-right: 0.2em;
-moz-transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
tbody > td > dl > dd > pre {
background: transparent;
border: 0;
padding: 0;
line-height: 20px;
margin: 0;
// Search input section
& > .search-input {
// Search Input and Button
& > div:nth-child(1) {
display: grid;
grid-template-columns: auto 8em;
& > div > input {
font-size: 18px;
height: 40px;
width: 100%;
}
& > button {
font-size: 24px;
height: 50px;
min-width: 4em;
}
}
// List of channels
& > div:nth-child(2) {
margin-bottom: 0.5em;
// "Channels: " label
& > div > h4 {
display: inline;
vertical-align: middle;
font-size: 1.2em;
margin-left: 0.2em;
}
}
}
tbody > td > dl > dt,
tbody > td > dl > dd {
margin-bottom: 1em;
// Loader during loading the search results
& > .loader-wrapper > h2 {
position: absolute;
top: 3em;
width: 100%;
text-align: center;
}
& > .search-no-results {
padding: 2em 1em;
text-align: center;
margin-bottom: 2em;
& > h2 {
margin-top: 0;
}
}
.search-result-button {
margin: 0;
padding: 0;
border: 0;
background: transparent;
display: inline;
font: inherit;
color: inherit;
text-align: inherit;
&:hover {
text-decoration: underline;
}
}
& > .search-results {
display: flex;
flex-direction: row;
// Buckets
& > ul {
list-style: none;
margin: 0 1em 0 0;
& > li {
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 1em;
border-radius: 4px;
& > ul {
list-style: none;
margin: 0;
& > li {
margin-bottom: 0.2em;
&.header {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 0.5em;
}
& > a {
display: grid;
grid-template-columns: auto auto;
color: #333;
padding: 0.5em 0.5em 0.5em 1em;
text-decoration: none;
&:hover {
text-decoration: none;
background: #eee;
border-radius: 4px;
}
& > span:first-child {
overflow: hidden;
}
& > span:last-child {
text-align: right;
margin-left: 0.3em;
}
&.selected {
background: #0081c2;
color: #FFF;
border-radius: 4px;
position: relative;
&:after {
content: "\2715";
font-size: 1.7em;
color: #fff;
position: absolute;
top: 4px;
right: 4px;
background: #0081c2;
bottom: 4px;
width: 1em;
padding: 2px;
}
& > span:last-child {
display: none;
}
}
}
}
}
}
}
// Results section
& > div {
width: 100%;
// Search results header
& > :nth-child(1) {
// Dropdown to show sorting options
& > div:nth-child(1) {
& > button {
& > .selected {
margin-right: 0.5em;
}
}
& > ul > li {
& > a {
padding: 3px 10px;
}
& > a:before {
display: inline-block;
content: " ";
width: 24.5px;
}
&.selected > a:before {
content: "\2714";
}
}
& > ul > li.header {
font-weight: bold;
padding: 3px 10px;
}
& > ul > li.header:before,
& > ul > li.divider:before {
display: none;
}
}
// Text that displays number of results
& > div:nth-child(2) {
font-size: 1.7em;
line-height: 1.3em;
& > p {
font-size: 0.7em;
}
}
}
// Search results list
& > :nth-child(2) {
list-style: none;
margin: 2em 0 0 0;
// Result item
& > li {
border-bottom: 1px solid #ccc;
padding-bottom: 2em;
margin-bottom: 2em;
&:last-child {
border-bottom: 0;
}
// Attribute name or option name
& > :nth-child(1) {
background: inherit;
border: 0;
padding: 0;
color: #08c;
font-size: 1.5em;
margin-bottom: 0.5em;
text-align: left;
display: block;
}
// Description
& > :nth-child(2) {
font-size: 1.2em;
margin-bottom: 0.5em;
text-align: left;
}
&.package {
.search-result-item();
// short details of a pacakge
& > :nth-child(3) {
color: #666;
list-style: none;
text-align: left;
margin: 0;
& > li {
display: inline-block;
margin-right: 1em;
& > a {
color: #666;
}
}
& > li:last-child {
margin-right: 0;
}
}
// longer details of a pacakge
& > :nth-child(5) {
margin: 2em 0 1em 1em;
text-align: left;
// how to install a package
& > :nth-child(1) {
h4 {
font-size: 1.2em;
line-height: 1em;
float: left;
}
ul.nav-tabs {
margin: 0;
& > li > a {
margin-right: 0;
}
}
div.tab-content {
padding: 1em;
border: 1px solid #ddd;
border-top: 0;
}
pre {
.terminal();
}
}
// long description of a package
& > :nth-child(2) {
margin-top: 1em;
}
// maintainers and platforms
& > :nth-child(3) {
margin-top: 1em;
display: grid;
grid-template-columns: auto auto;
}
}
}
&.option {
.search-result-item();
// short details of a pacakge
& > :nth-child(4) {
margin: 2em 0 1em 1em;
display: grid;
grid-template-columns: 100px 1fr;
column-gap: 1em;
row-gap: 0.5em;
& > div:nth-child(2n+1) {
font-weight: bold;
text-align: right;
}
& > div:nth-child(2n) {
pre {
background: transparent;
margin: 0;
padding: 0;
border: 0;
vertical-align: inherit;
display: inline;
}
pre code {
background: #333;
color: #fff;
padding: 0.5em
}
}
}
}
}
}
// Search results footer
& > :nth-child(3) {
margin-top: 1em;
& > ul > li > a {
cursor: pointer;
margin: 0 2px;
}
}
}
}
}
.sort-form,
.sort-form > .sort-group {
margin-bottom: 0;
}
.pager {
& > li > a {
cursor: pointer;
margin: 0 2px;
}
}
/* ------------------------------------------------------------------------- */
/* -- Loader --------------------------------------------------------------- */
/* ------------------------------------------------------------------------- */
.loader-wrapper {
height: 200px;
overflow: hidden;
position: relative;
}
.loader,
.loader:before,