aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-05-16 23:27:53 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-05-19 11:44:48 +0300
commit4718a059f842a791c89a1922996bb8f1dbea8f65 (patch)
treea6dcc621f8287d444a699355f89b4a383eafd283 /mod
parente2264d6c34de011753913dd9b447b3d38649619c (diff)
Add filter form to builds page
Diffstat (limited to 'mod')
-rw-r--r--mod/build-config.cxx8
-rw-r--r--mod/mod-build-log.cxx9
-rw-r--r--mod/mod-build-task.cxx6
-rw-r--r--mod/mod-builds.cxx263
-rw-r--r--mod/mod-repository-root.cxx63
-rw-r--r--mod/options.cli49
-rw-r--r--mod/page.cxx52
-rw-r--r--mod/page.hxx49
8 files changed, 439 insertions, 60 deletions
diff --git a/mod/build-config.cxx b/mod/build-config.cxx
index 5fd540f..9eb40ce 100644
--- a/mod/build-config.cxx
+++ b/mod/build-config.cxx
@@ -65,9 +65,9 @@ namespace brep
// encoded by design.
//
return host + root.string () +
- "?build-force&p=" + mime_url_encode (b.package_name) +
- "&v=" + b.package_version.string () +
- "&c=" + mime_url_encode (b.configuration) +
- "&t=" + b.toolchain_version.string () + "&reason=";
+ "?build-force&pn=" + mime_url_encode (b.package_name) +
+ "&pv=" + b.package_version.string () +
+ "&cf=" + mime_url_encode (b.configuration) +
+ "&tc=" + b.toolchain_version.string () + "&reason=";
}
}
diff --git a/mod/mod-build-log.cxx b/mod/mod-build-log.cxx
index b998f8d..a1e73ca 100644
--- a/mod/mod-build-log.cxx
+++ b/mod/mod-build-log.cxx
@@ -170,8 +170,8 @@ handle (request& rq, response& rs)
// Make sure the build configuration still exists.
//
- auto i (build_conf_map_->find (id.configuration.c_str ()));
- if (i == build_conf_map_->end ())
+ if (build_conf_map_->find (id.configuration.c_str ()) ==
+ build_conf_map_->end ())
config_expired ("no configuration");
// Make sure the package still exists.
@@ -215,9 +215,8 @@ handle (request& rq, response& rs)
<< "config: " << b->configuration << endl
<< "machine: " << *b->machine << " (" << *b->machine_summary << ")"
<< endl
- << "target: " << (i->second->target
- ? i->second->target->string ()
- : "<default>") << endl
+ << "target: " << (b->target ? b->target->string () : "<default>")
+ << endl
<< "timestamp: ";
butl::to_stream (os, b->timestamp, "%Y-%m-%d %H:%M:%S%[.N] %Z", true, true);
diff --git a/mod/mod-build-task.cxx b/mod/mod-build-task.cxx
index 3e02463..ec236c0 100644
--- a/mod/mod-build-task.cxx
+++ b/mod/mod-build-task.cxx
@@ -396,7 +396,8 @@ handle (request& rq, response& rs)
move (tqm.toolchain_name),
move (toolchain_version),
mh.name,
- move (mh.summary));
+ move (mh.summary),
+ cm.config->target);
build_db_->persist (b);
}
@@ -431,6 +432,7 @@ handle (request& rq, response& rs)
b->toolchain_name = move (tqm.toolchain_name);
b->machine = mh.name;
b->machine_summary = move (mh.summary);
+ b->target = cm.config->target;
b->timestamp = timestamp::clock::now ();
build_db_->update (b);
@@ -552,6 +554,8 @@ handle (request& rq, response& rs)
b->toolchain_name = tqm.toolchain_name;
b->machine_summary = mh.summary;
+ b->target = cm.config->target;
+
// Mark the section as loaded, so results are updated.
//
b->results_section.load ();
diff --git a/mod/mod-builds.cxx b/mod/mod-builds.cxx
index 6b6ab08..70ef605 100644
--- a/mod/mod-builds.cxx
+++ b/mod/mod-builds.cxx
@@ -11,8 +11,11 @@
#include <libbutl/timestamp.hxx> // to_string()
+#include <libbbot/manifest.hxx> // to_result_status(), to_string(result_status)
+
#include <web/xhtml.hxx>
#include <web/module.hxx>
+#include <web/mime-url-encoding.hxx>
#include <libbrep/build.hxx>
#include <libbrep/build-odb.hxx>
@@ -25,6 +28,7 @@
using namespace std;
using namespace bbot;
+using namespace web;
using namespace odb::core;
using namespace brep::cli;
@@ -60,12 +64,137 @@ init (scanner& s)
template <typename T, typename C>
static inline query<T>
-build_query (const C& configs)
+build_query (const C& configs, const brep::params::builds& params)
{
+ using namespace brep;
+
using query = query<T>;
- return query::id.configuration.in_range (configs.begin (), configs.end ()) &&
- (query::state == "testing" || query::state == "tested");
+ // Transform the wildcard to the LIKE-pattern.
+ //
+ auto transform = [] (const string& s) -> string
+ {
+ if (s.empty ())
+ return "%";
+
+ string r;
+ for (char c: s)
+ {
+ switch (c)
+ {
+ case '*': c = '%'; break;
+ case '?': c = '_'; break;
+ case '\\':
+ case '%':
+ case '_': r += '\\'; break;
+ }
+
+ r += c;
+ }
+
+ return r;
+ };
+
+ query q (
+ query::id.configuration.in_range (configs.begin (), configs.end ()) &&
+ (query::state == "testing" || query::state == "tested"));
+
+ // Note that there is no error reported if the filter parameters parsing
+ // fails. Instead, it is considered that no package builds match such a
+ // query.
+ //
+ try
+ {
+ // Package name.
+ //
+ if (!params.name ().empty ())
+ q = q && query::id.package.name.like (transform (params.name ()));
+
+ // Package version.
+ //
+ if (!params.version ().empty () && params.version () != "*")
+ {
+ version v (params.version ()); // May throw invalid_argument.
+ q = q && compare_version_eq (query::id.package.version, v, true);
+ }
+
+ // Build toolchain name/version.
+ //
+ const string& tc (params.toolchain ());
+
+ if (tc != "*")
+ {
+ size_t p (tc.find ('-'));
+ if (p == string::npos) // Invalid format.
+ throw invalid_argument ("");
+
+ string tn (tc, 0, p);
+ version tv (string (tc, p + 1)); // May throw invalid_argument.
+
+ q = q && query::toolchain_name == tn &&
+ compare_version_eq (query::id.toolchain_version, tv, true);
+ }
+
+ // Build configuration name.
+ //
+ if (!params.configuration ().empty ())
+ q = q && query::id.configuration.like (
+ transform (params.configuration ()));
+
+ // Build machine name.
+ //
+ if (!params.machine ().empty ())
+ query::machine.like (transform (params.machine ()));
+
+ // Build target.
+ //
+ const string& tg (params.target ());
+
+ if (tg != "*")
+ q = q && (tg.empty ()
+ ? query::target.is_null ()
+ : query::target.like (transform (tg)));
+
+ // Build result.
+ //
+ const string& rs (params.result ());
+
+ if (!rs.empty () && rs != "*")
+ {
+ if (rs == "pending")
+ q = q && query::forced;
+ else if (rs == "building")
+ q = q && query::state == "testing";
+ else
+ {
+ query sq (query::status == rs);
+ result_status st (to_result_status(rs)); // May throw invalid_argument.
+
+ if (st != result_status::success)
+ {
+ auto next = [&st] () -> bool
+ {
+ if (st == result_status::abnormal)
+ return false;
+
+ st = static_cast<result_status> (static_cast<uint8_t> (st) + 1);
+ return true;
+ };
+
+ while (next ())
+ sq = sq || query::status == to_string (st);
+ }
+
+ q = q && sq;
+ }
+ }
+ }
+ catch (const invalid_argument&)
+ {
+ return query (false);
+ }
+
+ return q;
}
bool brep::builds::
@@ -104,6 +233,15 @@ handle (request& rq, response& rs)
<< HEAD
<< TITLE << title << ~TITLE
<< CSS_LINKS (path ("builds.css"), root)
+ //
+ // This hack is required to avoid the "flash of unstyled content", which
+ // happens due to the presence of the autofocus attribute in the input
+ // element of the search form. The problem appears in Firefox and has a
+ // (4-year old, at the time of this writing) bug report:
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=712130.
+ //
+ << SCRIPT << " " << ~SCRIPT
<< ~HEAD
<< BODY
<< DIV_HEADER (root, options_->logo (), options_->menu ())
@@ -119,28 +257,103 @@ handle (request& rq, response& rs)
//
auto count (
build_db_->query_value<build_count> (
- build_query<build_count> (*build_conf_names_)));
+ build_query<build_count> (*build_conf_names_, params)));
- s << DIV_COUNTER (count, "Build", "Builds");
+ // Print the package builds filter form on the first page only.
+ //
+ if (page == 0)
+ {
+ // Populate the toolchains list with the distinct list of toolchain
+ // name/version pairs from all the existing package builds. Make sure the
+ // selected toolchain still present in the database. Otherwise fallback to
+ // the * wildcard selection.
+ //
+ string tc ("*");
+ vector<pair<string,string>> toolchains ({{"*", "*"}});
+ {
+ using query = query<toolchain>;
+
+ for (const auto& t: build_db_->query<toolchain> (
+ "ORDER BY" + query::toolchain_name +
+ order_by_version_desc (query::id.toolchain_version, false)))
+ {
+ string s (t.name + '-' + t.version.string ());
+ toolchains.emplace_back (s, s);
+
+ if (s == params.toolchain ())
+ tc = move (s);
+ }
+ }
+
+ // The 'action' attribute is optional in HTML5. While the standard doesn't
+ // specify browser behavior explicitly for the case the attribute is
+ // omitted, the only reasonable behavior is to default it to the current
+ // document URL. Note that we specify the function name using the "hidden"
+ // <input/> element since the action url must not contain the query part.
+ //
+ s << FORM
+ << *INPUT(TYPE="hidden", NAME="builds")
+ << TABLE(ID="filter", CLASS="proplist")
+ << TBODY
+ << TR_INPUT ("name", "pn", params.name (), "*", true)
+ << TR_INPUT ("version", "pv", params.version (), "*")
+ << TR_SELECT ("toolchain", "tc", tc, toolchains)
+
+ << TR(CLASS="config")
+ << TH << "config" << ~TH
+ << TD
+ << *INPUT(TYPE="text",
+ NAME="cf",
+ VALUE=params.configuration (),
+ PLACEHOLDER="*",
+ LIST="configs")
+ << DATALIST(ID="configs")
+ << *OPTION(VALUE="*");
+
+ for (const auto& c: *build_conf_names_)
+ s << *OPTION(VALUE=c);
+
+ s << ~DATALIST
+ << ~TD
+ << ~TR
+
+ << TR_INPUT ("machine", "mn", params.machine (), "*")
+ << TR_INPUT ("target", "tg", params.target (), "<default>")
+ << TR_INPUT ("result", "rs", params.result (), "*")
+ << ~TBODY
+ << ~TABLE
+ << TABLE(CLASS="form-table")
+ << TBODY
+ << TR
+ << TD(ID="build-count")
+ << DIV_COUNTER (count, "Build", "Builds")
+ << ~TD
+ << TD(ID="filter-btn")
+ << *INPUT(TYPE="submit", VALUE="Filter")
+ << ~TD
+ << ~TR
+ << ~TBODY
+ << ~TABLE
+ << ~FORM;
+ }
+ else
+ s << DIV_COUNTER (count, "Build", "Builds");
// Enclose the subsequent tables to be able to use nth-child CSS selector.
//
s << DIV;
for (auto& b: build_db_->query<build> (
- build_query<build> (*build_conf_names_) +
+ build_query<build> (*build_conf_names_, params) +
"ORDER BY" + query<build>::timestamp + "DESC" +
"OFFSET" + to_string (page * page_configs) +
"LIMIT" + to_string (page_configs)))
{
assert (b.machine);
- auto i (build_conf_map_->find (b.configuration.c_str ()));
- assert (i != build_conf_map_->end ());
- const build_config& c (*i->second);
-
string ts (butl::to_string (b.timestamp,
"%Y-%m-%d %H:%M:%S%[.N] %Z",
- true, true));
+ true,
+ true));
s << TABLE(CLASS="proplist build")
<< TBODY
@@ -151,7 +364,7 @@ handle (request& rq, response& rs)
b.toolchain_version.string ())
<< TR_VALUE ("config", b.configuration)
<< TR_VALUE ("machine", *b.machine)
- << TR_VALUE ("target", c.target ? c.target->string () : "<default>")
+ << TR_VALUE ("target", b.target ? b.target->string () : "<default>")
<< TR_VALUE ("timestamp", ts)
<< TR(CLASS="result")
<< TH << "result" << ~TH
@@ -220,8 +433,30 @@ handle (request& rq, response& rs)
t.commit ();
- s << DIV_PAGER (page, count, page_configs, options_->build_pages (),
- root.string () + "?builds")
+ string u (root.string () + "?builds");
+
+ auto add_filter = [&u] (const char* pn,
+ const string& pv,
+ const char* def = "")
+ {
+ if (pv != def)
+ {
+ u += '&';
+ u += pn;
+ u += '=';
+ u += mime_url_encode (pv);
+ }
+ };
+
+ add_filter ("pn", params.name ());
+ add_filter ("pv", params.version ());
+ add_filter ("tc", params.toolchain (), "*");
+ add_filter ("cf", params.configuration ());
+ add_filter ("mn", params.machine ());
+ add_filter ("tg", params.target (), "*");
+ add_filter ("rs", params.result ());
+
+ s << DIV_PAGER (page, count, page_configs, options_->build_pages (), u)
<< ~DIV
<< ~BODY
<< ~HTML;
diff --git a/mod/mod-repository-root.cxx b/mod/mod-repository-root.cxx
index c661b91..c8f2980 100644
--- a/mod/mod-repository-root.cxx
+++ b/mod/mod-repository-root.cxx
@@ -218,11 +218,22 @@ namespace brep
// Delegate the request handling to the selected sub-module. Intercept
// exception handling to add sub-module attribution.
//
- auto handle = [&rs, this] (request& rq, const char* name) -> bool
+ auto handle = [&rq, &rs, this] (const char* nm, bool fn = false) -> bool
{
try
{
- return handler_->handle (rq, rs, *log_);
+ // Delegate the handling straight away if the sub-module is not a
+ // function. Otherwise, cleanup the request not to confuse the
+ // sub-module with the unknown parameter.
+ //
+ if (!fn)
+ return handler_->handle (rq, rs, *log_);
+
+ name_values p (rq.parameters ());
+ p.erase (p.begin ());
+
+ request_proxy rp (rq, p);
+ return handler_->handle (rp, rs, *log_);
}
catch (const invalid_request&)
{
@@ -242,7 +253,7 @@ namespace brep
// module::handle() function call.
//
ostringstream os;
- os << name << ": " << e;
+ os << nm << ": " << e;
throw runtime_error (os.str ());
}
};
@@ -256,21 +267,15 @@ namespace brep
//
if (lpath.empty ())
{
- // Dispatch request handling to the repository_details, the
- // package_search or the one of build_* modules depending on the function
- // name passed as a first HTTP request parameter. The parameter should
- // have no value specified. Example: cppget.org/?about
+ // Dispatch request handling to the repository_details or the one of
+ // build_* modules depending on the function name passed as a first HTTP
+ // request parameter (example: cppget.org/?about). Dispatch to the
+ // package_search module if the function name is unavailable (no
+ // parameters) or is not recognized.
//
const name_values& params (rq.parameters ());
- if (!params.empty () && !params.front ().value)
+ if (!params.empty ())
{
- // Cleanup not to confuse the selected module with the unknown
- // parameter.
- //
- name_values p (params);
- p.erase (p.begin ());
-
- request_proxy rp (rq, p);
const string& fn (params.front ().name);
if (fn == "about")
@@ -278,46 +283,42 @@ namespace brep
if (handler_ == nullptr)
handler_.reset (new repository_details (*repository_details_));
- return handle (rp, "repository_details");
+ return handle ("repository_details", true);
}
else if (fn == "build-task")
{
if (handler_ == nullptr)
handler_.reset (new build_task (*build_task_));
- return handle (rp, "build_task");
+ return handle ("build_task", true);
}
else if (fn == "build-result")
{
if (handler_ == nullptr)
handler_.reset (new build_result (*build_result_));
- return handle (rp, "build_result");
+ return handle ("build_result", true);
}
else if (fn == "build-force")
{
if (handler_ == nullptr)
handler_.reset (new build_force (*build_force_));
- return handle (rp, "build_force");
+ return handle ("build_force", true);
}
else if (fn == "builds")
{
if (handler_ == nullptr)
handler_.reset (new builds (*builds_));
- return handle (rp, "builds");
+ return handle ("builds", true);
}
- else
- throw invalid_request (400, "unknown function");
}
- else
- {
- if (handler_ == nullptr)
- handler_.reset (new package_search (*package_search_));
- return handle (rq, "package_search");
- }
+ if (handler_ == nullptr)
+ handler_.reset (new package_search (*package_search_));
+
+ return handle ("package_search");
}
else
{
@@ -355,7 +356,7 @@ namespace brep
if (handler_ == nullptr)
handler_.reset (new package_details (*package_details_));
- return handle (rq, "package_details");
+ return handle ("package_details");
}
else if (++i == lpath.end ())
{
@@ -363,14 +364,14 @@ namespace brep
handler_.reset (
new package_version_details (*package_version_details_));
- return handle (rq, "package_version_details");
+ return handle ("package_version_details");
}
else if (*i == "log")
{
if (handler_ == nullptr)
handler_.reset (new build_log (*build_log_));
- return handle (rq, "build_log");
+ return handle ("build_log");
}
}
}
diff --git a/mod/options.cli b/mod/options.cli
index 9442e7f..ced3de2 100644
--- a/mod/options.cli
+++ b/mod/options.cli
@@ -392,7 +392,7 @@ namespace brep
{
// Package name.
//
- string package | p;
+ string package | pn;
// Package version. May not be url-encoded, in which case the plus
// character is considered literally (rather than as the encoded space
@@ -402,15 +402,15 @@ namespace brep
// @@ Make it of the version type? Maybe after it get moved to
// libbpkg/types.hxx or at least the second use case appear.
//
- string version | v;
+ string version | pv;
// Package build configuration.
//
- string configuration | c;
+ string configuration | cf;
// Toolchain version. May not be url-encoded (see above).
//
- string toolchain_version | t;
+ string toolchain_version | tc;
// Package rebuild reason. Must not be empty.
//
@@ -422,6 +422,47 @@ namespace brep
// Display packages build configurations list starting from this page.
//
uint16_t page | p;
+
+ // Package builds query filter options.
+ //
+
+ // Package name wildcard. An empty value is treated the same way as *.
+ //
+ string name | pn;
+
+ // Package version. If empty or *, then no version constraint is applied.
+ // Otherwise the build package version must match the value exactly.
+ //
+ string version | pv;
+
+ // Package build toolchain in the <name>-<version> form. If *, then no
+ // toolchain constraint is applied. Otherwise the build toolchain name
+ // and version must match the value exactly.
+ //
+ string toolchain | tc = "*";
+
+ // Package build configuration name wildcard. An empty value is treated
+ // the same way as *.
+ //
+ string configuration | cf;
+
+ // Package build machine name wildcard. An empty value is treated the
+ // same way as *.
+ //
+ string machine | mn;
+
+ // Package build target wildcard. If empty, then the default machine
+ // target is matched.
+ //
+ string target | tg = "*";
+
+ // Package build result. If empty or *, then no build result constraint
+ // is applied. Otherwise the value is supposed to be the one of the
+ // following (ordered) statuses: pending, building, success, warning,
+ // error, abort, abnormal. The first 3 statuses are checked for equality,
+ // the rest - for being greater or equal.
+ //
+ string result | rs;
};
}
}
diff --git a/mod/page.cxx b/mod/page.cxx
index de7b5f5..73c56be 100644
--- a/mod/page.cxx
+++ b/mod/page.cxx
@@ -80,7 +80,7 @@ namespace brep
{
// The 'action' attribute is optional in HTML5. While the standard doesn't
// specify browser behavior explicitly for the case the attribute is
- // ommited, the only reasonable behavior is to default it to the current
+ // omitted, the only reasonable behavior is to default it to the current
// document URL.
//
s << FORM(ID="search")
@@ -122,6 +122,56 @@ namespace brep
<< ~TR;
}
+ // TR_INPUT
+ //
+ void TR_INPUT::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS=label_)
+ << TH << label_ << ~TH
+ << TD
+ << INPUT(TYPE="text", NAME=name_);
+
+ if (!value_.empty ())
+ s << VALUE(value_);
+
+ if (!placeholder_.empty ())
+ s << PLACEHOLDER(placeholder_);
+
+ if (autofocus_)
+ s << AUTOFOCUS("autofocus");
+
+ s << ~INPUT
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_SELECT
+ //
+ void TR_SELECT::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS=label_)
+ << TH << label_ << ~TH
+ << TD
+ << SELECT(NAME=name_);
+
+ for (const auto& o: options_)
+ {
+ s << OPTION(VALUE=o.first);
+
+ if (o.first == value_)
+ s << SELECTED("selected");
+
+ s << o.second
+ << ~OPTION;
+ }
+
+ s << ~SELECT
+ << ~TD
+ << ~TR;
+ }
+
// TR_NAME
//
void TR_NAME::
diff --git a/mod/page.hxx b/mod/page.hxx
index 6d18f36..d7c44d6 100644
--- a/mod/page.hxx
+++ b/mod/page.hxx
@@ -108,6 +108,55 @@ namespace brep
const string& value_;
};
+ // Generates table row element, that has the 'label: <input type="text"/>'
+ // layout.
+ //
+ class TR_INPUT
+ {
+ public:
+ TR_INPUT (const string& l,
+ const string& n,
+ const string& v,
+ const string& p = string (),
+ bool a = false)
+ : label_ (l), name_ (n), value_ (v), placeholder_ (p), autofocus_ (a)
+ {
+ }
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& label_;
+ const string& name_;
+ const string& value_;
+ const string& placeholder_;
+ bool autofocus_;
+ };
+
+ // Generates table row element, that has the 'label: <select></select>'
+ // layout. Option elements are represented as a list of value/inner-text
+ // pairs.
+ //
+ class TR_SELECT
+ {
+ public:
+ TR_SELECT (const string& l,
+ const string& n,
+ const string& v,
+ const vector<pair<string, string>>& o)
+ : label_ (l), name_ (n), value_ (v), options_ (o) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& label_;
+ const string& name_;
+ const string& value_;
+ const vector<pair<string, string>>& options_;
+ };
+
// Generates package name element.
//
class TR_NAME