Adds faceted search (#261)
This commit is contained in:
parent
1c1a3ca7d1
commit
5bb94c9c92
|
@ -31,6 +31,8 @@ pkgs.stdenv.mkDerivation {
|
|||
name = "${package.name}-${package.version}";
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
|
||||
preferLocalBuild = true;
|
||||
|
||||
buildInputs =
|
||||
[
|
||||
yarnPkg
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 "<" "<"
|
||||
|> String.replace ">" ">"
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
872
src/Search.elm
872
src/Search.elm
File diff suppressed because it is too large
Load diff
13
src/Utils.elm
Normal file
13
src/Utils.elm
Normal 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 ]
|
|
@ -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>
|
||||
|
|
504
src/index.less
504
src/index.less
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue