diff --git a/Dockerfile b/Dockerfile index 1f3d650..aae308d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.12.2-erlang-24.0.5-ubuntu-xenial-20210114 +FROM hexpm/elixir:1.13.3-erlang-24.0.5-ubuntu-xenial-20210114 # install build dependencies USER root RUN apt-get update @@ -9,6 +9,8 @@ RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update RUN apt-get install -y nodejs yarn +RUN mix local.hex --force && \ + mix local.rebar --force ENV MIX_ENV=prod COPY ./assets/package.json assets/package.json COPY ./assets/package-lock.json assets/package-lock.json @@ -17,9 +19,7 @@ COPY ./mix.exs . COPY ./mix.lock . COPY ./config/config.exs config/config.exs COPY ./config/prod.exs config/prod.exs -RUN mix local.hex --force && \ - mix local.rebar --force && \ - mix deps.get && \ +RUN mix deps.get && \ mix deps.compile COPY ./lib ./lib COPY ./priv ./priv diff --git a/assets/css/syntax.css b/assets/css/syntax.css index 711fe6e..a430d19 100644 --- a/assets/css/syntax.css +++ b/assets/css/syntax.css @@ -12,10 +12,10 @@ } /* Comment */ -.dark .highlight .err { +/* .dark .highlight .err { color: #a61717; background-color: #e3d2d2 -} +} */ /* Error */ .dark .highlight .g { diff --git a/config/config.exs b/config/config.exs index 6037cc4..2d79132 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,6 +10,8 @@ import Config config :ash_hq, ecto_repos: [AshHq.Repo] +config :ash, allow_flow: true + config :ash_hq, ash_apis: [AshHq.Docs] config :ash_hq, AshHq.Docs, diff --git a/lib/ash_hq/docs/changes/load_search_data.ex b/lib/ash_hq/docs/changes/load_search_data.ex index b5797cd..8547001 100644 --- a/lib/ash_hq/docs/changes/load_search_data.ex +++ b/lib/ash_hq/docs/changes/load_search_data.ex @@ -3,12 +3,17 @@ defmodule AshHq.Docs.Preparations.LoadSearchData do def prepare(query, _, _) do query_string = Ash.Query.get_argument(query, :query) + to_load = AshHq.Docs.Extensions.Search.load_for_search(query.resource) if query_string do query - |> Ash.Query.load(search_headline: [query: query_string]) + |> Ash.Query.load( + search_headline: [query: query_string], + match_rank: [query: query_string], + name_matches: %{query: query_string, similarity: 0.7} + ) + |> Ash.Query.load(to_load) |> Ash.Query.sort(match_rank: {:asc, %{query: query_string}}) - |> Ash.Query.load(match_rank: [query: query_string]) else query end diff --git a/lib/ash_hq/docs/extensions/search/transformers/add_search_structure.ex b/lib/ash_hq/docs/extensions/search/transformers/add_search_structure.ex index 2004b1b..c3cec87 100644 --- a/lib/ash_hq/docs/extensions/search/transformers/add_search_structure.ex +++ b/lib/ash_hq/docs/extensions/search/transformers/add_search_structure.ex @@ -174,6 +174,9 @@ defmodule AshHq.Docs.Extensions.Search.Transformers.AddSearchStructure do [ Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare, preparation: AshHq.Docs.Preparations.LoadSearchData + ), + Transformer.build_entity!(Ash.Resource.Dsl, [:actions, :read], :prepare, + preparation: Ash.Resource.Preparation.Builtins.build(limit: 10) ) ] end diff --git a/lib/ash_hq/docs/flows/search/search.ex b/lib/ash_hq/docs/flows/search/search.ex new file mode 100644 index 0000000..5b90b99 --- /dev/null +++ b/lib/ash_hq/docs/flows/search/search.ex @@ -0,0 +1,80 @@ +defmodule AshHq.Docs.Search do + use Ash.Flow + + flow do + api AshHq.Docs + + argument :query, :string do + allow_nil? false + end + + argument :library_versions, {:array, :uuid} do + allow_nil? false + end + + returns :build_results + end + + steps do + read :options, AshHq.Docs.Option, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :dsls, AshHq.Docs.Dsl, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :guides, AshHq.Docs.Guide, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :library_versions, AshHq.Docs.LibraryVersion, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :extensions, AshHq.Docs.Extension, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :functions, AshHq.Docs.Function, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + read :modules, AshHq.Docs.Module, :search do + input %{ + library_versions: arg(:library_versions), + query: arg(:query) + } + end + + custom :build_results, AshHq.Docs.Search.Steps.BuildResults do + input %{ + dsls: result(:dsls), + options: result(:options), + guides: result(:guides), + library_versions: result(:library_versions), + extensions: result(:extensions), + functions: result(:functions), + modules: result(:modules) + } + end + end +end diff --git a/lib/ash_hq/docs/flows/search/steps/build_results.ex b/lib/ash_hq/docs/flows/search/steps/build_results.ex new file mode 100644 index 0000000..a064f27 --- /dev/null +++ b/lib/ash_hq/docs/flows/search/steps/build_results.ex @@ -0,0 +1,132 @@ +defmodule AshHq.Docs.Search.Steps.BuildResults do + use Ash.Flow.Step + + def run(input, _opts, _context) do + search_results = + input + |> Map.values() + |> List.flatten() + |> Enum.sort_by( + # false comes first, and we want all things where the name matches to go first + &{name_match_rank(&1), -&1.match_rank, Map.get(&1, :extension_order, -1), + Enum.count(Map.get(&1, :path, []))} + ) + + sort_rank = + search_results + |> Enum.with_index() + |> Map.new(fn {item, i} -> + {item.id, i} + end) + + results = + search_results + |> Enum.group_by(fn + %{extension_type: type} -> + type + + %AshHq.Docs.Function{module_name: module_name} -> + module_name + + %AshHq.Docs.Module{name: name} -> + name + + %AshHq.Docs.Guide{ + library_version: %{version: version, library_display_name: library_display_name} + } -> + "#{library_display_name} #{version}" + + %AshHq.Docs.Extension{ + library_version: %{version: version, library_display_name: library_display_name} + } -> + "#{library_display_name} #{version}" + + %AshHq.Docs.LibraryVersion{library_display_name: library_display_name, version: version} -> + "#{library_display_name} #{version}" + + other -> + raise "Nothing matching #{inspect(other)}" + end) + |> Enum.sort_by(fn {_type, items} -> + items + |> Enum.map(&Map.get(sort_rank, &1.id)) + |> Enum.min() + end) + |> Enum.map(fn {type, items} -> + {type, group_by_paths(items, sort_rank)} + end) + + item_list = item_list(results) + + {:ok, %{item_list: item_list, results: results}} + end + + defp item_list(results) do + List.flatten(do_item_list(results)) + end + + defp do_item_list({_key, %{items: items, further: further}}) do + do_item_list(items) ++ do_item_list(further) + end + + defp do_item_list(items) when is_list(items) do + Enum.map(items, &do_item_list/1) + end + + defp do_item_list(item), do: item + + defp group_by_paths(items, sort_rank) do + items + |> Enum.map(&{Map.get(&1, :path, []), &1}) + |> do_group_by_paths(sort_rank) + end + + defp do_group_by_paths(items, sort_rank, path_acc \\ []) do + {items_for_group, further} = + Enum.split_with(items, fn + {[], _} -> + true + + _ -> + false + end) + + further_items = + further + |> Enum.group_by( + fn {[next | _rest], _item} -> + next + end, + fn {[_next | rest], item} -> + {rest, item} + end + ) + |> Enum.sort_by(fn {_nested, items} -> + items + |> Enum.map(&elem(&1, 1)) + |> Enum.sort_by(&Map.get(sort_rank, &1.id)) + end) + |> Enum.map(fn {nested, items} -> + {nested, do_group_by_paths(items, sort_rank, path_acc ++ [nested])} + end) + + items = + items_for_group + |> Enum.map(&elem(&1, 1)) + |> Enum.sort_by(&Map.get(sort_rank, &1.id)) + + %{path: path_acc, items: items, further: further_items} + end + + defp name_match_rank(record) do + if record.name_matches do + -search_length(record) + else + 0 + end + end + + defp search_length(%resource{} = record) do + String.length(Map.get(record, AshHq.Docs.Extensions.Search.doc_attribute(resource))) + end +end diff --git a/lib/ash_hq/docs/resources/dsl/dsl.ex b/lib/ash_hq/docs/resources/dsl/dsl.ex index 73bbecd..f3f9d22 100644 --- a/lib/ash_hq/docs/resources/dsl/dsl.ex +++ b/lib/ash_hq/docs/resources/dsl/dsl.ex @@ -32,9 +32,7 @@ defmodule AshHq.Docs.Dsl do end actions do - read :read do - primary? true - end + defaults [:read, :destroy] create :create do argument :options, {:array, :map} diff --git a/lib/ash_hq/docs/resources/extension/extension.ex b/lib/ash_hq/docs/resources/extension/extension.ex index ef1a65c..770df0b 100644 --- a/lib/ash_hq/docs/resources/extension/extension.ex +++ b/lib/ash_hq/docs/resources/extension/extension.ex @@ -32,6 +32,8 @@ defmodule AshHq.Docs.Extension do end actions do + defaults [:read, :update, :destroy] + create :import do argument :library_version, :uuid do allow_nil? false diff --git a/lib/ash_hq/docs/resources/function/function.ex b/lib/ash_hq/docs/resources/function/function.ex index 3d22fc6..34cd3c7 100644 --- a/lib/ash_hq/docs/resources/function/function.ex +++ b/lib/ash_hq/docs/resources/function/function.ex @@ -20,7 +20,7 @@ defmodule AshHq.Docs.Function do postgres do table "functions" - repo(AshHq.Repo) + repo AshHq.Repo references do reference(:library_version, on_delete: :delete) @@ -68,7 +68,10 @@ defmodule AshHq.Docs.Function do end actions do + defaults [:read, :update, :destroy] + create :create do + primary? true argument :library_version, :uuid change manage_relationship(:library_version, type: :replace) diff --git a/lib/ash_hq/docs/resources/guide/changes/set_route.ex b/lib/ash_hq/docs/resources/guide/changes/set_route.ex new file mode 100644 index 0000000..7396a99 --- /dev/null +++ b/lib/ash_hq/docs/resources/guide/changes/set_route.ex @@ -0,0 +1,21 @@ +defmodule AshHq.Docs.Guide.Changes.SetRoute do + use Ash.Resource.Change + + def change(changeset, _, _) do + if !Ash.Changeset.get_attribute(changeset, :route) && + (Ash.Changeset.changing_attribute?(:name) || Ash.Changeset.changing_attribute?(:category)) do + category = Ash.Changeset.get_attribute(changeset, :category) + name = Ash.Changeset.get_attribute(changeset, :name) + Ash.Changeset.change_attribute(changeset, :route, to_path(category) <> "/" <> to_path(name)) + else + changeset + end + end + + defp to_path(string) do + string + |> String.split(~r/\s/, trim: true) + |> Enum.join("-") + |> String.downcase() + end +end diff --git a/lib/ash_hq/docs/resources/guide/guide.ex b/lib/ash_hq/docs/resources/guide/guide.ex index 882302b..303765f 100644 --- a/lib/ash_hq/docs/resources/guide/guide.ex +++ b/lib/ash_hq/docs/resources/guide/guide.ex @@ -9,13 +9,26 @@ defmodule AshHq.Docs.Guide do search do doc_attribute :text - load_for_search [:url_safe_name, library_version: [:library_name, :library_display_name]] + load_for_search library_version: [:library_name, :library_display_name] end code_interface do define_for AshHq.Docs end + actions do + defaults [:read, :update, :destroy] + + create :create do + primary? true + allow_nil_input [:route] + end + end + + changes do + change AshHq.Docs.Guide.Changes.SetRoute + end + postgres do repo AshHq.Repo table "guides" @@ -46,10 +59,15 @@ defmodule AshHq.Docs.Guide do constraints trim?: false, allow_empty?: true writable? false end - end - calculations do - calculate :url_safe_name, :string, expr(fragment("lower(replace(?, ' ', '-'))", name)) + attribute :category, :string do + default "Guides" + allow_nil? false + end + + attribute :route, :string do + allow_nil? false + end end relationships do diff --git a/lib/ash_hq/docs/resources/library/library.ex b/lib/ash_hq/docs/resources/library/library.ex index bb3f438..2ca6506 100644 --- a/lib/ash_hq/docs/resources/library/library.ex +++ b/lib/ash_hq/docs/resources/library/library.ex @@ -16,9 +16,7 @@ defmodule AshHq.Docs.Library do end actions do - read :read do - primary? true - end + defaults [:read, :create, :update, :destroy] read :by_name do argument :name, :string do diff --git a/lib/ash_hq/docs/resources/library_version/library_version.ex b/lib/ash_hq/docs/resources/library_version/library_version.ex index 75fe0b7..7e80a1b 100644 --- a/lib/ash_hq/docs/resources/library_version/library_version.ex +++ b/lib/ash_hq/docs/resources/library_version/library_version.ex @@ -33,9 +33,7 @@ defmodule AshHq.Docs.LibraryVersion do end actions do - read :read do - primary? true - end + defaults [:read, :update, :destroy] read :by_version do get? true diff --git a/lib/ash_hq/docs/resources/module/module.ex b/lib/ash_hq/docs/resources/module/module.ex index 04f9f97..aea4154 100644 --- a/lib/ash_hq/docs/resources/module/module.ex +++ b/lib/ash_hq/docs/resources/module/module.ex @@ -49,7 +49,10 @@ defmodule AshHq.Docs.Module do end actions do + defaults [:read, :update, :destroy] + create :create do + primary? true argument :functions, {:array, :map} argument :library_version, :uuid diff --git a/lib/ash_hq/docs/resources/option/option.ex b/lib/ash_hq/docs/resources/option/option.ex index 632c77b..afe93e4 100644 --- a/lib/ash_hq/docs/resources/option/option.ex +++ b/lib/ash_hq/docs/resources/option/option.ex @@ -64,11 +64,10 @@ defmodule AshHq.Docs.Option do end actions do - read :read do - primary? true - end + defaults [:read, :update, :destroy] create :create do + primary? true argument :library_version, :uuid argument :extension_id, :uuid do diff --git a/lib/ash_hq_web/components/doc_sidebar.ex b/lib/ash_hq_web/components/doc_sidebar.ex index a29b69a..f1e7282 100644 --- a/lib/ash_hq_web/components/doc_sidebar.ex +++ b/lib/ash_hq_web/components/doc_sidebar.ex @@ -37,28 +37,28 @@ defmodule AshHqWeb.Components.DocSidebar do {selected_version_name(library, @selected_versions)} {#if @library && @library_version && library.id == @library.id} - {#if !Enum.empty?(@library_version.guides)} + {#for {category, guides} <- guides_by_category(@library_version.guides)}