refactor: make schema dump and swagger spec work with split desc files

This commit is contained in:
Zaiming (Stone) Shi 2023-04-07 11:18:29 +02:00
parent 9b7800aa8c
commit 18974a8e11
21 changed files with 375 additions and 205 deletions

View File

@ -239,7 +239,6 @@ $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
.PHONY: .PHONY:
merge-config: merge-config:
@$(SCRIPTS)/merge-config.escript @$(SCRIPTS)/merge-config.escript
@$(SCRIPTS)/merge-i18n.escript
## elixir target is to create release packages using Elixir's Mix ## elixir target is to create release packages using Elixir's Mix
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir) .PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)

View File

@ -29,7 +29,7 @@
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.6"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.6"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.6"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.6"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.38.1"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.39.1"}}},
{emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}}, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}},
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},

View File

@ -25,9 +25,9 @@
-export([update/3, update/4]). -export([update/3, update/4]).
-export([remove/2, remove/3]). -export([remove/2, remove/3]).
-export([reset/2, reset/3]). -export([reset/2, reset/3]).
-export([dump_schema/1, dump_schema/3]). -export([dump_schema/2]).
-export([schema_module/0]). -export([schema_module/0]).
-export([gen_example_conf/4]). -export([gen_example_conf/2]).
%% for rpc %% for rpc
-export([get_node_and_config/1]). -export([get_node_and_config/1]).
@ -136,24 +136,21 @@ reset(Node, KeyPath, Opts) ->
emqx_conf_proto_v2:reset(Node, KeyPath, Opts). emqx_conf_proto_v2:reset(Node, KeyPath, Opts).
%% @doc Called from build script. %% @doc Called from build script.
-spec dump_schema(file:name_all()) -> ok. dump_schema(Dir, SchemaModule) ->
dump_schema(Dir) -> _ = application:load(emqx_dashboard),
I18nFile = emqx_dashboard:i18n_file(), ok = emqx_dashboard_desc_cache:init(),
dump_schema(Dir, emqx_conf_schema, I18nFile).
dump_schema(Dir, SchemaModule, I18nFile) ->
lists:foreach( lists:foreach(
fun(Lang) -> fun(Lang) ->
gen_config_md(Dir, I18nFile, SchemaModule, Lang), ok = gen_config_md(Dir, SchemaModule, Lang),
gen_api_schema_json(Dir, I18nFile, Lang), ok = gen_api_schema_json(Dir, Lang),
gen_example_conf(Dir, I18nFile, SchemaModule, Lang), ok = gen_schema_json(Dir, SchemaModule, Lang)
gen_schema_json(Dir, I18nFile, SchemaModule, Lang)
end, end,
["en", "zh"] ["en", "zh"]
). ),
ok = gen_example_conf(Dir, SchemaModule).
%% for scripts/spellcheck. %% for scripts/spellcheck.
gen_schema_json(Dir, I18nFile, SchemaModule, Lang) -> gen_schema_json(Dir, SchemaModule, Lang) ->
SchemaJsonFile = filename:join([Dir, "schema-" ++ Lang ++ ".json"]), SchemaJsonFile = filename:join([Dir, "schema-" ++ Lang ++ ".json"]),
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]), io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
%% EMQX_SCHEMA_FULL_DUMP is quite a hidden API %% EMQX_SCHEMA_FULL_DUMP is quite a hidden API
@ -164,40 +161,44 @@ gen_schema_json(Dir, I18nFile, SchemaModule, Lang) ->
false -> ?IMPORTANCE_LOW false -> ?IMPORTANCE_LOW
end, end,
io:format(user, "===< Including fields from importance level: ~p~n", [IncludeImportance]), io:format(user, "===< Including fields from importance level: ~p~n", [IncludeImportance]),
Opts = #{desc_file => I18nFile, lang => Lang, include_importance_up_from => IncludeImportance}, Opts = #{
include_importance_up_from => IncludeImportance,
desc_resolver => make_desc_resolver(Lang)
},
JsonMap = hocon_schema_json:gen(SchemaModule, Opts), JsonMap = hocon_schema_json:gen(SchemaModule, Opts),
IoData = emqx_utils_json:encode(JsonMap, [pretty, force_utf8]), IoData = emqx_utils_json:encode(JsonMap, [pretty, force_utf8]),
ok = file:write_file(SchemaJsonFile, IoData). ok = file:write_file(SchemaJsonFile, IoData).
gen_api_schema_json(Dir, I18nFile, Lang) -> gen_api_schema_json(Dir, Lang) ->
emqx_dashboard:init_i18n(I18nFile, list_to_binary(Lang)),
gen_api_schema_json_hotconf(Dir, Lang), gen_api_schema_json_hotconf(Dir, Lang),
gen_api_schema_json_bridge(Dir, Lang), gen_api_schema_json_bridge(Dir, Lang).
emqx_dashboard:clear_i18n().
gen_api_schema_json_hotconf(Dir, Lang) -> gen_api_schema_json_hotconf(Dir, Lang) ->
SchemaInfo = #{title => <<"EMQX Hot Conf API Schema">>, version => <<"0.1.0">>}, SchemaInfo = #{title => <<"EMQX Hot Conf API Schema">>, version => <<"0.1.0">>},
File = schema_filename(Dir, "hot-config-schema-", Lang), File = schema_filename(Dir, "hot-config-schema-", Lang),
ok = do_gen_api_schema_json(File, emqx_mgmt_api_configs, SchemaInfo). ok = do_gen_api_schema_json(File, emqx_mgmt_api_configs, SchemaInfo, Lang).
gen_api_schema_json_bridge(Dir, Lang) -> gen_api_schema_json_bridge(Dir, Lang) ->
SchemaInfo = #{title => <<"EMQX Data Bridge API Schema">>, version => <<"0.1.0">>}, SchemaInfo = #{title => <<"EMQX Data Bridge API Schema">>, version => <<"0.1.0">>},
File = schema_filename(Dir, "bridge-api-", Lang), File = schema_filename(Dir, "bridge-api-", Lang),
ok = do_gen_api_schema_json(File, emqx_bridge_api, SchemaInfo). ok = do_gen_api_schema_json(File, emqx_bridge_api, SchemaInfo, Lang).
schema_filename(Dir, Prefix, Lang) -> schema_filename(Dir, Prefix, Lang) ->
Filename = Prefix ++ Lang ++ ".json", Filename = Prefix ++ Lang ++ ".json",
filename:join([Dir, Filename]). filename:join([Dir, Filename]).
gen_config_md(Dir, I18nFile, SchemaModule, Lang) -> %% TODO: remove it and also remove hocon_md.erl and friends.
%% markdown generation from schema is a failure and we are moving to an interactive
%% viewer like swagger UI.
gen_config_md(Dir, SchemaModule, Lang) ->
SchemaMdFile = filename:join([Dir, "config-" ++ Lang ++ ".md"]), SchemaMdFile = filename:join([Dir, "config-" ++ Lang ++ ".md"]),
io:format(user, "===< Generating: ~s~n", [SchemaMdFile]), io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang). ok = gen_doc(SchemaMdFile, SchemaModule, Lang).
gen_example_conf(Dir, I18nFile, SchemaModule, Lang) -> gen_example_conf(Dir, SchemaModule) ->
SchemaMdFile = filename:join([Dir, "emqx.conf." ++ Lang ++ ".example"]), SchemaMdFile = filename:join([Dir, "emqx.conf.example"]),
io:format(user, "===< Generating: ~s~n", [SchemaMdFile]), io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
ok = gen_example(SchemaMdFile, SchemaModule, I18nFile, Lang). ok = gen_example(SchemaMdFile, SchemaModule).
%% @doc return the root schema module. %% @doc return the root schema module.
-spec schema_module() -> module(). -spec schema_module() -> module().
@ -211,35 +212,48 @@ schema_module() ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok. %% @doc Make a resolver function that can be used to lookup the description by hocon_schema_json dump.
gen_doc(File, SchemaModule, I18nFile, Lang) -> make_desc_resolver(Lang) ->
fun
({desc, Namespace, Id}) ->
emqx_dashboard_desc_cache:lookup(Lang, Namespace, Id, desc);
(Desc) ->
unicode:characters_to_binary(Desc)
end.
-spec gen_doc(file:name_all(), module(), string()) -> ok.
gen_doc(File, SchemaModule, Lang) ->
Version = emqx_release:version(), Version = emqx_release:version(),
Title = Title =
"# " ++ emqx_release:description() ++ " Configuration\n\n" ++ "# " ++ emqx_release:description() ++ " Configuration\n\n" ++
"<!--" ++ Version ++ "-->", "<!--" ++ Version ++ "-->",
BodyFile = filename:join([rel, "emqx_conf.template." ++ Lang ++ ".md"]), BodyFile = filename:join([rel, "emqx_conf.template." ++ Lang ++ ".md"]),
{ok, Body} = file:read_file(BodyFile), {ok, Body} = file:read_file(BodyFile),
Opts = #{title => Title, body => Body, desc_file => I18nFile, lang => Lang}, Resolver = make_desc_resolver(Lang),
Opts = #{title => Title, body => Body, desc_resolver => Resolver},
Doc = hocon_schema_md:gen(SchemaModule, Opts), Doc = hocon_schema_md:gen(SchemaModule, Opts),
file:write_file(File, Doc). file:write_file(File, Doc).
gen_example(File, SchemaModule, I18nFile, Lang) -> gen_example(File, SchemaModule) ->
%% we do not generate description in example files
%% so there is no need for a desc_resolver
Opts = #{ Opts = #{
title => <<"EMQX Configuration Example">>, title => <<"EMQX Configuration Example">>,
body => <<"">>, body => <<"">>,
desc_file => I18nFile,
lang => Lang,
include_importance_up_from => ?IMPORTANCE_MEDIUM include_importance_up_from => ?IMPORTANCE_MEDIUM
}, },
Example = hocon_schema_example:gen(SchemaModule, Opts), Example = hocon_schema_example:gen(SchemaModule, Opts),
file:write_file(File, Example). file:write_file(File, Example).
%% Only gen hot_conf schema, not all configuration fields. %% Only gen hot_conf schema, not all configuration fields.
do_gen_api_schema_json(File, SchemaMod, SchemaInfo) -> do_gen_api_schema_json(File, SchemaMod, SchemaInfo, Lang) ->
io:format(user, "===< Generating: ~s~n", [File]), io:format(user, "===< Generating: ~s~n", [File]),
{ApiSpec0, Components0} = emqx_dashboard_swagger:spec( {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
SchemaMod, SchemaMod,
#{schema_converter => fun hocon_schema_to_spec/2} #{
schema_converter => fun hocon_schema_to_spec/2,
i18n_lang => Lang
}
), ),
ApiSpec = lists:foldl( ApiSpec = lists:foldl(
fun({Path, Spec, _, _}, Acc) -> fun({Path, Spec, _, _}, Acc) ->
@ -278,13 +292,6 @@ do_gen_api_schema_json(File, SchemaMod, SchemaInfo) ->
), ),
file:write_file(File, IoData). file:write_file(File, IoData).
-define(INIT_SCHEMA, #{
fields => #{},
translations => #{},
validations => [],
namespace => undefined
}).
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])). -define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), -define(TO_COMPONENTS_SCHEMA(_M_, _F_),
iolist_to_binary([ iolist_to_binary([

View File

@ -16,22 +16,13 @@
-module(emqx_dashboard). -module(emqx_dashboard).
-define(APP, ?MODULE).
-export([ -export([
start_listeners/0, start_listeners/0,
start_listeners/1, start_listeners/1,
stop_listeners/1, stop_listeners/1,
stop_listeners/0, stop_listeners/0,
list_listeners/0 list_listeners/0,
]). wait_for_listeners/0
-export([
init_i18n/2,
init_i18n/0,
get_i18n/0,
i18n_file/0,
clear_i18n/0
]). ]).
%% Authorization %% Authorization
@ -90,30 +81,34 @@ start_listeners(Listeners) ->
dispatch => Dispatch, dispatch => Dispatch,
middlewares => [?EMQX_MIDDLE, cowboy_router, cowboy_handler] middlewares => [?EMQX_MIDDLE, cowboy_router, cowboy_handler]
}, },
Res = {OkListeners, ErrListeners} =
lists:foldl( lists:foldl(
fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, Acc) -> fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, {OkAcc, ErrAcc}) ->
Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts}, Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts},
case minirest:start(Name, RanchOptions, Minirest) of case minirest:start(Name, RanchOptions, Minirest) of
{ok, _} -> {ok, _} ->
?ULOG("Listener ~ts on ~ts started.~n", [ ?ULOG("Listener ~ts on ~ts started.~n", [
Name, emqx_listeners:format_bind(Bind) Name, emqx_listeners:format_bind(Bind)
]), ]),
Acc; {[Name | OkAcc], ErrAcc};
{error, _Reason} -> {error, _Reason} ->
%% Don't record the reason because minirest already does(too much logs noise). %% Don't record the reason because minirest already does(too much logs noise).
[Name | Acc] {OkAcc, [Name | ErrAcc]}
end end
end, end,
[], {[], []},
listeners(Listeners) listeners(Listeners)
), ),
case Res of case ErrListeners of
[] -> ok; [] ->
_ -> {error, Res} optvar:set(emqx_dashboard_listeners_ready, OkListeners),
ok;
_ ->
{error, ErrListeners}
end. end.
stop_listeners(Listeners) -> stop_listeners(Listeners) ->
optvar:unset(emqx_dashboard_listeners_ready),
[ [
begin begin
case minirest:stop(Name) of case minirest:stop(Name) of
@ -129,23 +124,8 @@ stop_listeners(Listeners) ->
], ],
ok. ok.
get_i18n() -> wait_for_listeners() ->
application:get_env(emqx_dashboard, i18n). optvar:read(emqx_dashboard_listeners_ready).
init_i18n(File, Lang) when is_atom(Lang) ->
init_i18n(File, atom_to_binary(Lang));
init_i18n(File, Lang) when is_binary(Lang) ->
Cache = hocon_schema:new_desc_cache(File),
application:set_env(emqx_dashboard, i18n, #{lang => Lang, cache => Cache}).
clear_i18n() ->
case application:get_env(emqx_dashboard, i18n) of
{ok, #{cache := Cache}} ->
hocon_schema:delete_desc_cache(Cache),
application:unset_env(emqx_dashboard, i18n);
undefined ->
ok
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% internal %% internal
@ -187,11 +167,6 @@ ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port}; ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}. ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
init_i18n() ->
File = i18n_file(),
Lang = emqx_conf:get([dashboard, i18n_lang], en),
init_i18n(File, Lang).
ranch_opts(Options) -> ranch_opts(Options) ->
Keys = [ Keys = [
handshake_timeout, handshake_timeout,
@ -255,12 +230,6 @@ return_unauthorized(Code, Message) ->
}, },
#{code => Code, message => Message}}. #{code => Code, message => Message}}.
i18n_file() ->
case application:get_env(emqx_dashboard, i18n_file) of
undefined -> filename:join([code:priv_dir(emqx_dashboard), "i18n.conf"]);
{ok, File} -> File
end.
listeners() -> listeners() ->
emqx_conf:get([dashboard, listeners], #{}). emqx_conf:get([dashboard, listeners], #{}).

View File

@ -0,0 +1,105 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc This module is used to cache the description of the configuration items.
-module(emqx_dashboard_desc_cache).
-export([init/0]).
%% internal exports
-export([load_desc/2, lookup/4, lookup/5]).
-include_lib("emqx/include/logger.hrl").
%% @doc Global ETS table to cache the description of the configuration items.
%% The table is owned by the emqx_dashboard_sup the root supervisor of emqx_dashboard.
%% The cache is initialized with the default language (English) and
%% all the desc.<lang>.hocon files in the www/static directory (extracted from dashboard package).
init() ->
ok = ensure_app_loaded(emqx_dashboard),
PrivDir = code:priv_dir(emqx_dashboard),
EngDesc = filename:join([PrivDir, "desc.en.hocon"]),
WwwStaticDir = filename:join([PrivDir, "www", "static"]),
OtherLangDesc0 = filelib:wildcard("desc.*.hocon", WwwStaticDir),
OtherLangDesc = lists:map(fun(F) -> filename:join([WwwStaticDir, F]) end, OtherLangDesc0),
Files = [EngDesc | OtherLangDesc],
?MODULE = ets:new(?MODULE, [named_table, public, set, {read_concurrency, true}]),
ok = lists:foreach(fun(F) -> load_desc(?MODULE, F) end, Files).
%% @doc Load the description of the configuration items from the file.
%% Load is incremental, so it can be called multiple times.
%% NOTE: no garbage collection is done, because stale entries are harmless.
load_desc(EtsTab, File) ->
?SLOG(info, #{msg => "loading desc", file => File}),
{ok, Descs} = hocon:load(File),
["desc", Lang, "hocon"] = string:tokens(filename:basename(File), "."),
Insert = fun(Namespace, Id, Tag, Text) ->
Key = {bin(Lang), bin(Namespace), bin(Id), bin(Tag)},
true = ets:insert(EtsTab, {Key, bin(Text)}),
ok
end,
walk_ns(Insert, maps:to_list(Descs)).
%% @doc Lookup the description of the configuration item from the global cache.
lookup(Lang, Namespace, Id, Tag) ->
lookup(?MODULE, Lang, Namespace, Id, Tag).
%% @doc Lookup the description of the configuration item from the given cache.
lookup(EtsTab, Lang0, Namespace, Id, Tag) ->
Lang = bin(Lang0),
case ets:lookup(EtsTab, {Lang, bin(Namespace), bin(Id), bin(Tag)}) of
[{_, Desc}] ->
Desc;
[] when Lang =/= <<"en">> ->
%% fallback to English
lookup(EtsTab, <<"en">>, Namespace, Id, Tag);
_ ->
%% undefined but not <<>>
undefined
end.
%% The desc files are of names like:
%% desc.en.hocon or desc.zh.hocon
%% And with content like:
%% namespace.id.desc = "description"
%% namespace.id.label = "label"
walk_ns(_Insert, []) ->
ok;
walk_ns(Insert, [{Namespace, Ids} | Rest]) ->
walk_id(Insert, Namespace, maps:to_list(Ids)),
walk_ns(Insert, Rest).
walk_id(_Insert, _Namespace, []) ->
ok;
walk_id(Insert, Namespace, [{Id, Tags} | Rest]) ->
walk_tag(Insert, Namespace, Id, maps:to_list(Tags)),
walk_id(Insert, Namespace, Rest).
walk_tag(_Insert, _Namespace, _Id, []) ->
ok;
walk_tag(Insert, Namespace, Id, [{Tag, Text} | Rest]) ->
ok = Insert(Namespace, Id, Tag, Text),
walk_tag(Insert, Namespace, Id, Rest).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;
bin(L) when is_list(L) -> list_to_binary(L).
ensure_app_loaded(App) ->
case application:load(App) of
ok -> ok;
{error, {already_loaded, _}} -> ok
end.

View File

@ -15,9 +15,11 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_dashboard_listener). -module(emqx_dashboard_listener).
-include_lib("emqx/include/logger.hrl").
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
-include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
%% API %% API
-export([add_handler/0, remove_handler/0]). -export([add_handler/0, remove_handler/0]).
-export([pre_config_update/3, post_config_update/5]). -export([pre_config_update/3, post_config_update/5]).
@ -54,12 +56,10 @@ init([]) ->
{ok, undefined, {continue, regenerate_dispatch}}. {ok, undefined, {continue, regenerate_dispatch}}.
handle_continue(regenerate_dispatch, _State) -> handle_continue(regenerate_dispatch, _State) ->
NewState = regenerate_minirest_dispatch(), %% initialize the swagger dispatches
{noreply, NewState, hibernate}. ready = regenerate_minirest_dispatch(),
{noreply, ready, hibernate}.
handle_call(is_ready, _From, retry) ->
NewState = regenerate_minirest_dispatch(),
{reply, NewState, NewState, hibernate};
handle_call(is_ready, _From, State) -> handle_call(is_ready, _From, State) ->
{reply, State, State, hibernate}; {reply, State, State, hibernate};
handle_call(_Request, _From, State) -> handle_call(_Request, _From, State) ->
@ -68,6 +68,9 @@ handle_call(_Request, _From, State) ->
handle_cast(_Request, State) -> handle_cast(_Request, State) ->
{noreply, State, hibernate}. {noreply, State, hibernate}.
handle_info(i18n_lang_changed, _State) ->
NewState = regenerate_minirest_dispatch(),
{noreply, NewState, hibernate};
handle_info({update_listeners, OldListeners, NewListeners}, _State) -> handle_info({update_listeners, OldListeners, NewListeners}, _State) ->
ok = emqx_dashboard:stop_listeners(OldListeners), ok = emqx_dashboard:stop_listeners(OldListeners),
ok = emqx_dashboard:start_listeners(NewListeners), ok = emqx_dashboard:start_listeners(NewListeners),
@ -83,29 +86,26 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%% generate dispatch is very slow. %% generate dispatch is very slow, takes about 1s.
regenerate_minirest_dispatch() -> regenerate_minirest_dispatch() ->
try %% optvar:read waits for the var to be set
emqx_dashboard:init_i18n(), Names = emqx_dashboard:wait_for_listeners(),
lists:foreach( {Time, ok} = timer:tc(fun() -> do_regenerate_minirest_dispatch(Names) end),
fun(Listener) -> Lang = emqx:get_config([dashboard, i18n_lang]),
minirest:update_dispatch(element(1, Listener)) ?tp(info, regenerate_minirest_dispatch, #{
end, elapsed => erlang:convert_time_unit(Time, microsecond, millisecond),
emqx_dashboard:list_listeners() listeners => Names,
), i18n_lang => Lang
ready
catch
T:E:S ->
?SLOG(error, #{
msg => "regenerate_minirest_dispatch_failed",
reason => E,
type => T,
stacktrace => S
}), }),
retry ready.
after
emqx_dashboard:clear_i18n() do_regenerate_minirest_dispatch(Names) ->
end. lists:foreach(
fun(Name) ->
ok = minirest:update_dispatch(Name)
end,
Names
).
add_handler() -> add_handler() ->
Roots = emqx_dashboard_schema:roots(), Roots = emqx_dashboard_schema:roots(),
@ -117,6 +117,12 @@ remove_handler() ->
ok = emqx_config_handler:remove_handler(Roots), ok = emqx_config_handler:remove_handler(Roots),
ok. ok.
pre_config_update(_Path, {change_i18n_lang, NewLang}, RawConf) ->
%% e.g. emqx_conf:update([dashboard], {change_i18n_lang, zh}, #{}).
%% TODO: check if there is such a language (all languages are cached in emqx_dashboard_desc_cache)
Update = #{<<"i18n_lang">> => NewLang},
NewConf = emqx_utils_maps:deep_merge(RawConf, Update),
{ok, NewConf};
pre_config_update(_Path, UpdateConf0, RawConf) -> pre_config_update(_Path, UpdateConf0, RawConf) ->
UpdateConf = remove_sensitive_data(UpdateConf0), UpdateConf = remove_sensitive_data(UpdateConf0),
NewConf = emqx_utils_maps:deep_merge(RawConf, UpdateConf), NewConf = emqx_utils_maps:deep_merge(RawConf, UpdateConf),
@ -139,6 +145,8 @@ remove_sensitive_data(Conf0) ->
Conf1 Conf1
end. end.
post_config_update(_, {change_i18n_lang, _}, _NewConf, _OldConf, _AppEnvs) ->
delay_job(i18n_lang_changed);
post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) -> post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) ->
OldHttp = get_listener(http, OldConf), OldHttp = get_listener(http, OldConf),
OldHttps = get_listener(https, OldConf), OldHttps = get_listener(https, OldConf),
@ -148,7 +156,12 @@ post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) ->
{StopHttps, StartHttps} = diff_listeners(https, OldHttps, NewHttps), {StopHttps, StartHttps} = diff_listeners(https, OldHttps, NewHttps),
Stop = maps:merge(StopHttp, StopHttps), Stop = maps:merge(StopHttp, StopHttps),
Start = maps:merge(StartHttp, StartHttps), Start = maps:merge(StartHttp, StartHttps),
_ = erlang:send_after(500, ?MODULE, {update_listeners, Stop, Start}), delay_job({update_listeners, Stop, Start}).
%% in post_config_update, the config is not yet persisted to persistent_term
%% so we need to delegate the listener update to the gen_server a bit later
delay_job(Msg) ->
_ = erlang:send_after(500, ?MODULE, Msg),
ok. ok.
get_listener(Type, Conf) -> get_listener(Type, Conf) ->

