From 93d3514676cad95b94bbb3e483d02b7ea0076bba Mon Sep 17 00:00:00 2001 From: Jordan Bracco Date: Sun, 17 Apr 2022 03:25:09 +0000 Subject: WIP --- apps/dreki_web/src/admin/dreki_web_admin_tasks.erl | 30 +++ apps/dreki_web/src/admin/dreki_web_admin_world.erl | 28 +++ apps/dreki_web/src/api/dreki_web_index.erl | 9 + apps/dreki_web/src/api/dreki_web_task.erl | 21 ++ apps/dreki_web/src/cowboy_access_log_h.erl | 238 +++++++++++++++++++++ apps/dreki_web/src/dreki_web.app.src | 3 +- apps/dreki_web/src/dreki_web_admin_tasks.erl | 30 --- apps/dreki_web/src/dreki_web_admin_world.erl | 28 --- apps/dreki_web/src/dreki_web_app.erl | 12 +- apps/dreki_web/src/dreki_web_handler.erl | 99 +++++++++ apps/dreki_web/src/dreki_web_index.erl | 9 - apps/dreki_web/src/dreki_web_task.erl | 21 -- apps/dreki_web/src/dreki_web_ui.erl | 43 ---- apps/dreki_web/src/dreki_web_ui_error.erl | 14 -- apps/dreki_web/src/dreki_web_ui_index.erl | 31 --- apps/dreki_web/src/dreki_web_ui_json_form.erl | 127 ----------- apps/dreki_web/src/dreki_web_ui_node.erl | 23 -- apps/dreki_web/src/dreki_web_ui_stores.erl | 126 ----------- apps/dreki_web/src/dreki_web_ui_task.erl | 20 -- apps/dreki_web/src/dreki_web_ui_tasks.erl | 14 -- apps/dreki_web/src/ui/dreki_web_ui.erl | 44 ++++ apps/dreki_web/src/ui/dreki_web_ui_error.erl | 14 ++ apps/dreki_web/src/ui/dreki_web_ui_index.erl | 31 +++ apps/dreki_web/src/ui/dreki_web_ui_json_form.erl | 160 ++++++++++++++ apps/dreki_web/src/ui/dreki_web_ui_node.erl | 23 ++ apps/dreki_web/src/ui/dreki_web_ui_stores.erl | 189 ++++++++++++++++ apps/dreki_web/src/ui/dreki_web_ui_task.erl | 20 ++ apps/dreki_web/src/ui/dreki_web_ui_tasks.erl | 14 ++ 28 files changed, 931 insertions(+), 490 deletions(-) create mode 100644 apps/dreki_web/src/admin/dreki_web_admin_tasks.erl create mode 100644 apps/dreki_web/src/admin/dreki_web_admin_world.erl create mode 100644 apps/dreki_web/src/api/dreki_web_index.erl create mode 100644 apps/dreki_web/src/api/dreki_web_task.erl create mode 100644 apps/dreki_web/src/cowboy_access_log_h.erl delete mode 100644 apps/dreki_web/src/dreki_web_admin_tasks.erl delete mode 100644 apps/dreki_web/src/dreki_web_admin_world.erl create mode 100644 apps/dreki_web/src/dreki_web_handler.erl delete mode 100644 apps/dreki_web/src/dreki_web_index.erl delete mode 100644 apps/dreki_web/src/dreki_web_task.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_error.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_index.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_json_form.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_node.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_stores.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_task.erl delete mode 100644 apps/dreki_web/src/dreki_web_ui_tasks.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_error.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_index.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_json_form.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_node.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_stores.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_task.erl create mode 100644 apps/dreki_web/src/ui/dreki_web_ui_tasks.erl (limited to 'apps/dreki_web/src') diff --git a/apps/dreki_web/src/admin/dreki_web_admin_tasks.erl b/apps/dreki_web/src/admin/dreki_web_admin_tasks.erl new file mode 100644 index 0000000..adc2689 --- /dev/null +++ b/apps/dreki_web/src/admin/dreki_web_admin_tasks.erl @@ -0,0 +1,30 @@ +-module(dreki_web_admin_tasks). +-behaviour(cowboy_rest). +-export([init/2]). +-export([allowed_methods/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +allowed_methods(Req, State) -> + {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. + +content_types_accepted(Req, State) -> + dreki_web:content_types_accepted(Req, State). + +content_types_provided(Req, State) -> + dreki_web:content_types_provided(Req, State). + +list(Req, State, Format) -> + {ok, Db} = dreki_tasks:open(), + {ok, Tasks} = dreki_tasks:all(Db), + MTasks = lists:foldl(fun (T, Acc) -> [dreki_task:to_map(T) | Acc] end, [], Tasks), + dreki_web:reply(Req, 200, #{error => false, tasks => MTasks}, [], Format). + +to_json(Req, State) -> + list(Req, State, json). + +to_yaml(Req, State) -> + list(Req, State, yaml). diff --git a/apps/dreki_web/src/admin/dreki_web_admin_world.erl b/apps/dreki_web/src/admin/dreki_web_admin_world.erl new file mode 100644 index 0000000..1ceaaee --- /dev/null +++ b/apps/dreki_web/src/admin/dreki_web_admin_world.erl @@ -0,0 +1,28 @@ +-module(dreki_web_admin_world). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req, index) -> + Json = #{<<"data">> => #{<<"service">> => <<"dreki">>}}, + {ok, dreki_web:reply_json(Req, 200, Json), undefined}; + +init(Req, graph) -> + World = dreki_world_dns:as_map(), + Json = #{<<"data">> => World}, + {ok, dreki_web:reply_json(Req, 200, Json), undefined}; + +init(Req, {graph, dot}) -> + World = dreki_world_dns:as_map(), + Vertices = [dot_format_vertex(N) || N <- maps:get(vertices, World)], + {ok, Dot} = world_graph_dot_dtl:render([{vertices, Vertices}, {edges, maps:get(edges, World)}]), + cowboy_req:reply(200, #{<<"content-type">> => <<"text/vnd.graphviz">>}, Dot, Req); + +init(Req, _) -> + dreki_web_error:init(Req, #{code => 404, status => "Not found"}). + +dot_format_vertex(V = #{type := node}) -> + V#{shape => <<"box">>, color => <<"#c2410c">>, class => <<"dreki-world-graph-node">>}; +dot_format_vertex(V = #{type := root}) -> + V#{shape => <<"polygon">>, color => <<"#a16207">>, class => <<"dreki-world-graph-root">>}; +dot_format_vertex(V = #{type := region}) -> + V#{shape => <<"egg">>, color => <<"#4d7c0f">>, class => <<"dreki-world-graph-region">>}. diff --git a/apps/dreki_web/src/api/dreki_web_index.erl b/apps/dreki_web/src/api/dreki_web_index.erl new file mode 100644 index 0000000..2ed8a38 --- /dev/null +++ b/apps/dreki_web/src/api/dreki_web_index.erl @@ -0,0 +1,9 @@ +-module(dreki_web_index). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req = #{method := <<"GET">>}, _) -> + Json = #{<<"error">> => false, <<"service">> => <<"dreki">>}, + {ok, dreki_web:reply_json(Req, 200, Json), undefined}; +init(Req, _) -> + dreki_web_error:init(Req, #{code => 400, status => "Bad request"}). diff --git a/apps/dreki_web/src/api/dreki_web_task.erl b/apps/dreki_web/src/api/dreki_web_task.erl new file mode 100644 index 0000000..fdcf9cd --- /dev/null +++ b/apps/dreki_web/src/api/dreki_web_task.erl @@ -0,0 +1,21 @@ +-module(dreki_web_task). +-behaviour(cowboy_handler). +-behaviour(cowboy_rest). +-export([init/2]). +-export([allowed_methods/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +allowed_methods(Req, State) -> + {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. + +content_types_accepted(Req, State) -> + dreki_web:content_types_accepted(Req, State). + +content_types_provided(Req, State) -> + dreki_web:content_types_provided(Req, State). + + diff --git a/apps/dreki_web/src/cowboy_access_log_h.erl b/apps/dreki_web/src/cowboy_access_log_h.erl new file mode 100644 index 0000000..433d706 --- /dev/null +++ b/apps/dreki_web/src/cowboy_access_log_h.erl @@ -0,0 +1,238 @@ +-module(cowboy_access_log_h). +-behaviour(cowboy_stream). + +-dialyzer(no_undefined_callbacks). + +-type extra_info_fun() :: fun((cowboy_req:req()) -> #{atom() => term()}). +-export_type([extra_info_fun/0]). + +%% API exports + +-export([set_extra_info_fun/2]). + +%% callback exports + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-type state() :: #{ + next := any(), + req := cowboy_req:req(), + meta := #{started_at => genlib_time:ts()}, + ext_fun := extra_info_fun() +}. + +%% API + +-spec set_extra_info_fun(extra_info_fun(), cowboy:opts()) + -> cowboy:opts(). +set_extra_info_fun(Fun, Opts) when is_function(Fun, 1) -> + Opts#{extra_info_fun => Fun}. + +%% callbacks + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), state()}. +init(StreamID, Req, Opts) -> + State = make_state(Req, Opts), + {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts), + {Commands0, State#{next => Next}}. + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::state(). +data(StreamID, IsFin, Data, #{next := Next0} = State) -> + {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + {Commands0, State#{next => Next}}. + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::state(). +info(StreamID, {IsResponse, Code, Headers, _} = Info, #{req := Req, next := Next0} = State) when + IsResponse == response; + IsResponse == error_response +-> + _ = log_access_safe(Code, Headers, State, get_request_body_length(Req)), + {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands0, State#{next => Next}}; +info(StreamID, Info, #{next := Next0} = State) -> + {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands0, State#{next => Next}}. + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), state()) -> any(). +terminate(StreamID, Reason, #{next := Next}) -> + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). + +%% NOTE: in early_error cowboy uses PartialReq, a cowboy_req:req() - like structure +%% for more info see https://ninenines.eu/docs/en/cowboy/2.7/manual/cowboy_stream/#_callbacks + +early_error(StreamID, Reason, PartialReq, {_, Code, Headers, _} = Resp, Opts) -> + State = make_state(PartialReq, Opts), + _ = log_access_safe(Code, Headers, State, undefined), + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, State). + +%% private functions + +log_access_safe(Code, Headers, #{req := Req} = State, ReqBodyLength) -> + try + logger:log(info, prepare_meta(Code, Headers, State, ReqBodyLength)), + Req + catch + Class:Reason:Stacktrace -> + Stack = genlib_format:format_stacktrace(Stacktrace, [newlines]), + _ = logger:error( + "Log access failed for: [~p, ~p, ~p]~nwith: ~p:~p~nstacktrace: ~ts", + [Code, Headers, Req, Class, Reason, Stack] + ), + Req + end. + +get_process_meta() -> + case logger:get_process_metadata() of + undefined -> + #{}; + Meta -> + Meta + end. + +% domain field specifies the functional area that send log event +% as we want to save logs from this app in a separate file, +% we can easily filter logs by their domain using OTP filter functions. +prepare_meta(Code, Headers, #{req := Req, meta:= Meta0, ext_fun := F}, ReqBodyLength) -> + AccessMeta = genlib_map:compact(#{ + domain => [cowboy_access_log], + status => Code, + remote_addr => get_remote_addr(Req), + peer_addr => get_peer_addr(Req), + request_method => cowboy_req:method(Req), + request_path => cowboy_req:path(Req), + request_length => ReqBodyLength, + response_length => get_response_len(Headers), + request_time => get_request_duration(Meta0), + trace_id => maps:get(<<"trace-id">>, Req, <<"no-trace">>), + 'http_x-request-id' => cowboy_req:header(<<"x-request-id">>, Req, undefined) + }), + AccessMeta1 = maps:merge(get_process_meta(), AccessMeta), + maps:merge(F(Req), AccessMeta1). + +get_request_body_length(Req) -> + case cowboy_req:has_body(Req) of + false -> undefined; + true -> cowboy_req:body_length(Req) + end. + +get_peer_addr(Req) -> + {IP, _Port} = cowboy_req:peer(Req), + genlib:to_binary(inet:ntoa(IP)). + +get_remote_addr(Req) -> + case determine_remote_addr(Req) of + {ok, RemoteAddr} -> + genlib:to_binary(inet:ntoa(RemoteAddr)); + _ -> + undefined + end. + +determine_remote_addr(Req) -> + Peer = cowboy_req:peer(Req), + Value = cowboy_req:header(<<"x-forwarded-for">>, Req), + determine_remote_addr_from_header(Value, Peer). + +determine_remote_addr_from_header(undefined, {IP, _Port}) -> + % undefined, assuming no proxies were involved + {ok, IP}; +determine_remote_addr_from_header(Value, _Peer) when is_binary(Value) -> + ClientPeer = string:strip(binary_to_list(Value)), + case string:tokens(ClientPeer, ", ") of + [ClientIP | _Proxies] -> + inet:parse_strict_address(ClientIP); + _ -> + {error, malformed} + end. + +get_request_duration(Meta) -> + case maps:get(started_at, Meta, undefined) of + undefined -> + undefined; + StartTime -> + (genlib_time:ticks() - StartTime) / 1000000 + end. + +get_response_len(Headers) -> + case maps:get(<<"content-length">>, Headers, undefined) of + undefined -> + undefined; + Len -> + genlib:to_int(Len) + end. + +make_state(Req, Opts) -> + ExtFun = make_ext_fun(Opts), + set_meta(#{req => Req, ext_fun => ExtFun}). + +set_meta(State) -> + State#{meta => #{started_at => genlib_time:ticks()}}. + +make_ext_fun(Opts) -> + maps:get(extra_info_fun, Opts, fun(_Req) -> #{} end). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec filter_meta_test() -> _. +filter_meta_test() -> + Req = #{ + pid => self(), + peer => {{42, 42, 42, 42}, 4242}, + method => <<"GET">>, + path => <<>>, + qs => <<>>, + version => 'HTTP/1.1', + headers => #{}, + host => <<>>, + port => undefined, + has_body => true + }, + State = make_state(Req, #{}), + #{ + request_method := <<"GET">>, + request_path := <<>>, + request_time := _, + response_length := 33, + request_length := 100, + peer_addr := <<"42.42.42.42">>, + status := 200 + } = prepare_meta(200, #{<<"content-length">> => <<"33">>}, State, 100). + +-spec filter_meta_for_error_test() -> _. +filter_meta_for_error_test() -> + Req = #{ + pid => self(), + peer => {{42, 42, 42, 42}, 4242}, + method => <<"GET">>, + path => <<>>, + qs => <<>>, + version => 'HTTP/1.1', + headers => #{}, + host => <<>>, + port => undefined, + has_body => true + }, + State = make_state(Req, #{}), + #{ + peer_addr := <<"42.42.42.42">>, + remote_addr := <<"42.42.42.42">>, + request_method := <<"GET">>, + request_path := <<>>, + request_time := _, + status := 400 + } = prepare_meta(400, #{}, State, undefined). + +-endif. diff --git a/apps/dreki_web/src/dreki_web.app.src b/apps/dreki_web/src/dreki_web.app.src index cf731fc..4122e3b 100644 --- a/apps/dreki_web/src/dreki_web.app.src +++ b/apps/dreki_web/src/dreki_web.app.src @@ -10,7 +10,8 @@ cowboy, trails, cowboy_telemetry, - opentelemetry_cowboy + opentelemetry_cowboy, + prometheus_cowboy ]}, {env,[]}, {modules, []}, diff --git a/apps/dreki_web/src/dreki_web_admin_tasks.erl b/apps/dreki_web/src/dreki_web_admin_tasks.erl deleted file mode 100644 index adc2689..0000000 --- a/apps/dreki_web/src/dreki_web_admin_tasks.erl +++ /dev/null @@ -1,30 +0,0 @@ --module(dreki_web_admin_tasks). --behaviour(cowboy_rest). --export([init/2]). --export([allowed_methods/2]). --export([content_types_accepted/2]). --export([content_types_provided/2]). - -init(Req, State) -> - {cowboy_rest, Req, State}. - -allowed_methods(Req, State) -> - {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. - -content_types_accepted(Req, State) -> - dreki_web:content_types_accepted(Req, State). - -content_types_provided(Req, State) -> - dreki_web:content_types_provided(Req, State). - -list(Req, State, Format) -> - {ok, Db} = dreki_tasks:open(), - {ok, Tasks} = dreki_tasks:all(Db), - MTasks = lists:foldl(fun (T, Acc) -> [dreki_task:to_map(T) | Acc] end, [], Tasks), - dreki_web:reply(Req, 200, #{error => false, tasks => MTasks}, [], Format). - -to_json(Req, State) -> - list(Req, State, json). - -to_yaml(Req, State) -> - list(Req, State, yaml). diff --git a/apps/dreki_web/src/dreki_web_admin_world.erl b/apps/dreki_web/src/dreki_web_admin_world.erl deleted file mode 100644 index 1ceaaee..0000000 --- a/apps/dreki_web/src/dreki_web_admin_world.erl +++ /dev/null @@ -1,28 +0,0 @@ --module(dreki_web_admin_world). --behaviour(cowboy_handler). --export([init/2]). - -init(Req, index) -> - Json = #{<<"data">> => #{<<"service">> => <<"dreki">>}}, - {ok, dreki_web:reply_json(Req, 200, Json), undefined}; - -init(Req, graph) -> - World = dreki_world_dns:as_map(), - Json = #{<<"data">> => World}, - {ok, dreki_web:reply_json(Req, 200, Json), undefined}; - -init(Req, {graph, dot}) -> - World = dreki_world_dns:as_map(), - Vertices = [dot_format_vertex(N) || N <- maps:get(vertices, World)], - {ok, Dot} = world_graph_dot_dtl:render([{vertices, Vertices}, {edges, maps:get(edges, World)}]), - cowboy_req:reply(200, #{<<"content-type">> => <<"text/vnd.graphviz">>}, Dot, Req); - -init(Req, _) -> - dreki_web_error:init(Req, #{code => 404, status => "Not found"}). - -dot_format_vertex(V = #{type := node}) -> - V#{shape => <<"box">>, color => <<"#c2410c">>, class => <<"dreki-world-graph-node">>}; -dot_format_vertex(V = #{type := root}) -> - V#{shape => <<"polygon">>, color => <<"#a16207">>, class => <<"dreki-world-graph-root">>}; -dot_format_vertex(V = #{type := region}) -> - V#{shape => <<"egg">>, color => <<"#4d7c0f">>, class => <<"dreki-world-graph-region">>}. diff --git a/apps/dreki_web/src/dreki_web_app.erl b/apps/dreki_web/src/dreki_web_app.erl index 5b6454e..5b3c1a0 100644 --- a/apps/dreki_web/src/dreki_web_app.erl +++ b/apps/dreki_web/src/dreki_web_app.erl @@ -13,8 +13,12 @@ start(_StartType, _StartArgs) -> Config = application:get_all_env(dreki_web), Transport = proplists:get_value(transport, Config), CowboyEnv = #{ - middlewares => [dreki_web_auth, cowboy_router, cowboy_handler], - stream_handlers => [cowboy_telemetry_h, cowboy_stream_h], + middlewares => [dreki_web_auth, cowboy_router, dreki_web_handler], + stream_handlers => [cowboy_telemetry_h, + cowboy_access_log_h, + cowboy_metrics_h, + cowboy_stream_h], + metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1, env => #{ dispatch => routes() } @@ -35,7 +39,8 @@ routes() -> Trails = [ {"/", dreki_web_index, undefined}, {"/static/[...]", cowboy_static, - {priv_dir, dreki_web, "static", [{mimetypes, dreki_web, detect_web_mimetype}]}}, + {priv_dir, dreki_web, "static", [{mimetypes, dreki_web, detect_web_mimetype}]}}, + {"/metrics/[:registry]", prometheus_cowboy2_handler, []}, %% API {"/api/tasks/:id", dreki_web_task, undefined}, @@ -59,6 +64,7 @@ routes() -> {"/admin/:location/:namespace/:directory", dreki_web_ui_stores, undefined}, {"/admin/:location/:namespace/:directory/_/:action", dreki_web_ui_stores, action}, {"/admin/:location/:namespace/:directory/:id", dreki_web_ui_stores, undefined}, + {"/admin/:location/:namespace/:directory/:id/_/:action", dreki_web_ui_stores, action}, {"/admin/[...]", dreki_web_ui_error, #{code => 404, status => <<"Not found">>}}, diff --git a/apps/dreki_web/src/dreki_web_handler.erl b/apps/dreki_web/src/dreki_web_handler.erl new file mode 100644 index 0000000..a30fd1d --- /dev/null +++ b/apps/dreki_web/src/dreki_web_handler.erl @@ -0,0 +1,99 @@ +%% Copyright (c) 2011-2017, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Handler middleware. +%% +%% Execute the handler given by the handler and handler_opts +%% environment values. The result of this execution is added to the +%% environment under the result value. +-module(dreki_web_handler). +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-behaviour(cowboy_middleware). + +-export([execute/2]). +-export([terminate/4]). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback terminate(any(), map(), any()) -> ok. +-optional_callbacks([terminate/3]). + +-spec execute(Req, Env) -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req, Env=#{handler := Handler}) -> + Headers = maps:get(headers, Req), + otel_propagator_text_map:extract(maps:to_list(Headers)), + HandlerB = atom_to_binary(Handler), + Method = maps:get(method, Req), + Attributes = [ + {'http.host', maps:get(host, Req)}, + {'http.host.port', maps:get(port, Req)}, + {'http.method', Method}, + {'http.scheme', maps:get(scheme, Req)}, + {'http.target', maps:get(path, Req)}, + {'http.user_agent', maps:get(<<"user-agent">>, Headers, <<"">>)} + ], + SpanName = iolist_to_binary([<<"HTTP ">>, Method, <<" ">>, HandlerB]), + logger:debug("SpanName: ~p", [SpanName]), + ?with_span(SpanName, #{}, fun(Ctx) -> execute_(Req, Env, Ctx) end). + +execute_(Req0, Env=#{handler := Handler, handler_opts := HandlerOpts}, Ctx) -> + TraceId = otel_span:hex_trace_id(otel_tracer:current_span_ctx()), + Req = Req0#{<<"trace-id">> => TraceId}, + try Handler:init(Req, HandlerOpts) of + {ok, Req2, State} -> + Headers0 = maps:get(headers, Req2), + Headers = otel_propagator_text_map:inject(Headers0, + fun(Headers, Key, Value) -> + maps:put(Key, Value, Headers) + end), + Req3 = maps:put(headers, Headers, Req2), + Result = terminate(normal, Req3, State, Handler), + {ok, Req3, Env#{result => Result, trace_id => TraceId}}; + {Mod, Req2, State} -> + ?add_event(<<"HTTP_UPGRADE">>, #{module => Mod}), + Mod:upgrade(Req2, Env, Handler, State); + {Mod, Req2, State, Opts} -> + ?add_event(<<"HTTP_UPGRADE">>, #{module => Mod}), + Mod:upgrade(Req2, Env, Handler, State, Opts) + catch Class:Reason:Stacktrace -> + ?set_status(error, iolist_to_binary([atom_to_binary(Class)])), + render_error(Class, Reason, Stacktrace, Handler, Req, Env, Ctx) + end. + +render_error(Class, Reason, Stacktrace, Handler, Req, Env, Ctx) -> + ReasonS = lists:flatten(io_lib:format("~p", [Reason])), + Assigns = [ + {dreki_node, node()}, + {site_title, "Dreki"}, + {class, atom_to_binary(Class)}, + {reason, ReasonS}, + {stacktrace, [lists:flatten(io_lib:format("~p", [Line])) || Line <- Stacktrace]}, + {trace_id, maps:get(<<"trace-id">>, Req, <<"no-trace">>)} + ], + logger:error(#{app => dreki_web, handler => Handler, error => {Class, Reason, Stacktrace}}), + {ok, Html} = crash_dtl:render(Assigns), + cowboy_req:reply(500, #{<<"content-type">> => <<"text/html">>}, Html, Req). + +-spec terminate(any(), Req | undefined, any(), module()) -> ok when Req::cowboy_req:req(). +terminate(Reason, Req, State, Handler) -> + case erlang:function_exported(Handler, terminate, 3) of + true -> + Handler:terminate(Reason, Req, State); + false -> + ok + end. diff --git a/apps/dreki_web/src/dreki_web_index.erl b/apps/dreki_web/src/dreki_web_index.erl deleted file mode 100644 index 2ed8a38..0000000 --- a/apps/dreki_web/src/dreki_web_index.erl +++ /dev/null @@ -1,9 +0,0 @@ --module(dreki_web_index). --behaviour(cowboy_handler). --export([init/2]). - -init(Req = #{method := <<"GET">>}, _) -> - Json = #{<<"error">> => false, <<"service">> => <<"dreki">>}, - {ok, dreki_web:reply_json(Req, 200, Json), undefined}; -init(Req, _) -> - dreki_web_error:init(Req, #{code => 400, status => "Bad request"}). diff --git a/apps/dreki_web/src/dreki_web_task.erl b/apps/dreki_web/src/dreki_web_task.erl deleted file mode 100644 index fdcf9cd..0000000 --- a/apps/dreki_web/src/dreki_web_task.erl +++ /dev/null @@ -1,21 +0,0 @@ --module(dreki_web_task). --behaviour(cowboy_handler). --behaviour(cowboy_rest). --export([init/2]). --export([allowed_methods/2]). --export([content_types_accepted/2]). --export([content_types_provided/2]). - -init(Req, State) -> - {cowboy_rest, Req, State}. - -allowed_methods(Req, State) -> - {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. - -content_types_accepted(Req, State) -> - dreki_web:content_types_accepted(Req, State). - -content_types_provided(Req, State) -> - dreki_web:content_types_provided(Req, State). - - diff --git a/apps/dreki_web/src/dreki_web_ui.erl b/apps/dreki_web/src/dreki_web_ui.erl deleted file mode 100644 index d60cc6e..0000000 --- a/apps/dreki_web/src/dreki_web_ui.erl +++ /dev/null @@ -1,43 +0,0 @@ --module(dreki_web_ui). --export([render/3, reply_html/3, reply_html/4]). - -render(Req, InnerModule, Assigns0) -> - Assigns = assigns(Req, Assigns0), - {ok, InnerHtml} = InnerModule:render(Assigns), - render_layout(Req, InnerHtml, Assigns). - -reply_html(Req, Code, Html) -> - reply_html(Req, Code, Html, #{}). - -reply_html(Req, Code, Html, Headers0) -> - Headers = maps:put(<<"content-type">>, <<"text/html">>, Headers0), - cowboy_req:reply(Code, Headers, Html, Req). - -render_layout(Req, InnerHtml, Assigns) -> - {ok, Html} = layout_dtl:render([{"inner", InnerHtml} | Assigns]), - Html. - -assigns(Req, Assigns0) -> - Assigns = clean_assigns(Assigns0), - [{"site_title", "Dreki"}, - {"identity_id", maps:get(identity_id, Req)}, - {"identity", maps:get(identity, Req)}, - {"identity_name", dreki_web:identity_name(Req)}, - {"dreki_node", node()}, - {"dreki_world", dreki_world:to_map()} - | Assigns]. - -content_types_accepted(Req, State) -> - {[ - {{ <<"multipart">>, <<"form-data">>, '*'}, from_form} - ], Req, State}. - -content_types_provided(Req, State) -> - {[{{ <<"text">>, <<"html">>, '*'}, to_html}], Req, State}. - -clean_assigns(Assigns) when is_list(Assigns) -> - Assigns; -clean_assigns(Map) when is_map(Map) -> - maps:fold(fun (Key, Value, Acc) -> [{Key, clean_assigns(Value)} | Acc] end, [], Map); -clean_assigns(Other) -> - Other. diff --git a/apps/dreki_web/src/dreki_web_ui_error.erl b/apps/dreki_web/src/dreki_web_ui_error.erl deleted file mode 100644 index ccda150..0000000 --- a/apps/dreki_web/src/dreki_web_ui_error.erl +++ /dev/null @@ -1,14 +0,0 @@ --module(dreki_web_ui_error). --behaviour(cowboy_handler). --export([init/2]). - -init(Req, not_found) -> - reply(Req, 404, <<"Not Found">>, undefined); -init(Req, State = #{code := Code, status := Status}) -> - reply(Req, Code, Status, maps:get(message, State, undefined)). - -reply(Req0, Code, Status, Msg) -> - Assigns = [{"message", Msg}, {"status", Status}], - Html = dreki_web_ui:render(Req0, error_dtl, Assigns), - Req = dreki_web_ui:reply_html(Req0, Code, Html), - {ok, Req, undefined}. diff --git a/apps/dreki_web/src/dreki_web_ui_index.erl b/apps/dreki_web/src/dreki_web_ui_index.erl deleted file mode 100644 index 9f4684e..0000000 --- a/apps/dreki_web/src/dreki_web_ui_index.erl +++ /dev/null @@ -1,31 +0,0 @@ --module(dreki_web_ui_index). --behaviour(cowboy_handler). --export([init/2]). - -init(Req = #{method := <<"GET">>}, State) -> - PrettyWorld = jsone:encode(dreki_world:to_map(), [canonical_form, {space, 1}, {indent, 4}]), - LocalTasksStores = dreki_tasks:local_stores(), - {ok, Peers} = partisan_peer_service:members(), - NavTree = nav_tree(dreki_world_dns:as_map()), - Html = dreki_web_ui:render(Req, index_dtl, [ - {"page_title", "Admin UI"}, - {"dreki_world_pretty_json", PrettyWorld}, - {"dreki_local_tasks_stores", LocalTasksStores}, - {"dreki_peers", Peers}, - {"nav_tree", NavTree} - ]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; -init(Req, _) -> - dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). - -nav_tree(#{vertices := Vertices}) -> - nav_tree(Vertices, []). - -nav_tree([Region = #{type := region, name := Name, data := #{display_name := Display}} | Rest], Acc) -> - nav_tree(Rest, [Region#{href => <<"/admin/regions/", Name/binary>>, title => Display} | Acc]); -nav_tree([Node = #{type := node, name := Name, data := #{display_name := Display}} | Rest], Acc) -> - nav_tree(Rest, [Node#{href => <<"/admin/nodes/", Name/binary>>, title => Display} | Acc]); -nav_tree([#{type := Root} | Rest], Acc) -> - nav_tree(Rest, Acc); -nav_tree([], Acc) -> - Acc. diff --git a/apps/dreki_web/src/dreki_web_ui_json_form.erl b/apps/dreki_web/src/dreki_web_ui_json_form.erl deleted file mode 100644 index 49f69f8..0000000 --- a/apps/dreki_web/src/dreki_web_ui_json_form.erl +++ /dev/null @@ -1,127 +0,0 @@ --module(dreki_web_ui_json_form). --export([render_html/2]). --export([render/2]). --export([to_html/1]). - --type dreki_form() :: #{ - input => binary(), - label => binary() -}. - -render_html(Schema, Opts) -> - Abstract = render(Schema, Opts), - to_html(Abstract). - -to_html(Atom) when is_atom(Atom) -> - as_binary(Atom); -to_html(Binary) when is_binary(Binary) -> - Binary; -to_html(Abstract) -> - to_html(Abstract, []). - -to_html([{Node, Attrs} | Rest], Acc) -> - BNode = as_binary(Node), - AttrsS = attrs_to_html(Attrs), - to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>] | Acc]); -to_html([{Node, Attrs, Content} | Rest], Acc) -> - BNode = as_binary(Node), - AttrsS = attrs_to_html(Attrs), - logger:debug("Node ~p Attrs ~p Content ~p", [Node, Attrs, Content]), - to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>, to_html(Content), <<">, BNode, <<">">>] | Acc]); -to_html([], Acc) -> - lists:reverse(Acc). - -attrs_to_html(Attrs) -> attrs_to_html(Attrs, []). -attrs_to_html([{Attr, undefined} | Rest], Acc) -> - attrs_to_html(Rest, Acc); -attrs_to_html([{Attr, false} | Rest], Acc) -> - attrs_to_html(Rest, Acc); -attrs_to_html([{Attr, true} | Rest], Acc) -> - attrs_to_html(Rest, [[as_binary(Attr), <<" ">>] | Acc]); -attrs_to_html([{Attr, Num} | Rest], Acc) when is_integer(Num) -> - attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, Num, <<" ">>] | Acc]); -attrs_to_html([{Attr, Value} | Rest], Acc) -> - attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, <<"\"">>, as_binary(Value), <<"\"">>, <<" ">>] | Acc]); -attrs_to_html([], Acc) -> - Acc. - -render(Schema, Opts) -> - lists:reverse(maps:fold(fun (Key, Value, Acc) -> - case render_property(Key, Value, Schema, Schema, Opts) of - {ok, Html} -> [Html | Acc]; - ignore -> Acc - end - end, [], maps:get(properties, Schema))). - -render_property(Field, Config = #{enum := Enum}, Parent, Schema, Opts) -> - FormAttrs = maps:get(<<"dreki:form">>, Config, #{}), - Required = lists:member(Field, maps:get(required, Parent, [])), - Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)), - InputOpts = #{required => Required, label => Label, values => Enum}, - {ok, render_input(select, Field, undefined, InputOpts, Opts)}; - -render_property(Field, Config = #{type := Type}, Parent, Schema, Opts) -> - FormAttrs = maps:get(<<"dreki:form">>, Config, #{}), - Input = maps:get(input, FormAttrs, input), - InputType = maps:get(input_type, FormAttrs, input_type(Type)), - Required = lists:member(Field, maps:get(required, Parent, [])), - Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)), - InputOpts = #{required => Required, label => Label, input_type => InputType}, - {ok, render_input(Input, Field, Type, InputOpts, Opts)}; - -render_property(Field, #{'$ref' := Ref}, Parent, Schema = #{'$defs' := Defs}, Opts) -> - case maps:get(Ref, Defs, undefined) of - undefined -> logger:error("didn't get ref ~p", [Ref]); - ExpandedRef -> - logger:debug("Skipping ref for now: ~p ~p", [Ref, ExpandedRef]) - end, - ignore. - -base_attributes(Field, IOpts, FOpts) -> - Name = field_name(Field, FOpts), - {Name, [ - {id, field_id(Field, FOpts)}, - {name, Name}, - {required, maps:get(required, IOpts, false)} - ]}. - -render_input(select, Field, _, IOpts, FOpts) -> - {Name, Attributes} = base_attributes(Field, IOpts, FOpts), - OptionsHtml = lists:map(fun (Opt) -> {option, [], Opt} end, maps:get(values, IOpts, [])), - {'div', [{class, <<"json-field">>}], [ - {label, [{for, Name}], maps:get(label, IOpts, Field)}, - {select, Attributes, OptionsHtml} - ]}; - -render_input(input, Field, Type, IOpts, FOpts) -> - {Name, Attributes0} = base_attributes(Field, IOpts, FOpts), - Attributes = [ - {value, maps:get(value, IOpts, undefined)}, - {placeholder, maps:get(placeholder, IOpts, undefined)}, - {readonly, maps:get(readonly, IOpts, false)}, - {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)}, - {type, maps:get(input_type, IOpts)}, - {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">>} - | Attributes0], - HtmlNode = {'div', [{class, <<"json-field">>}], [ - {label, [{for, Name}, {class, <<"block text-sm font-medium text-gray-700">>}], maps:get(label, IOpts, Field)}, - {'div', [{class, <<"mt-1">>}], [{input, Attributes}]} - ]}, - HtmlNode. - -as_binary(Atom) when is_atom(Atom) -> - atom_to_binary(Atom); -as_binary(B) when is_binary(B) -> - B. - -input_type(_) -> <<"text">>. - -field_name(Field, Opts) -> - FB = as_binary(Field), - BaseName = maps:get(name, Opts, <<"form">>), - <<"[", BaseName/binary, "]", FB/binary>>. - -field_id(Field, Opts) -> - FB = as_binary(Field), - BaseId = maps:get(id, Opts, <<"form">>), - <>. diff --git a/apps/dreki_web/src/dreki_web_ui_node.erl b/apps/dreki_web/src/dreki_web_ui_node.erl deleted file mode 100644 index fdb7c77..0000000 --- a/apps/dreki_web/src/dreki_web_ui_node.erl +++ /dev/null @@ -1,23 +0,0 @@ --module(dreki_web_ui_node). --behaviour(cowboy_handler). --behaviour(cowboy_rest). --export([init/2]). --export([allowed_methods/2]). --export([content_types_accepted/2]). --export([content_types_provided/2]). - -init(Req, State) -> - {cowboy_rest, Req, State}. - -allowed_methods(Req, State) -> - {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. - -content_types_accepted(Req, State) -> - dreki_web:content_types_accepted(Req, State). - -content_types_provided(Req, State) -> - dreki_web:content_types_provided(Req, State). - -to_html(Req, State) -> - Html = dreki_web_ui:render(Req, node, []), - {Html, Req, State}. diff --git a/apps/dreki_web/src/dreki_web_ui_stores.erl b/apps/dreki_web/src/dreki_web_ui_stores.erl deleted file mode 100644 index d39e571..0000000 --- a/apps/dreki_web/src/dreki_web_ui_stores.erl +++ /dev/null @@ -1,126 +0,0 @@ --module(dreki_web_ui_stores). --behaviour(cowboy_handler). --export([init/2]). - - -init(Req, action) -> - with_location(Req#{action => cowboy_req:binding(action, Req)}, cowboy_req:binding(location, Req, undefined)); -init(Req, Action) -> - with_location(Req#{action => Action}, cowboy_req:binding(location, Req, undefined)). - -with_location(Req, undefined) -> - with_namespace(Req#{urn => dreki_world:root_path()}); -with_location(Req, <<"-">>) -> - with_namespace(Req#{urn => dreki_world:root_path()}); -with_location(Req0, Location) -> - Root = dreki_world:root_path(), - {ok, XUrn} = dreki_urn:expand(<>), - Req = Req0#{urn => maps:get(urn, XUrn)}, - with_namespace(Req). - -with_namespace(Req) -> - with_namespace(Req, cowboy_req:binding(namespace, Req)). -with_namespace(Req, undefined) -> - request(Req); -with_namespace(Req = #{urn := Urn0}, Namespace) -> - Urn = <>, - with_directory(Req#{urn => Urn}). - -with_directory(Req) -> - with_directory(Req, cowboy_req:binding(directory, Req)). -with_directory(Req, undefined) -> - request(Req); -with_directory(Req = #{urn := Urn0}, Directory) -> - Urn = <>, - with_id(Req#{urn => Urn}). - -with_id(Req) -> - with_id(Req, cowboy_req:binding(id, Req)). -with_id(Req, undefined) -> - request(Req); -with_id(Req = #{urn := Urn0}, Id) -> - Urn = <>, - request(Req#{urn => Urn}). - -request(Req = #{action := Action, method := Method, urn := Urn}) -> - logger:debug("Faked Urn ~p", [Urn]), - case dreki_urn:expand(Urn) of - {ok, XUrn} -> request(Method, Action, XUrn, Req); - {error, EMap=#{}} -> dreki_web_error:init(Req, EMap); - {error, _Error} -> dreki_web_error:init(Req, #{code => 404, status => "Not Found"}) - end. - -%% Stores list -request(<<"GET">>, undefined, #{resource := #{namespace := NS}}, Req) -> - {ok, Stores0} = dreki_store:stores(NS), - Stores = lists:map(fun (Store) -> - S = dreki_store:store_as_map(Store), - S#{href => urn_to_path(maps:get(urn, S))} - end, Stores0), - Html = dreki_web_ui:render(Req, namespace_dtl, [{namespace, NS}, {stores, Stores}]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; - -%% List -request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) -> - {ok, Result} = dreki_store:list(Urn), - Results = lists:map(fun(Result) -> - Result#{href => urn_to_path(maps:get('@id', Result))} - end, maps:get(data, Result)), - Html = dreki_web_ui:render(Req, store_list_dtl, [ - {location, Loc}, {namespace, NS}, {directory, Dir}, {results, Results}, - {new, href(Req, <<"_/new">>)} - ]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; - -%% New -request(<<"GET">>, <<"new">>, #{urn := Urn, location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) -> - logger:debug("Actual Urn: ~p", [Urn]), - {ok, Schemas} = dreki_store:list(<>), - {ok, Schema} = dreki_store:get(<>), - Form = dreki_web_ui_json_form:render_html(Schema, #{}), - Html = dreki_web_ui:render(Req, store_new_dtl, [ - {location, Loc}, {namespace, NS}, {directory, Dir}, - {schema, Schema}, {schemas, Schemas}, - {target, urn_to_path(Urn)}, {method, <<"POST">>}, - {form, Form} - ]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; - -%% Show -request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> - {ok, Result0} = dreki_store:get(Urn), - Result = Result0#{'@href' => urn_to_path(Urn)}, - Html = dreki_web_ui:render(Req, store_show_dtl, [{location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, {result, Result}]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}. - -derpinit(Req, _) -> - Json = #{<<"error">> => false, <<"service">> => <<"dreki">>}, - logger:debug("REQ: ~p", [Req]), - {ok, dreki_web:reply_json(Req, 200, Json), undefined}. - - -bad_request(Req) -> - dreki_web_error:init(Req, #{code => 400, status => "Bad request"}). - -href(Req = #{path := Path}, Append) -> - <>. - -location_to_path(Location) -> - Root = dreki_world:root_path(), - case Location =:= Root of - true -> <<"/admin/-">>; - false -> binary:replace(Location, <>, <<"/admin/">>) - end. - -urn_to_path(Urn) when is_binary(Urn) -> - {ok, XUrn} = dreki_urn:expand(Urn), - urn_to_path(XUrn); -urn_to_path(#{location := Location, resource := #{namespace := NS}}) -> - LP = location_to_path(Location), - <>; -urn_to_path(#{location := Location, resource := #{directory := #{directory := Dir, namespace := NS}}}) -> - LP = location_to_path(Location), - <>; -urn_to_path(#{location := Location, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}) -> - LP = location_to_path(Location), - <>. diff --git a/apps/dreki_web/src/dreki_web_ui_task.erl b/apps/dreki_web/src/dreki_web_ui_task.erl deleted file mode 100644 index 1ced583..0000000 --- a/apps/dreki_web/src/dreki_web_ui_task.erl +++ /dev/null @@ -1,20 +0,0 @@ --module(dreki_web_ui_task). --behaviour(cowboy_handler). --export([init/2]). - -init(Req = #{method := <<"GET">>}, State) -> - Id = cowboy_req:binding(id, Req), - {ok, Db} = dreki_tasks:open(), - case dreki_tasks:get(Db, Id) of - {ok, Task} -> - MTask = dreki_task:to_map(Task), - PrettyParams = jsone:encode(maps:get(params, MTask), [canonical_form, {space, 1}, {indent, 4}]), - Html = dreki_web_ui:render(Req, task_dtl, [{"page_title", <<"Task: ", Id/binary>>}, {"task", MTask}, {"task_pretty_params", PrettyParams}]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; - Error -> - logger:debug("Failed to lookup task ~p: ~p", [Id, Error]), - dreki_web_ui_error:init(Req, #{code => 404, status => "Not found"}) - end; -init(Req, _) -> - dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). - diff --git a/apps/dreki_web/src/dreki_web_ui_tasks.erl b/apps/dreki_web/src/dreki_web_ui_tasks.erl deleted file mode 100644 index e9748b8..0000000 --- a/apps/dreki_web/src/dreki_web_ui_tasks.erl +++ /dev/null @@ -1,14 +0,0 @@ --module(dreki_web_ui_tasks). --behaviour(cowboy_handler). --export([init/2]). - -init(Req = #{method := <<"GET">>}, State) -> - Local = maps:fold(fun - (Ln, #{mod := Mod, path := Path}, Acc) -> - [#{name => Ln, mod => Mod, path => Path, url => <<"/api/admin/tasks/", Path>>} | Acc] - end, [], dreki_tasks:local_stores()), - Html = dreki_web_ui:render(Req, tasks_dtl, [{"page_title", "Tasks"}, {"stores", Local}, {"tasks", []}]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; -init(Req, _) -> - dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). - diff --git a/apps/dreki_web/src/ui/dreki_web_ui.erl b/apps/dreki_web/src/ui/dreki_web_ui.erl new file mode 100644 index 0000000..e394632 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui.erl @@ -0,0 +1,44 @@ +-module(dreki_web_ui). +-export([render/3, reply_html/3, reply_html/4]). + +render(Req, InnerModule, Assigns0) -> + Assigns = assigns(Req, Assigns0), + {ok, InnerHtml} = InnerModule:render(Assigns), + render_layout(Req, InnerHtml, Assigns). + +reply_html(Req, Code, Html) -> + reply_html(Req, Code, Html, #{}). + +reply_html(Req, Code, Html, Headers0) -> + Headers = maps:put(<<"content-type">>, <<"text/html">>, Headers0), + cowboy_req:reply(Code, Headers, Html, Req). + +render_layout(Req, InnerHtml, Assigns) -> + {ok, Html} = layout_dtl:render([{"inner", InnerHtml} | Assigns]), + Html. + +assigns(Req, Assigns0) -> + Assigns = clean_assigns(Assigns0), + [{"site_title", "Dreki"}, + {"identity_id", maps:get(identity_id, Req)}, + {"identity", maps:get(identity, Req)}, + {"identity_name", dreki_web:identity_name(Req)}, + {"dreki_node", node()}, + {"dreki_world", dreki_world:to_map()}, + {"trace_id", maps:get(<<"trace-id">>, Req, <<"no-trace">>)} + | Assigns]. + +content_types_accepted(Req, State) -> + {[ + {{ <<"multipart">>, <<"form-data">>, '*'}, from_form} + ], Req, State}. + +content_types_provided(Req, State) -> + {[{{ <<"text">>, <<"html">>, '*'}, to_html}], Req, State}. + +clean_assigns(Assigns) when is_list(Assigns) -> + Assigns; +clean_assigns(Map) when is_map(Map) -> + maps:fold(fun (Key, Value, Acc) -> [{Key, clean_assigns(Value)} | Acc] end, [], Map); +clean_assigns(Other) -> + Other. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_error.erl b/apps/dreki_web/src/ui/dreki_web_ui_error.erl new file mode 100644 index 0000000..ccda150 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_error.erl @@ -0,0 +1,14 @@ +-module(dreki_web_ui_error). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req, not_found) -> + reply(Req, 404, <<"Not Found">>, undefined); +init(Req, State = #{code := Code, status := Status}) -> + reply(Req, Code, Status, maps:get(message, State, undefined)). + +reply(Req0, Code, Status, Msg) -> + Assigns = [{"message", Msg}, {"status", Status}], + Html = dreki_web_ui:render(Req0, error_dtl, Assigns), + Req = dreki_web_ui:reply_html(Req0, Code, Html), + {ok, Req, undefined}. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_index.erl b/apps/dreki_web/src/ui/dreki_web_ui_index.erl new file mode 100644 index 0000000..9f4684e --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_index.erl @@ -0,0 +1,31 @@ +-module(dreki_web_ui_index). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req = #{method := <<"GET">>}, State) -> + PrettyWorld = jsone:encode(dreki_world:to_map(), [canonical_form, {space, 1}, {indent, 4}]), + LocalTasksStores = dreki_tasks:local_stores(), + {ok, Peers} = partisan_peer_service:members(), + NavTree = nav_tree(dreki_world_dns:as_map()), + Html = dreki_web_ui:render(Req, index_dtl, [ + {"page_title", "Admin UI"}, + {"dreki_world_pretty_json", PrettyWorld}, + {"dreki_local_tasks_stores", LocalTasksStores}, + {"dreki_peers", Peers}, + {"nav_tree", NavTree} + ]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; +init(Req, _) -> + dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). + +nav_tree(#{vertices := Vertices}) -> + nav_tree(Vertices, []). + +nav_tree([Region = #{type := region, name := Name, data := #{display_name := Display}} | Rest], Acc) -> + nav_tree(Rest, [Region#{href => <<"/admin/regions/", Name/binary>>, title => Display} | Acc]); +nav_tree([Node = #{type := node, name := Name, data := #{display_name := Display}} | Rest], Acc) -> + nav_tree(Rest, [Node#{href => <<"/admin/nodes/", Name/binary>>, title => Display} | Acc]); +nav_tree([#{type := Root} | Rest], Acc) -> + nav_tree(Rest, Acc); +nav_tree([], Acc) -> + Acc. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl new file mode 100644 index 0000000..51a9de4 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl @@ -0,0 +1,160 @@ +-module(dreki_web_ui_json_form). +-export([render_html/2]). +-export([render/2]). +-export([to_html/1]). + +-type dreki_form() :: #{ + input => binary(), + label => binary() +}. + +render_html(Schema, Opts) -> + Abstract = render(Schema, Opts), + to_html(Abstract). + +to_html(Atom) when is_atom(Atom) -> + as_binary(Atom); +to_html(Binary) when is_binary(Binary) -> + Binary; +to_html(Abstract) -> + to_html(Abstract, []). + +to_html([{Node, Attrs} | Rest], Acc) -> + BNode = as_binary(Node), + AttrsS = attrs_to_html(Attrs), + to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>] | Acc]); +to_html([{Node, Attrs, Content} | Rest], Acc) -> + BNode = as_binary(Node), + AttrsS = attrs_to_html(Attrs), + logger:debug("Node ~p Attrs ~p Content ~p", [Node, Attrs, Content]), + to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>, to_html(Content), <<">, BNode, <<">">>] | Acc]); +to_html([List | Rest], Acc) when is_list(List) -> + to_html(Rest, [to_html(List) | Acc]); +to_html([], Acc) -> + lists:reverse(Acc). + +attrs_to_html(Attrs) -> attrs_to_html(Attrs, []). +attrs_to_html([{Attr, undefined} | Rest], Acc) -> + attrs_to_html(Rest, Acc); +attrs_to_html([{Attr, false} | Rest], Acc) -> + attrs_to_html(Rest, Acc); +attrs_to_html([{Attr, true} | Rest], Acc) -> + attrs_to_html(Rest, [[as_binary(Attr), <<" ">>] | Acc]); +attrs_to_html([{Attr, Num} | Rest], Acc) when is_integer(Num) -> + attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, Num, <<" ">>] | Acc]); +attrs_to_html([{Attr, Value} | Rest], Acc) -> + attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, <<"\"">>, as_binary(Value), <<"\"">>, <<" ">>] | Acc]); +attrs_to_html([], Acc) -> + Acc. + +put_new(Key, Value, Map) -> + case maps:is_key(Key, Map) of + true -> + Map; + false -> + maps:put(Key, Value, Map) + end. + +render(Schema, Opts) -> + render(Schema, Opts, {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}]}). + +render(Schema, Opts, {Elem, Attrs}) -> + [{Elem, Attrs, render(Schema, Opts, undefined)}]; +render(Schema, Opts0, undefined) -> + Opts = put_new(name, <<"form">>, Opts0), + lists:reverse(maps:fold(fun (Key, Value, Acc) -> + case render_property(Key, Value, Schema, Schema, Opts) of + {ok, Html} -> [Html | Acc]; + ignore -> Acc + end + end, [], maps:get(properties, Schema))). + +render_property(Field, Config = #{enum := Enum}, Parent, Schema, Opts) -> + FormAttrs = maps:get(<<"dreki:form">>, Config, #{}), + Required = lists:member(Field, maps:get(required, Parent, [])), + Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)), + InputOpts = #{required => Required, label => Label, values => Enum}, + {ok, render_input(select, Field, undefined, InputOpts, Opts)}; + +render_property(Field, Config = #{type := Type}, Parent, Schema, Opts) -> + FormAttrs = maps:get(<<"dreki:form">>, Config, #{}), + Input = maps:get(input, FormAttrs, input), + InputType = maps:get(input_type, FormAttrs, input_type(Type)), + Required = lists:member(Field, maps:get(required, Parent, [])), + Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)), + InputOpts = #{required => Required, label => Label, input_type => InputType}, + {ok, render_input(Input, Field, Type, InputOpts, Opts)}; + +render_property(Key, SubSchema = #{<<"@schema">> := SubSchemaUrn}, Parent, Schema, Opts) -> + Name = maps:get(name, Opts), + KeyB = atom_to_binary(Key), + InnerForm = render(SubSchema, Opts#{name => <>}, undefined), + Head = {'div', [{class, <<"sm:col-span-6 mb-3">>}], [ + {h3, [{class, <<"text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">>}], maps:get(title, SubSchema, SubSchemaUrn)}, + {p, [{class, <<"mt-1 text-sm text-gray-500 dark:text-gray-400">>}], maps:get(description, SubSchema, <<>>)} + ]}, + {ok, {'div', [{class, <<"sm:col-span-6">>}], + [ + {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}], [Head, InnerForm]} + ]}}; + +render_property(Field, #{'$ref' := Ref}, Parent, Schema = #{'$defs' := Defs}, Opts) -> + case maps:get(Ref, Defs, undefined) of + undefined -> logger:error("didn't get ref ~p", [Ref]); + ExpandedRef -> + logger:debug("Skipping ref for now: ~p ~p", [Ref, ExpandedRef]) + end, + ignore. + +base_attributes(Field, IOpts, FOpts) -> + Name = field_name(Field, FOpts), + {Name, [ + {id, field_id(Field, FOpts)}, + {name, Name}, + {required, maps:get(required, IOpts, false)} + ]}. + +render_input(select, Field, _, IOpts, FOpts) -> + {Name, Attributes} = base_attributes(Field, IOpts, FOpts), + OptionsHtml = lists:map(fun (Opt) -> {option, [], Opt} end, maps:get(values, IOpts, [])), + {'div', [{class, <<"json-field">>}], + [ + label(Field, Name, maps:get(label, IOpts, Field)), + {select, Attributes, OptionsHtml} + ]}; + +render_input(input, Field, Type, IOpts, FOpts) -> + {Name, Attributes0} = base_attributes(Field, IOpts, FOpts), + Attributes = [ + {value, maps:get(value, IOpts, undefined)}, + {placeholder, maps:get(placeholder, IOpts, undefined)}, + {readonly, maps:get(readonly, IOpts, false)}, + {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)}, + {type, maps:get(input_type, IOpts)}, + {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-800">>} + | Attributes0], + {'div', [{class, <<"json-field sm:col-span-6">>}], + [ + label(Field, Name, maps:get(label, IOpts, Field)), + {'div', [{class, <<"mt-1">>}], [{input, Attributes}]} + ]}. + +label(_Field, Name, Label) -> + {label, [{for, Name}, {class, <<"block text-sm font-medium dark:text-gray-400 text-gray-700 dark:text-gray-400">>}], Label}. + +as_binary(Atom) when is_atom(Atom) -> + atom_to_binary(Atom); +as_binary(B) when is_binary(B) -> + B. + +input_type(_) -> <<"text">>. + +field_name(Field, Opts) -> + FB = as_binary(Field), + BaseName = maps:get(name, Opts, <<"form">>), + <>. + +field_id(Field, Opts) -> + FB = as_binary(Field), + BaseId = maps:get(id, Opts, <<"form">>), + <>. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_node.erl b/apps/dreki_web/src/ui/dreki_web_ui_node.erl new file mode 100644 index 0000000..fdb7c77 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_node.erl @@ -0,0 +1,23 @@ +-module(dreki_web_ui_node). +-behaviour(cowboy_handler). +-behaviour(cowboy_rest). +-export([init/2]). +-export([allowed_methods/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +allowed_methods(Req, State) -> + {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}. + +content_types_accepted(Req, State) -> + dreki_web:content_types_accepted(Req, State). + +content_types_provided(Req, State) -> + dreki_web:content_types_provided(Req, State). + +to_html(Req, State) -> + Html = dreki_web_ui:render(Req, node, []), + {Html, Req, State}. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_stores.erl b/apps/dreki_web/src/ui/dreki_web_ui_stores.erl new file mode 100644 index 0000000..e39cd48 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_stores.erl @@ -0,0 +1,189 @@ +-module(dreki_web_ui_stores). +-behaviour(cowboy_handler). +-export([init/2]). + + +init(Req, action) -> + with_location(Req#{action => cowboy_req:binding(action, Req)}, cowboy_req:binding(location, Req, undefined)); +init(Req, Action) -> + with_location(Req#{action => Action}, cowboy_req:binding(location, Req, undefined)). + +with_location(Req, undefined) -> + with_namespace(Req#{urn => dreki_world:root_path()}); +with_location(Req, <<"-">>) -> + with_namespace(Req#{urn => dreki_world:root_path()}); +with_location(Req0, Location) -> + Root = dreki_world:root_path(), + {ok, XUrn} = dreki_urn:expand(<>), + Req = Req0#{urn => maps:get(urn, XUrn)}, + with_namespace(Req). + +with_namespace(Req) -> + with_namespace(Req, cowboy_req:binding(namespace, Req)). +with_namespace(Req, undefined) -> + request(Req); +with_namespace(Req = #{urn := Urn0}, Namespace) -> + Urn = <>, + with_directory(Req#{urn => Urn}). + +with_directory(Req) -> + with_directory(Req, cowboy_req:binding(directory, Req)). +with_directory(Req, undefined) -> + request(Req); +with_directory(Req = #{urn := Urn0}, Directory) -> + Urn = <>, + with_id(Req#{urn => Urn}). + +with_id(Req) -> + with_id(Req, cowboy_req:binding(id, Req)). +with_id(Req, undefined) -> + request(Req); +with_id(Req = #{urn := Urn0}, Id) -> + Urn = <>, + request(Req#{urn => Urn}). + +request(Req = #{action := Action, method := Method, urn := Urn}) -> + logger:debug("Faked Urn ~p", [Urn]), + case dreki_urn:expand(Urn) of + {ok, XUrn} -> request(Method, Action, XUrn, Req); + {error, EMap=#{}} -> dreki_web_error:init(Req, EMap); + {error, _Error} -> dreki_web_error:init(Req, #{code => 404, status => "Not Found"}) + end. + +%% Stores list +request(<<"GET">>, undefined, #{resource := #{namespace := NS}}, Req) -> + {ok, Stores0} = dreki_store:stores(NS), + Stores = lists:map(fun (Store) -> + S = dreki_store:store_as_map(Store), + S#{href => urn_to_path(maps:get(urn, S))} + end, Stores0), + Html = dreki_web_ui:render(Req, namespace_dtl, [{namespace, NS}, {stores, Stores}]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; + +%% List +request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) -> + {ok, Result} = dreki_store:list(Urn), + Results0 = lists:map(fun(Result) -> + Result#{href => urn_to_path(maps:get('@id', Result))} + end, maps:get(data, Result)), + Results = deep_map_to_list(Results0), + Html = dreki_web_ui:render(Req, store_list_dtl, [ + {location, Loc}, {namespace, NS}, {directory, Dir}, + {results, Results}, + {actions, [ + [{title, "New"}, {href, href(Req, <<"_/new">>)}] + ]} + ]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; + +%% New +request(<<"GET">>, <<"new">>, #{urn := Urn, location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) -> + logger:debug("Actual Urn: ~p", [Urn]), + {ok, Schemas} = dreki_store:list(<>), + {ok, Schema} = dreki_store:get(<>), + Form = dreki_web_ui_json_form:render_html(Schema, #{}), + Html = dreki_web_ui:render(Req, store_new_dtl, [ + {location, Loc}, {namespace, NS}, {directory, Dir}, + {schema, Schema}, {schemas, Schemas}, + {target, urn_to_path(Urn)}, {method, <<"POST">>}, + {form, Form} + ]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; + +%% Show +request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> + {ok, Result0} = dreki_store:get(Urn), + Result = Result0#{'@href' => urn_to_path(Urn)}, + Actions = lists:foldr(fun (#{id := Id, title := Title}, Acc) -> + IdB = atom_to_binary(Id), + Action = [{id, Id}, {title, Title}, {href, href(Req, <<"_/", IdB/binary>>)}], + [Action | Acc] + end, [], maps:get('@actions', Result)), + Html = dreki_web_ui:render(Req, store_show_dtl, [{location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, + {result, deep_map_to_list(Result)}, + {actions, [ + [{title, "Edit (UI)"}, {href, href(Req, <<"_/edit">>)}] + | Actions + ]} + ]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; + +%% Show action +request(<<"GET">>, Action0, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> + {ok, Result0} = dreki_store:get(Urn), + Result = Result0#{'@href' => urn_to_path(Urn)}, + Action = binary_to_existing_atom(Action0), + Path = href(Req, <<"_/", Action0/binary>>), + Act = lists:filter(fun (A) -> maps:get(id, A) =:= Action end, maps:get('@actions', Result)), + case Act of + [#{title := Title, new := {Mod, Fun, Args}}] -> + {ok, Schema} = apply(Mod, Fun, [Result | Args]), + Form = dreki_web_ui_json_form:render_html(Schema, #{}), + Html = dreki_web_ui:render(Req, store_new_dtl, [ + {location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, + {method, <<"POST">>}, + {title, Title}, + {result, deep_map_to_list(Result)}, + {target, Path}, + {form, Form} + ]), + {dreki_web_ui:reply_html(Req, 200, Html), undefined}; + _ -> + dreki_web_ui_error:init(Req, #{code => 404, status => "Not Found", message => "No such action"}) + end; +request(<<"POST">>, Action0, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> + {ok, Result0} = dreki_store:get(Urn), + Result = Result0#{'@href' => urn_to_path(Urn)}, + Action = binary_to_existing_atom(Action0), + Act = lists:filter(fun (A) -> maps:get(id, A) =:= Action end, maps:get('@actions', Result)), + case Act of + [#{title := Title, new := {Mod, Fun, Args}}] -> + Req; + _ -> + dreki_web_ui_error:init(Req, #{code => 404, status => "Not Found", message => "No such action"}) + end. + +derpinit(Req, _) -> + Json = #{<<"error">> => false, <<"service">> => <<"dreki">>}, + logger:debug("REQ: ~p", [Req]), + {ok, dreki_web:reply_json(Req, 200, Json), undefined}. + + +bad_request(Req) -> + dreki_web_error:init(Req, #{code => 400, status => "Bad request"}). + +href(Req = #{path := Path}, Append) -> + <>. + +location_to_path(Location) -> + Root = dreki_world:root_path(), + case Location =:= Root of + true -> <<"/admin/-">>; + false -> binary:replace(Location, <>, <<"/admin/">>) + end. + +deep_map_to_list(List) when is_list(List) -> + [deep_map_to_list(Map) || Map <- List]; +deep_map_to_list(Map) when is_map(Map) -> + deep_map_to_list(maps:to_list(Map), []). + +deep_map_to_list([{Key, Map} | Rest], Acc) when is_map(Map) -> + List = deep_map_to_list(Map), + deep_map_to_list(Rest, [{Key, List} | Acc]); +deep_map_to_list([Item | Rest], Acc) -> + deep_map_to_list(Rest, [Item | Acc]); +deep_map_to_list([], Acc) -> + Acc. + +urn_to_path(Urn) when is_binary(Urn) -> + {ok, XUrn} = dreki_urn:expand(Urn), + urn_to_path(XUrn); +urn_to_path(#{location := Location, resource := #{namespace := NS}}) -> + LP = location_to_path(Location), + <>; +urn_to_path(#{location := Location, resource := #{directory := #{directory := Dir, namespace := NS}}}) -> + LP = location_to_path(Location), + <>; +urn_to_path(#{location := Location, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}) -> + LP = location_to_path(Location), + <>. diff --git a/apps/dreki_web/src/ui/dreki_web_ui_task.erl b/apps/dreki_web/src/ui/dreki_web_ui_task.erl new file mode 100644 index 0000000..1ced583 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_task.erl @@ -0,0 +1,20 @@ +-module(dreki_web_ui_task). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req = #{method := <<"GET">>}, State) -> + Id = cowboy_req:binding(id, Req), + {ok, Db} = dreki_tasks:open(), + case dreki_tasks:get(Db, Id) of + {ok, Task} -> + MTask = dreki_task:to_map(Task), + PrettyParams = jsone:encode(maps:get(params, MTask), [canonical_form, {space, 1}, {indent, 4}]), + Html = dreki_web_ui:render(Req, task_dtl, [{"page_title", <<"Task: ", Id/binary>>}, {"task", MTask}, {"task_pretty_params", PrettyParams}]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; + Error -> + logger:debug("Failed to lookup task ~p: ~p", [Id, Error]), + dreki_web_ui_error:init(Req, #{code => 404, status => "Not found"}) + end; +init(Req, _) -> + dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). + diff --git a/apps/dreki_web/src/ui/dreki_web_ui_tasks.erl b/apps/dreki_web/src/ui/dreki_web_ui_tasks.erl new file mode 100644 index 0000000..e9748b8 --- /dev/null +++ b/apps/dreki_web/src/ui/dreki_web_ui_tasks.erl @@ -0,0 +1,14 @@ +-module(dreki_web_ui_tasks). +-behaviour(cowboy_handler). +-export([init/2]). + +init(Req = #{method := <<"GET">>}, State) -> + Local = maps:fold(fun + (Ln, #{mod := Mod, path := Path}, Acc) -> + [#{name => Ln, mod => Mod, path => Path, url => <<"/api/admin/tasks/", Path>>} | Acc] + end, [], dreki_tasks:local_stores()), + Html = dreki_web_ui:render(Req, tasks_dtl, [{"page_title", "Tasks"}, {"stores", Local}, {"tasks", []}]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), State}; +init(Req, _) -> + dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}). + -- cgit v1.2.3