From 4718a059f842a791c89a1922996bb8f1dbea8f65 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Tue, 16 May 2017 23:27:53 +0300 Subject: Add filter form to builds page --- mod/mod-builds.cxx | 263 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 249 insertions(+), 14 deletions(-) (limited to 'mod/mod-builds.cxx') 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 // to_string() +#include // to_result_status(), to_string(result_status) + #include #include +#include #include #include @@ -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 static inline query -build_query (const C& configs) +build_query (const C& configs, const brep::params::builds& params) { + using namespace brep; + using query = query; - 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 (static_cast (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_query (*build_conf_names_))); + build_query (*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> toolchains ({{"*", "*"}}); + { + using query = query; + + for (const auto& t: build_db_->query ( + "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" + // 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 (), "") + << 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_query (*build_conf_names_) + + build_query (*build_conf_names_, params) + "ORDER BY" + query::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 () : "") + << TR_VALUE ("target", b.target ? b.target->string () : "") << 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; -- cgit v1.1