View File

@ -233,6 +233,8 @@ cors(required) -> false;
cors(desc) -> ?DESC(cors); cors(desc) -> ?DESC(cors);
cors(_) -> undefined. cors(_) -> undefined.
%% TODO: change it to string type
%% It will be up to the dashboard package which languagues to support
i18n_lang(type) -> ?ENUM([en, zh]); i18n_lang(type) -> ?ENUM([en, zh]);
i18n_lang(default) -> en; i18n_lang(default) -> en;
i18n_lang('readOnly') -> true; i18n_lang('readOnly') -> true;

View File

@ -28,6 +28,8 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) -> init([]) ->
%% supervisor owns the cache table
ok = emqx_dashboard_desc_cache:init(),
{ok, {ok,
{{one_for_one, 5, 100}, [ {{one_for_one, 5, 100}, [
?CHILD(emqx_dashboard_listener, brutal_kill), ?CHILD(emqx_dashboard_listener, brutal_kill),

View File

@ -84,7 +84,8 @@
-type spec_opts() :: #{ -type spec_opts() :: #{
check_schema => boolean() | filter(), check_schema => boolean() | filter(),
translate_body => boolean(), translate_body => boolean(),
schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map()) schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map()),
i18n_lang => atom()
}. }.
-type route_path() :: string() | binary(). -type route_path() :: string() | binary().
@ -333,11 +334,11 @@ check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map
%% tags, description, summary, security, deprecated %% tags, description, summary, security, deprecated
meta_to_spec(Meta, Module, Options) -> meta_to_spec(Meta, Module, Options) ->
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module), {Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module, Options),
{RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module, Options), {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module, Options),
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options), {Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
{ {
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)), generate_method_desc(to_spec(Meta, Params, RequestBody, Responses), Options),
lists:usort(Refs1 ++ Refs2 ++ Refs3) lists:usort(Refs1 ++ Refs2 ++ Refs3)
}. }.
@ -348,13 +349,13 @@ to_spec(Meta, Params, RequestBody, Responses) ->
Spec = to_spec(Meta, Params, [], Responses), Spec = to_spec(Meta, Params, [], Responses),
maps:put('requestBody', RequestBody, Spec). maps:put('requestBody', RequestBody, Spec).
generate_method_desc(Spec = #{desc := _Desc}) -> generate_method_desc(Spec = #{desc := _Desc}, Options) ->
Spec1 = trans_description(maps:remove(desc, Spec), Spec), Spec1 = trans_description(maps:remove(desc, Spec), Spec, Options),
trans_tags(Spec1); trans_tags(Spec1);
generate_method_desc(Spec = #{description := _Desc}) -> generate_method_desc(Spec = #{description := _Desc}, Options) ->
Spec1 = trans_description(Spec, Spec), Spec1 = trans_description(Spec, Spec, Options),
trans_tags(Spec1); trans_tags(Spec1);
generate_method_desc(Spec) -> generate_method_desc(Spec, _Options) ->
trans_tags(Spec). trans_tags(Spec).
trans_tags(Spec = #{tags := Tags}) -> trans_tags(Spec = #{tags := Tags}) ->
@ -362,7 +363,7 @@ trans_tags(Spec = #{tags := Tags}) ->
trans_tags(Spec) -> trans_tags(Spec) ->
Spec. Spec.
parameters(Params, Module) -> parameters(Params, Module, Options) ->
{SpecList, AllRefs} = {SpecList, AllRefs} =
lists:foldl( lists:foldl(
fun(Param, {Acc, RefsAcc}) -> fun(Param, {Acc, RefsAcc}) ->
@ -388,7 +389,7 @@ parameters(Params, Module) ->
Type Type
), ),
Spec1 = trans_required(Spec0, Required, In), Spec1 = trans_required(Spec0, Required, In),
Spec2 = trans_description(Spec1, Type), Spec2 = trans_description(Spec1, Type, Options),
{[Spec2 | Acc], Refs ++ RefsAcc} {[Spec2 | Acc], Refs ++ RefsAcc}
end end
end, end,
@ -432,38 +433,38 @@ trans_required(Spec, true, _) -> Spec#{required => true};
trans_required(Spec, _, path) -> Spec#{required => true}; trans_required(Spec, _, path) -> Spec#{required => true};
trans_required(Spec, _, _) -> Spec. trans_required(Spec, _, _) -> Spec.
trans_desc(Init, Hocon, Func, Name) -> trans_desc(Init, Hocon, Func, Name, Options) ->
Spec0 = trans_description(Init, Hocon), Spec0 = trans_description(Init, Hocon, Options),
case Func =:= fun hocon_schema_to_spec/2 of case Func =:= fun hocon_schema_to_spec/2 of
true -> true ->
Spec0; Spec0;
false -> false ->
Spec1 = trans_label(Spec0, Hocon, Name), Spec1 = trans_label(Spec0, Hocon, Name, Options),
case Spec1 of case Spec1 of
#{description := _} -> Spec1; #{description := _} -> Spec1;
_ -> Spec1#{description => <<Name/binary, " Description">>} _ -> Spec1#{description => <<Name/binary, " Description">>}
end end
end. end.
trans_description(Spec, Hocon) -> trans_description(Spec, Hocon, Options) ->
Desc = Desc =
case desc_struct(Hocon) of case desc_struct(Hocon) of
undefined -> undefined; undefined -> undefined;
?DESC(_, _) = Struct -> get_i18n(<<"desc">>, Struct, undefined); ?DESC(_, _) = Struct -> get_i18n(<<"desc">>, Struct, undefined, Options);
Struct -> to_bin(Struct) Text -> to_bin(Text)
end, end,
case Desc of case Desc of
undefined -> undefined ->
Spec; Spec;
Desc -> Desc ->
Desc1 = binary:replace(Desc, [<<"\n">>], <<"<br/>">>, [global]), Desc1 = binary:replace(Desc, [<<"\n">>], <<"<br/>">>, [global]),
maybe_add_summary_from_label(Spec#{description => Desc1}, Hocon) maybe_add_summary_from_label(Spec#{description => Desc1}, Hocon, Options)
end. end.
maybe_add_summary_from_label(Spec, Hocon) -> maybe_add_summary_from_label(Spec, Hocon, Options) ->
Label = Label =
case desc_struct(Hocon) of case desc_struct(Hocon) of
?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, undefined); ?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, undefined, Options);
_ -> undefined _ -> undefined
end, end,
case Label of case Label of
@ -471,20 +472,30 @@ maybe_add_summary_from_label(Spec, Hocon) ->
_ -> Spec#{summary => Label} _ -> Spec#{summary => Label}
end. end.
get_i18n(Key, Struct, Default) -> get_i18n(Tag, ?DESC(Namespace, Id), Default, Options) ->
{ok, #{cache := Cache, lang := Lang}} = emqx_dashboard:get_i18n(), Lang = get_lang(Options),
Desc = hocon_schema:resolve_schema(Struct, Cache), case emqx_dashboard_desc_cache:lookup(Lang, Namespace, Id, Tag) of
emqx_utils_maps:deep_get([Key, Lang], Desc, Default). undefined ->
Default;
Text ->
Text
end.
trans_label(Spec, Hocon, Default) -> %% So far i18n_lang in options is only used at build time.
%% At runtime, it's still the global config which controls the language.
get_lang(#{i18n_lang := Lang}) -> Lang;
get_lang(_) -> emqx:get_config([dashboard, i18n_lang]).
trans_label(Spec, Hocon, Default, Options) ->
Label = Label =
case desc_struct(Hocon) of case desc_struct(Hocon) of
?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, Default); ?DESC(_, _) = Struct -> get_i18n(<<"label">>, Struct, Default, Options);
_ -> Default _ -> Default
end, end,
Spec#{label => Label}. Spec#{label => Label}.
desc_struct(Hocon) -> desc_struct(Hocon) ->
R =
case hocon_schema:field_schema(Hocon, desc) of case hocon_schema:field_schema(Hocon, desc) of
undefined -> undefined ->
case hocon_schema:field_schema(Hocon, description) of case hocon_schema:field_schema(Hocon, description) of
@ -493,7 +504,12 @@ desc_struct(Hocon) ->
end; end;
Struct -> Struct ->
Struct Struct
end. end,
ensure_bin(R).
ensure_bin(undefined) -> undefined;
ensure_bin(?DESC(_Namespace, _Id) = Desc) -> Desc;
ensure_bin(Text) -> to_bin(Text).
get_ref_desc(?R_REF(Mod, Name)) -> get_ref_desc(?R_REF(Mod, Name)) ->
case erlang:function_exported(Mod, desc, 1) of case erlang:function_exported(Mod, desc, 1) of
@ -524,7 +540,7 @@ responses(Responses, Module, Options) ->
{Spec, Refs}. {Spec, Refs}.
response(Status, ?DESC(_Mod, _Id) = Schema, {Acc, RefsAcc, Module, Options}) -> response(Status, ?DESC(_Mod, _Id) = Schema, {Acc, RefsAcc, Module, Options}) ->
Desc = trans_description(#{}, #{desc => Schema}), Desc = trans_description(#{}, #{desc => Schema}, Options),
{Acc#{integer_to_binary(Status) => Desc}, RefsAcc, Module, Options}; {Acc#{integer_to_binary(Status) => Desc}, RefsAcc, Module, Options};
response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) -> response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options}; {Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
@ -553,7 +569,7 @@ response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
Hocon = hocon_schema:field_schema(Schema, type), Hocon = hocon_schema:field_schema(Schema, type),
Examples = hocon_schema:field_schema(Schema, examples), Examples = hocon_schema:field_schema(Schema, examples),
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module), {Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
Init = trans_description(#{}, Schema), Init = trans_description(#{}, Schema, Options),
Content = content(Spec, Examples), Content = content(Spec, Examples),
{ {
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
@ -563,7 +579,7 @@ response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
}; };
false -> false ->
{Props, Refs} = parse_object(Schema, Module, Options), {Props, Refs} = parse_object(Schema, Module, Options),
Init = trans_description(#{}, Schema), Init = trans_description(#{}, Schema, Options),
Content = Init#{<<"content">> => content(Props)}, Content = Init#{<<"content">> => content(Props)},
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options} {Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
end. end.
@ -590,7 +606,7 @@ components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
%% parameters in ref only have one value, not array %% parameters in ref only have one value, not array
components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) -> components(Options, [{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) ->
Props = hocon_schema_fields(Module, Field), Props = hocon_schema_fields(Module, Field),
{[Param], SubRefs} = parameters(Props, Module), {[Param], SubRefs} = parameters(Props, Module, Options),
Namespace = namespace(Module), Namespace = namespace(Module),
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param}, NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param},
components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc). components(Options, Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
@ -869,7 +885,7 @@ parse_object_loop([{Name, Hocon} | Rest], Module, Options, Props, Required, Refs
HoconType = hocon_schema:field_schema(Hocon, type), HoconType = hocon_schema:field_schema(Hocon, type),
Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon), Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
SchemaToSpec = schema_converter(Options), SchemaToSpec = schema_converter(Options),
Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin), Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin, Options),
{Prop, Refs1} = SchemaToSpec(HoconType, Module), {Prop, Refs1} = SchemaToSpec(HoconType, Module),
NewRequiredAcc = NewRequiredAcc =
case is_required(Hocon) of case is_required(Hocon) of

View File

@ -0,0 +1,51 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_dashboard_listener_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
ok = change_i18n_lang(en),
Config.
end_per_suite(_Config) ->
ok = change_i18n_lang(en),
emqx_mgmt_api_test_util:end_suite([emqx_conf]).
t_change_i18n_lang(_Config) ->
?check_trace(
begin
ok = change_i18n_lang(zh),
{ok, _} = ?block_until(#{?snk_kind := regenerate_minirest_dispatch}, 10_000),
ok
end,
fun(ok, Trace) ->
?assertMatch([#{i18n_lang := zh}], ?of_kind(regenerate_minirest_dispatch, Trace))
end
),
ok.
change_i18n_lang(Lang) ->
{ok, _} = emqx_conf:update([dashboard], {change_i18n_lang, Lang}, #{}),
ok.

View File

@ -64,7 +64,6 @@ groups() ->
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf]), emqx_mgmt_api_test_util:init_suite([emqx_conf]),
emqx_dashboard:init_i18n(),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->

View File

@ -33,7 +33,6 @@ init_per_suite(Config) ->
mria:start(), mria:start(),
application:load(emqx_dashboard), application:load(emqx_dashboard),
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
emqx_dashboard:init_i18n(),
Config. Config.
set_special_configs(emqx_dashboard) -> set_special_configs(emqx_dashboard) ->

View File

@ -33,7 +33,6 @@ all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf]), emqx_mgmt_api_test_util:init_suite([emqx_conf]),
emqx_dashboard:init_i18n(),
Config. Config.
end_per_suite(Config) -> end_per_suite(Config) ->

View File

@ -686,7 +686,6 @@ t_jq(_) ->
%% Got timeout as expected %% Got timeout as expected
got_timeout got_timeout
end, end,
_ConfigRootKey = emqx_rule_engine_schema:namespace(),
?assertThrow( ?assertThrow(
{jq_exception, {timeout, _}}, {jq_exception, {timeout, _}},
apply_func(jq, [TOProgram, <<"-2">>]) apply_func(jq, [TOProgram, <<"-2">>])

3
build
View File

@ -117,8 +117,7 @@ make_docs() {
mkdir -p "$docdir" "$dashboard_www_static" mkdir -p "$docdir" "$dashboard_www_static"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \ erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
"I18nFile = filename:join([apps, emqx_dashboard, priv, 'i18n.conf']), \ "ok = emqx_conf:dump_schema('$docdir', $SCHEMA_MODULE), \
ok = emqx_conf:dump_schema('$docdir', $SCHEMA_MODULE, I18nFile), \
halt(0)." halt(0)."
cp "$docdir"/bridge-api-*.json "$dashboard_www_static" cp "$docdir"/bridge-api-*.json "$dashboard_www_static"
cp "$docdir"/hot-config-schema-*.json "$dashboard_www_static" cp "$docdir"/hot-config-schema-*.json "$dashboard_www_static"

View File

@ -72,7 +72,7 @@ defmodule EMQXUmbrella.MixProject do
# in conflict by emqtt and hocon # in conflict by emqtt and hocon
{:getopt, "1.0.2", override: true}, {:getopt, "1.0.2", override: true},
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.7", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.7", override: true},
{:hocon, github: "emqx/hocon", tag: "0.38.1", override: true}, {:hocon, github: "emqx/hocon", tag: "0.39.1", override: true},
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.2", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.2", override: true},
{:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:esasl, github: "emqx/esasl", tag: "0.2.0"},
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},

View File

@ -75,7 +75,7 @@
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}}
, {getopt, "1.0.2"} , {getopt, "1.0.2"}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.7"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.7"}}}
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.38.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.39.1"}}}
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}}
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}

View File

@ -479,8 +479,7 @@ etc_overlay(ReleaseType, Edition) ->
[ [
{mkdir, "etc/"}, {mkdir, "etc/"},
{copy, "{{base_dir}}/lib/emqx/etc/certs", "etc/"}, {copy, "{{base_dir}}/lib/emqx/etc/certs", "etc/"},
{copy, "_build/docgen/" ++ name(Edition) ++ "/emqx.conf.en.example", {copy, "_build/docgen/" ++ name(Edition) ++ "/emqx.conf.example", "etc/emqx.conf.example"}
"etc/emqx.conf.example"}
] ++ ] ++
lists:map( lists:map(
fun fun

View File

@ -34,7 +34,10 @@ main(_) ->
ok = file:write_file("apps/emqx_conf/etc/emqx-enterprise.conf.all", EnterpriseConf); ok = file:write_file("apps/emqx_conf/etc/emqx-enterprise.conf.all", EnterpriseConf);
false -> false ->
ok ok
end. end,
merge_desc_files_per_lang("en"),
%% TODO: remove this when we have zh translation moved to dashboard package
merge_desc_files_per_lang("zh").
is_enterprise() -> is_enterprise() ->
Profile = os:getenv("PROFILE", "emqx"), Profile = os:getenv("PROFILE", "emqx"),
@ -96,3 +99,48 @@ try_enter_child(Dir, Files, Cfgs) ->
true -> true ->
get_all_cfgs(filename:join([Dir, "src"]), Cfgs) get_all_cfgs(filename:join([Dir, "src"]), Cfgs)
end. end.
%% Desc files merge is for now done locally in emqx.git repo for all languages.
%% When zh and other languages are moved to a separate repo,
%% we will only merge the en files.
%% The file for other languages will be merged in the other repo,
%% the built as a part of the dashboard package,
%% finally got pulled at build time as a part of the dashboard package.
merge_desc_files_per_lang(Lang) ->
BaseConf = <<"">>,
Cfgs0 = get_all_desc_files(Lang),
Conf = do_merge_desc_files_per_lang(BaseConf, Cfgs0),
OutputFile = case Lang of
"en" ->
%% en desc will always be in the priv dir of emqx_dashboard
"apps/emqx_dashboard/priv/desc.en.hocon";
"zh" ->
%% so far we inject zh desc as if it's extracted from dashboard package
%% TODO: remove this when we have zh translation moved to dashboard package
"apps/emqx_dashboard/priv/www/static/desc.zh.hocon"
end,
ok = filelib:ensure_dir(OutputFile),
ok = file:write_file(OutputFile, Conf).
do_merge_desc_files_per_lang(BaseConf, Cfgs) ->
lists:foldl(
fun(CfgFile, Acc) ->
case filelib:is_regular(CfgFile) of
true ->
{ok, Bin1} = file:read_file(CfgFile),
[Acc, io_lib:nl(), Bin1];
false -> Acc
end
end, BaseConf, Cfgs).
get_all_desc_files(Lang) ->
Dir =
case Lang of
"en" ->
filename:join(["rel", "i18n"]);
"zh" ->
%% TODO: remove this when we have zh translation moved to dashboard package
filename:join(["rel", "i18n", "zh"])
end,
Files = filelib:wildcard("*.hocon", Dir),
lists:map(fun(Name) -> filename:join([Dir, Name]) end, Files).

View File

@ -1,35 +0,0 @@
#!/usr/bin/env escript
-mode(compile).
main(_) ->
main_per_lang("en"),
main_per_lang("zh").
main_per_lang(Lang) ->
BaseConf = <<"">>,
Cfgs0 = get_all_files(Lang),
Conf = merge(BaseConf, Cfgs0),
OutputFile = "apps/emqx_dashboard/priv/i18n." ++ Lang ++ ".conf",
ok = filelib:ensure_dir(OutputFile),
ok = file:write_file(OutputFile, Conf).
merge(BaseConf, Cfgs) ->
lists:foldl(
fun(CfgFile, Acc) ->
case filelib:is_regular(CfgFile) of
true ->
{ok, Bin1} = file:read_file(CfgFile),
[Acc, io_lib:nl(), Bin1];
false -> Acc
end
end, BaseConf, Cfgs).
get_all_files(Lang) ->
Dir =
case Lang of
"en" -> filename:join(["rel", "i18n"]);
"zh" -> filename:join(["rel", "i18n", "zh"])
end,
Files = filelib:wildcard("*.hocon", Dir),
lists:map(fun(Name) -> filename:join([Dir, Name]) end, Files).

View File

@ -20,5 +20,4 @@ cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
./scripts/get-dashboard.sh "$dashboard_version" ./scripts/get-dashboard.sh "$dashboard_version"
./scripts/merge-config.escript ./scripts/merge-config.escript
./scripts/merge-i18n.escript
./scripts/update-bom.sh "$PROFILE_STR" ./rel ./scripts/update-bom.sh "$PROFILE_STR" ./rel