summaryrefslogtreecommitdiffstats
path: root/extensions/ProductDashboard
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/ProductDashboard')
-rw-r--r--extensions/ProductDashboard/Config.pm14
-rw-r--r--extensions/ProductDashboard/Extension.pm192
-rw-r--r--extensions/ProductDashboard/lib/Queries.pm475
-rw-r--r--extensions/ProductDashboard/lib/Util.pm95
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl9
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl234
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl146
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl34
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl38
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl87
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl27
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl122
-rw-r--r--extensions/ProductDashboard/web/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--extensions/ProductDashboard/web/js/components.js75
-rw-r--r--extensions/ProductDashboard/web/js/duplicates.js28
-rw-r--r--extensions/ProductDashboard/web/js/popularity.js28
-rw-r--r--extensions/ProductDashboard/web/js/recents.js32
-rw-r--r--extensions/ProductDashboard/web/js/roadmap.js24
-rw-r--r--extensions/ProductDashboard/web/js/summary.js45
-rw-r--r--extensions/ProductDashboard/web/styles/productdashboard.css45
21 files changed, 1762 insertions, 0 deletions
diff --git a/extensions/ProductDashboard/Config.pm b/extensions/ProductDashboard/Config.pm
new file mode 100644
index 000000000..3a4654974
--- /dev/null
+++ b/extensions/ProductDashboard/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use constant NAME => 'ProductDashboard';
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProductDashboard/Extension.pm b/extensions/ProductDashboard/Extension.pm
new file mode 100644
index 000000000..57192f195
--- /dev/null
+++ b/extensions/ProductDashboard/Extension.pm
@@ -0,0 +1,192 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::Field;
+
+use Bugzilla::Extension::ProductDashboard::Queries;
+use Bugzilla::Extension::ProductDashboard::Util;
+
+our $VERSION = BUGZILLA_VERSION;
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ my $page = $args->{page_id};
+ my $vars = $args->{vars};
+
+ if ($page =~ m{^productdashboard\.}) {
+ _page_dashboard($vars);
+ }
+}
+
+sub _page_dashboard {
+ my $vars = shift;
+
+ my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ # Switch to shadow db since we are just reading information
+ Bugzilla->switch_to_shadow_db();
+
+ # All pages point to the same part of the documentation.
+ $vars->{'doc_section'} = 'bugreports.html';
+
+ # Forget any previously selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => 'X',
+ -expires => "Fri, 01-Jan-1970 00:00:00 GMT");
+
+ # If the user cannot enter bugs in any product, stop here.
+ scalar @{$user->get_selectable_products}
+ || ThrowUserError('no_products');
+
+ # Create data structures representing each classification
+ my @classifications = ();
+ foreach my $c (@{$user->get_selectable_classifications}) {
+ # Create hash to hold attributes for each classification.
+ my %classification = (
+ 'name' => $c->name,
+ 'products' => [ @{$user->get_selectable_products($c->id)} ]
+ );
+ # Assign hash back to classification array.
+ push @classifications, \%classification;
+ }
+ $vars->{'classifications'} = \@classifications;
+
+ my $product_name = trim($input->{'product'} || '');
+
+ if (!$product_name && $cgi->cookie('PRODUCT_DASHBOARD')) {
+ $product_name = $cgi->cookie('PRODUCT_DASHBOARD');
+ }
+
+ return if !$product_name;
+
+ # Do not use Bugzilla::Product::check_product() here, else the user
+ # could know whether the product doesn't exist or is not accessible.
+ my $product = new Bugzilla::Product({'name' => $product_name});
+
+ # We need to check and make sure that the user has permission
+ # to enter a bug against this product.
+ if (!$product || !$user->can_enter_product($product->name)) {
+ return;
+ }
+
+ # Remember selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => $product->name,
+ -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+
+ my $current_tab_name = $input->{'tab'} || "summary";
+ trick_taint($current_tab_name);
+ $vars->{'current_tab_name'} = $current_tab_name;
+
+ my $bug_status = trim($input->{'bug_status'} || 'open');
+
+ $vars->{'bug_status'} = $bug_status;
+ $vars->{'product'} = $product;
+ $vars->{'bug_link_all'} = bug_link_all($product);
+ $vars->{'bug_link_open'} = bug_link_open($product);
+ $vars->{'bug_link_closed'} = bug_link_closed($product);
+ $vars->{'total_bugs'} = total_bugs($product);
+ $vars->{'total_open_bugs'} = total_open_bugs($product);
+ $vars->{'total_closed_bugs'} = total_closed_bugs($product);
+ $vars->{'severities'} = get_legal_field_values('bug_severity');
+
+ $vars->{'open_bugs_percentage'} = int($vars->{'total_open_bugs'} / $vars->{'total_bugs'} * 100);
+ $vars->{'closed_bugs_percentage'} = int($vars->{'total_closed_bugs'} / $vars->{'total_bugs'} * 100);
+
+ if ($current_tab_name eq 'summary') {
+ $vars->{'by_priority'} = by_priority($product, $bug_status);
+ $vars->{'by_severity'} = by_severity($product, $bug_status);
+ $vars->{'by_assignee'} = by_assignee($product, $bug_status, 50);
+ $vars->{'by_status'} = by_status($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'recents') {
+ my $recent_days = $input->{'recent_days'} || 7;
+ (detaint_natural($recent_days) && $recent_days > 0 && $recent_days < 101)
+ || ThrowUserError('product_dashboard_invalid_recent_days');
+
+ my $params = {
+ product => $product,
+ days => $recent_days,
+ date_from => $input->{'date_from'} || '',
+ date_to => $input->{'date_to'} || '',
+ };
+
+ $vars->{'recently_opened'} = recently_opened($params);
+ $vars->{'recently_closed'} = recently_closed($params);
+ $vars->{'recent_days'} = $recent_days;
+ $vars->{'date_from'} = $input->{'date_from'};
+ $vars->{'date_to'} = $input->{'date_to'};
+ }
+
+ if ($current_tab_name eq 'components') {
+ if ($input->{'component'}) {
+ $vars->{'summary'} = by_value_summary($product, 'component', $input->{'component'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'component';
+ $vars->{'summary'}{'value'} = $input->{'component'};
+ }
+ elsif ($input->{'version'}) {
+ $vars->{'summary'} = by_value_summary($product, 'version', $input->{'version'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'version';
+ $vars->{'summary'}{'value'} = $input->{'version'};
+ }
+ elsif ($input->{'target_milestone'} && Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'summary'} = by_value_summary($product, 'target_milestone', $input->{'target_milestone'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'target_milestone';
+ $vars->{'summary'}{'value'} = $input->{'target_milestone'};
+ }
+ else {
+ $vars->{'by_component'} = by_component($product, $bug_status);
+ $vars->{'by_version'} = by_version($product, $bug_status);
+ if (Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'by_milestone'} = by_milestone($product, $bug_status);
+ }
+ }
+ }
+
+ if ($current_tab_name eq 'duplicates') {
+ $vars->{'by_duplicate'} = by_duplicate($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'popularity') {
+ $vars->{'by_popularity'} = by_popularity($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'roadmap') {
+ foreach my $milestone (@{$product->milestones}){
+ my %milestone_stats;
+ $milestone_stats{'name'} = $milestone->name;
+ $milestone_stats{'total_bugs'} = total_bug_milestone($product, $milestone);
+ $milestone_stats{'open_bugs'} = bug_milestone_by_status($product, $milestone, 'open');
+ $milestone_stats{'closed_bugs'} = bug_milestone_by_status($product, $milestone, 'closed');
+ $milestone_stats{'link_total'} = bug_milestone_link_total($product, $milestone);
+ $milestone_stats{'link_open'} = bug_milestone_link_open($product, $milestone);
+ $milestone_stats{'link_closed'} = bug_milestone_link_closed($product, $milestone);
+ $milestone_stats{'percentage'} = $milestone_stats{'total_bugs'}
+ ? int(($milestone_stats{'closed_bugs'} / $milestone_stats{'total_bugs'}) * 100)
+ : 0;
+ push (@{$vars->{'by_roadmap'}}, \%milestone_stats);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
+
diff --git a/extensions/ProductDashboard/lib/Queries.pm b/extensions/ProductDashboard/lib/Queries.pm
new file mode 100644
index 000000000..b58298ea8
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Queries.pm
@@ -0,0 +1,475 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Queries;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Queries::EXPORT = qw(
+ total_bugs
+ total_open_bugs
+ total_closed_bugs
+ by_version
+ by_value_summary
+ by_milestone
+ by_priority
+ by_severity
+ by_component
+ by_assignee
+ by_status
+ by_duplicate
+ by_popularity
+ recently_opened
+ recently_closed
+ total_bug_milestone
+ bug_milestone_by_status
+);
+
+use Bugzilla::CGI;
+use Bugzilla::User;
+use Bugzilla::Util;
+use Bugzilla::Component;
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+
+use Bugzilla::Extension::ProductDashboard::Util qw(open_states closed_states
+ quoted_open_states quoted_closed_states);
+
+sub total_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?", undef, $product->id);
+}
+
+sub total_open_bugs {
+ my $product = shift;
+ my $bug_status = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN (" . join(',', quoted_open_states()) . ")
+ AND product_id = ?", undef, $product->id);
+}
+
+sub total_closed_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN (" . join(',', quoted_closed_states()) . ")
+ AND product_id = ?", undef, $product->id);
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__closed__";
+}
+
+sub by_version {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY version
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_milestone {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY target_milestone
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_priority {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY priority
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_severity {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY bug_severity
+ ORDER BY COUNT(bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_component {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs INNER JOIN components ON bugs.component_id = components.id
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY components.name
+ ORDER BY COUNT(bugs.bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub by_value_summary {
+ my ($product, $type, $value, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = "SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id ";
+
+ if ($type eq 'component') {
+ Bugzilla::Component->check({ product => $product, name => $value });
+ $query .= "AND components.name = ? " if $type eq 'component';
+ }
+ elsif ($type eq 'version') {
+ Bugzilla::Version->check({ product => $product, name => $value });
+ $query .= "AND bugs.version = ? " if $type eq 'version';
+ }
+ elsif ($type eq 'target_milestone') {
+ Bugzilla::Milestone->check({ product => $product, name => $value });
+ $query .= "AND bugs.target_milestone = ? " if $type eq 'target_milestone';
+ }
+
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ") " if $bug_status eq 'open';
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ") " if $bug_status eq 'closed';
+
+ trick_taint($value);
+
+ my $past_due_bugs = $dbh->selectall_arrayref($query .
+ "AND (bugs.deadline IS NOT NULL AND bugs.deadline != '')
+ AND bugs.deadline < now() ORDER BY bugs.deadline LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $updated_recently_bugs = $dbh->selectall_arrayref($query .
+ "AND bugs.delta_ts != bugs.creation_ts " .
+ "ORDER BY bugs.delta_ts DESC LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $timestamp = $dbh->selectrow_array("SELECT " . $dbh->sql_date_format("LOCALTIMESTAMP(0)", "%Y-%m-%d"));
+
+ return {
+ timestamp => $timestamp,
+ past_due => _filter_bugs($past_due_bugs),
+ updated_recently => _filter_bugs($updated_recently_bugs),
+ };
+}
+
+sub by_assignee {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1], $_->[2] ] }
+ @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs, profiles
+ WHERE bugs.product_id = ?
+ AND bugs.assigned_to = profiles.userid
+ $extra
+ GROUP BY profiles.login_name
+ ORDER BY COUNT(bugs.bug_id) DESC $limit",
+ undef, $product->id, $product->id)};
+
+ return \@result;
+}
+
+sub by_status {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id),
+ ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100))
+ FROM bugs
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY bugs.bug_status
+ ORDER BY COUNT(bugs.bug_id) DESC",
+ undef, $product->id, $product->id);
+}
+
+sub total_bug_milestone {
+ my ($product, $milestone) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ?",
+ undef, $milestone->name, $product->id);
+}
+
+sub bug_milestone_by_status {
+ my ($product, $milestone, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra = '';
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ? $extra",
+ undef,
+ $milestone->name,
+ $product->id);
+
+}
+
+sub by_duplicate {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my $extra = '';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ COUNT(duplicates.dupe) AS dupe_count
+ FROM bugs, duplicates, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_id = duplicates.dupe_of
+ $extra
+ GROUP BY bugs.bug_id, bugs.bug_status, components.name,
+ bugs.bug_severity, bugs.short_desc
+ HAVING COUNT(duplicates.dupe) > 1
+ ORDER BY COUNT(duplicates.dupe) DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub by_popularity {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my $extra = '';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ bugs.votes AS votes
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.votes > 1
+ $extra
+ ORDER BY bugs.votes DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_opened {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= CURRENT_DATE() - INTERVAL ? DAY";
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_closed {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs_activity.bug_when >= ? AND bugs_activity.bug_when <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs_activity.bug_when >= CURRENT_DATE() - INTERVAL ? DAY";
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT DISTINCT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components, bugs_activity
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")
+ AND bugs.bug_id = bugs_activity.bug_id
+ AND bugs_activity.added IN (" . join(',', quoted_closed_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub _filter_bugs {
+ my ($unfiltered_bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return [] if !$unfiltered_bugs;
+
+ my @unfiltered_bug_ids = map { $_->{'id'} } @$unfiltered_bugs;
+ my %filtered_bug_ids = map { $_ => 1 } @{ Bugzilla->user->visible_bugs(\@unfiltered_bug_ids) };
+
+ my @filtered_bugs;
+ foreach my $bug (@$unfiltered_bugs) {
+ next if !$filtered_bug_ids{$bug->{'id'}};
+ push(@filtered_bugs, $bug);
+ }
+
+ return \@filtered_bugs;
+}
+
+1;
diff --git a/extensions/ProductDashboard/lib/Util.pm b/extensions/ProductDashboard/lib/Util.pm
new file mode 100644
index 000000000..5d9c161ef
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Util.pm
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Util;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Util::EXPORT = qw(
+ bug_link_all
+ bug_link_open
+ bug_link_closed
+ open_states
+ closed_states
+ quoted_open_states
+ quoted_closed_states
+ bug_milestone_link_total
+ bug_milestone_link_open
+ bug_milestone_link_closed
+);
+
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+our $_open_states;
+sub open_states {
+ $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 });
+ return wantarray ? @$_open_states : $_open_states;
+}
+
+our $_quoted_open_states;
+sub quoted_open_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ];
+ return wantarray ? @$_quoted_open_states : $_quoted_open_states;
+}
+
+our $_closed_states;
+sub closed_states {
+ $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 });
+ return wantarray ? @$_closed_states : $_closed_states;
+}
+
+our $_quoted_closed_states;
+sub quoted_closed_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ];
+ return wantarray ? @$_quoted_closed_states : $_quoted_closed_states;
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__closed__";
+}
+
+sub bug_milestone_link_total {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name);
+}
+
+sub bug_milestone_link_open {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__open__";
+}
+
+sub bug_milestone_link_closed {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__closed__";
+}
+
+1;
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
new file mode 100644
index 000000000..e9be8a13d
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+ <li><span class="separator"> | </span><a href="page.cgi?id=productdashboard.html">Product Dashboard</a></li>
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..d8af64d31
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "product_dashboard_invalid_recent_days" %]
+ [% title = "Invalid Recent Days" %]
+ Invalid value for recent days.
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
new file mode 100644
index 000000000..f48d8f812
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
@@ -0,0 +1,234 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% javascript_urls = [ "js/util.js", "js/field.js" ] %]
+
+[% IF current_tab_name == 'summary' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/summary.js") %]
+ [% ELSIF current_tab_name == 'recents' %]
+ [% yui = [ "calendar" ] %]
+ [% javascript_urls.push("js/field.js") %]
+ [% javascript_urls.push("js/util.js") %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/recents.js") %]
+[% ELSIF current_tab_name == 'components' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/components.js") %]
+[% ELSIF current_tab_name == 'duplicates' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/duplicates.js") %]
+[% ELSIF current_tab_name == 'popularity' %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/popularity.js") %]
+[% ELSIF current_tab_name == 'roadmap' && Param('usetargetmilestone') %]
+ [% javascript_urls.push("extensions/ProductDashboard/web/js/roadmap.js") %]
+[% END %]
+
+[% filtered_product = product.name FILTER html %]
+[% PROCESS global/header.html.tmpl
+ title = "Product Dashboard: $filtered_product"
+ style_urls = [ "skins/standard/buglist.css",
+ "js/yui/assets/skins/sam/paginator.css",
+ "extensions/ProductDashboard/web/styles/productdashboard.css" ]
+%]
+
+<script type="text/javascript">
+<!--
+ PD = {};
+ [%# Set up severities list for proper sorting %]
+ PD.severities = new Array();
+ [% sort_count = 0 %]
+ [% FOREACH s = severities %]
+ PD.severities['[% s FILTER js %]'] = [% sort_count FILTER js %];
+ [% sort_count = sort_count + 1 %]
+ [% END %]
+-->
+</script>
+
+[% url_filtered_product = product.name FILTER uri %]
+[% url_filtered_status = bug_status FILTER uri %]
+
+[% tabs = [
+ {
+ name => "summary",
+ label => "Summary",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=summary"
+ },
+ {
+ name => "recents",
+ label => "Recents",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=recents"
+ },
+ {
+ name => "components",
+ label => "Components/Versions",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=components"
+ },
+ {
+ name => "duplicates",
+ label => "Duplicates",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=duplicates"
+ },
+ {
+ name => "roadmap",
+ label => "Road Map",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap"
+ },
+ ]
+%]
+
+[% IF product.votesperuser %]
+ [%
+ tabs.push({
+ name => "popularity",
+ label => "Popularity",
+ link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity"
+ })
+ %]
+[% END %]
+
+[% FOREACH tab IN tabs %]
+ [% IF tab.name == current_tab_name %]
+ [% current_tab = tab %]
+ [% LAST %]
+ [% END %]
+[% END %]
+
+[% full_bug_count = 0 %]
+[% IF bug_status == 'open' %]
+ [% full_bug_count = total_open_bugs %]
+[% ELSIF bug_status == 'closed' %]
+ [% full_bug_count = total_closed_bugs %]
+[% ELSE %]
+ [% full_bug_count = total_bugs %]
+[% END %]
+
+[% bug_link = bug_link_all %]
+[% IF bug_status == 'open' %]
+ [% bug_link = bug_link_open %]
+[% ELSIF bug_status == 'closed' %]
+ [% bug_link = bug_link_closed %]
+[% END %]
+
+<div class="yui3-skin-sam">
+ <a name="top"></a>
+
+ <form action="page.cgi" method="get">
+ <input type="hidden" name="id" value="productdashboard.html">
+ <input type="hidden" name="tab" value="[% current_tab.name FILTER html %]">
+
+ [% IF summary.keys %]
+ <input type="hidden" name="[% summary.type FILTER html %]" value="[% summary.value FILTER html %]">
+ [% END %]
+
+ [% IF product %]
+ <span id="product_dashboard_links">
+ <ul>
+ <li><a href="[% urlbase FILTER none %]enter_bug.cgi?product=[% product.name FILTER uri %]">
+ Create a new [% terms.bug %] in this product</a></li>
+ <li><a href="[% urlbase FILTER none %]describecomponents.cgi?product=[% product.name FILTER uri %]">
+ Show full component descriptions for this product</a></li>
+ </ul>
+ </span>
+ [% END %]
+
+ <strong>Choose product:</strong>
+ <select name="product">
+ [% FOREACH c = classifications %]
+ <optgroup label="[% c.name FILTER html %]">
+ [% FOREACH p = c.products %]
+ <option value="[% p.name FILTER html %]"
+ [% IF p.name == product.name %]selected="selected"[% END %]>
+ [% p.name FILTER html %]</option>
+ [% END %]</optgroup>
+ [% END %]
+ </select>
+ <select name="bug_status" id="bug_status">
+ [% statuses = [ { name = 'open', label = "Open $terms.Bugs" },
+ { name = 'closed', label = "Closed $terms.Bugs" },
+ { name = 'all', label = "All $terms.Bugs" } ] %]
+ [% FOREACH status = statuses %]
+ <option value="[% status.name FILTER html %]"
+ [% " selected" IF bug_status == "${status.name}" %]>
+ [% status.label FILTER html %]
+ </option>
+ [% END %]
+ </select>
+
+ <input type="submit" value="[% IF product %]Change[% ELSE %]Submit[% END %]">
+
+ [% IF product %]
+ <div class="product_name">
+ [% product.name FILTER html %]
+ </div>
+
+ <div class="product_description">
+ [% product.description FILTER none %]
+ </div>
+
+ [% WRAPPER global/tabs.html.tmpl
+ tabs = tabs
+ current_tab = current_tab
+ %]
+
+ [% IF current_tab.name == 'summary' %]
+ [% PROCESS pages/productdashboard/summary.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'recents' %]
+ [% PROCESS pages/productdashboard/recents.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'components' %]
+ [% PROCESS pages/productdashboard/components.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'duplicates' %]
+ [% PROCESS pages/productdashboard/duplicates.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'popularity' %]
+ [% PROCESS pages/productdashboard/popularity.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'roadmap' && Param('usetargetmilestone') %]
+ [% PROCESS pages/productdashboard/roadmap.html.tmpl %]
+ [% END %]
+
+ [% END %][%# END WRAPPER %]
+ [% END %]
+
+ </form>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK bar_graph %]
+ [% IF full_bug_count > 0 %][%# No divide by zero %]
+ [% percentage_bugs = (count / full_bug_count) * 100 FILTER format('%02.2f') %]
+ [% ELSE %]
+ [% percentage_bugs = 0 %]
+ [% END %]
+ <div class="bar_graph">
+ <table cellpadding="0" cellspacing="0" width="300px">
+ <tr>
+ <td width="[% percentage_bugs FILTER html %]%">
+ <table cellpadding="0" cellspacing="0" width="100%">
+ <tr>
+ <td bgcolor="#3c78b5">
+ <a title="[% percentage_bugs FILTER html %]%">
+ <img src="extensions/ProductDashboard/web/images/spacer.gif" height=10 width="100%" title="[% percentage_bugs FILTER html %]%">
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td width="[% 100 - percentage_bugs FILTER html %]%">&nbsp;&nbsp;&nbsp;[% percentage_bugs FILTER html %]%</td>
+ </tr>
+ </table>
+ </div>
+[% END %]
+
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
new file mode 100644
index 000000000..6b0e7240a
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
@@ -0,0 +1,146 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF summary.keys %]
+
+<h3>Summary for [% summary.type FILTER html %]: [% summary.value FILTER html %]</h3>
+
+<script>
+<!--
+ // Past due
+ [% IF user.is_timetracker %]
+ PD.past_due = [
+ [% FOREACH bug = summary.past_due %]
+ {
+ id: '[% bug.id FILTER js %]',
+ bug_status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+ [% END %]
+
+ // Updated recently
+ PD.updated_recently = [
+ [% FOREACH bug = summary.updated_recently %]
+ {
+ id: '[% bug.id FILTER js %]',
+ bug_status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+-->
+</script>
+
+[% IF user.is_timetracker %]
+ <p>
+ <a href="#past_due">Past Due</a> |
+ <a href="#updated_recently">Updated Recently</a>
+ </p>
+[% END %]
+
+<div class="yui3-skin-sam">
+
+ [% IF user.is_timetracker %]
+ <a name="past_due"></a>
+ <b>[% summary.past_due.size FILTER html %] Past Due [% terms.Bugs %]</b> (deadline is before today's date)
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&field0-0-0=deadline&type0-0-0=lessthan&value0-0-0=[% summary.timestamp FILTER uri %]&order=deadline">full list</a>)
+ <div id="past_due"></div>
+ <br>
+ [% END %]
+
+ <a name="updated_recently"></a>
+ <b>[% summary.updated_recently.size FILTER html %] Most Recently Updated [% terms.Bugs %]</b>
+ [% IF user.is_timetracker %](<a href="#top">back to top</a>)[% END %]
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&order=changeddate DESC">full list</a>)
+ <div id="updated_recently"></div>
+</div>
+
+[% ELSE %]
+
+<script type="text/javascript">
+<!--
+ PD.product_name = '[% product.name FILTER js %]';
+ PD.bug_status = '[% bug_status FILTER js %]';
+
+ // Component counts
+ PD.component_counts = [
+ [% FOREACH col = by_component %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;component=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Version counts
+ PD.version_counts = [
+ [% FOREACH col = by_version %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;version=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ [% IF Param('usetargetmilestone') %]
+ // Milestone counts
+ PD.milestone_counts = [
+ [% FOREACH col = by_milestone %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER html %]&amp;target_milestone=[% col.0 FILTER uri %]">Link</a>'
+ },
+ [% END %]
+ ];
+ [% END %]
+-->
+</script>
+
+<h3>[% terms.Bug %] counts per component, version and milestone.</h3>
+
+<p>
+ <a href="#component">Component</a> |
+ <a href="#version">Version</a> |
+ <a href="#milestone">Milestone</a>
+</p>
+
+<p>Click on a value to show a list of most recently updated [% terms.bugs %].</p>
+
+<div class="yui3-skin-sam">
+ <a name="component"></a>
+ <b>Component</b>
+ <div id="component_counts"></div>
+ <br>
+ <a name="version"></a>
+ <b>Version</b>
+ (<a href="#top">back to top</a>)
+ <div id="version_counts"></div>
+ [% IF Param('usetargetmilestone') %]
+ <br>
+ <a name="milestone"></a>
+ <b>Milestone</b>
+ (<a href="#top">back to top</a>)
+ <div id="milestone_counts"></div>
+ [% END %]
+</div>
+
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
new file mode 100644
index 000000000..585cdc829
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
@@ -0,0 +1,34 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+ PD.duplicates = [
+ [% FOREACH bug = by_duplicate %]
+ {
+ id: '[% bug.id FILTER js %]',
+ count: '[% bug.dupe_count FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most duplicated [% terms.bugs %]</h3>
+
+[% IF by_duplicate.size %]
+ <b>[% by_duplicate.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui3-skin-sam">
+ <div id="duplicates"></div>
+ </div>
+[% ELSE %]
+ <b>No duplicate [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
new file mode 100644
index 000000000..933f26c81
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
@@ -0,0 +1,38 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+</style>
+
+<script type="text/javascript">
+ PD.popularity = [
+ [% FOREACH bug = by_popularity %]
+ {
+ id: '[% bug.id FILTER js %]',
+ count: '[% bug.votes FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most voted on [% terms.bugs %]</h3>
+
+[% IF by_popularity.size %]
+ <b>[% by_popularity.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui3-skin-sam">
+ <div id="popularity"></div>
+ </div>
+[% ELSE %]
+ <b>No [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
new file mode 100644
index 000000000..66320e174
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
@@ -0,0 +1,87 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+ PD.recents = {};
+
+ // Recently opened
+ PD.recents.opened = [
+ [% FOREACH bug = recently_opened %]
+ {
+ id: '[% bug.id FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+
+ // Recently closed
+ PD.recents.closed = [
+ [% FOREACH bug = recently_closed %]
+ {
+ id: '[% bug.id FILTER js %]',
+ status: '[% bug.status FILTER js %]',
+ version: '[% bug.version FILTER js %]',
+ component: '[% bug.component FILTER js %]',
+ severity: '[% bug.severity FILTER js %]',
+ summary: '[% bug.summary FILTER js %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Most recently opened and closed [% terms.bugs %]</h3>
+
+<p>
+ Activity within the last <input type="text" size="4" name="recent_days"
+ value="[% recent_days FILTER html %]">
+ days (between 1 and 100) or from
+ <input name="date_from" size="10" id="date_from"
+ value="[% date_from FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_from"
+ onclick="showCalendar('date_from')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_from"></span>
+ to
+ <input name="date_to" size="10" id="date_to"
+ value="[% date_to FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_to"
+ onclick="showCalendar('date_to')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_to"></span>
+ <script type="text/javascript">
+ createCalendar('date_from')
+ createCalendar('date_to')
+ </script>
+ <input type="submit" name="change" value="Change">
+</p>
+<p>
+ <a href="#recently_opened">Recently Opened</a>
+ <span class="separator"> | </span>
+ <a href="#recently_closed">Recently Closed</a>
+</p>
+
+<div class="yui-skin-sam">
+ <a name="recently_opened"></a>
+ <b>[% recently_opened.size FILTER html %] Recently Opened [% terms.Bugs %]</b>
+ <div id="recently_opened"></div>
+ <br>
+ <a name="recently_closed"></a>
+ <b>[% recently_closed.size FILTER html %] Recently Closed [% terms.Bugs %]</b>
+ (<a href="#top">back to top</a>)
+ <div id="recently_closed"></div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
new file mode 100644
index 000000000..b31827fbd
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+<!--
+ PD.roadmap = [
+ [% FOREACH milestone = by_roadmap %]
+ {
+ name: '[% milestone.name FILTER js %]',
+ percentage: '[% milestone.percentage FILTER js %]',
+ link: '<a href="[% milestone.link_closed FILTER html %]">[% milestone.closed_bugs FILTER html %]</a> of <a href="[% milestone.link_total FILTER html %]"> [% milestone.total_bugs FILTER html %]</a> [% terms.bugs %] have been closed',
+ },
+ [% END %]
+ ];
+-->
+</script>
+
+<h3>Percentage of [% terms.bug %] closure per milestone</h3>
+
+<div class="yui3-skin-sam">
+ <div id="bug_milestones"></div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
new file mode 100644
index 000000000..5afba25a9
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
@@ -0,0 +1,122 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script>
+ PD.summary = {};
+
+ // global counts
+ PD.summary.bug_counts = [
+ {
+ name: "Total [% terms.Bugs %]",
+ count: [% total_bugs || 0 FILTER js %],
+ percentage: 100,
+ link: '<a href="[% bug_link_all FILTER js %]">Link</a>',
+ },
+ {
+ name: "Open [% terms.Bugs %]",
+ count: [% total_open_bugs || 0 FILTER js %],
+ percentage: [% open_bugs_percentage FILTER js %],
+ link: '<a href="[% bug_link_open FILTER js %]">Link</a>',
+ },
+ {
+ name: "Closed [% terms.Bugs %]",
+ count: [% total_closed_bugs || 0 FILTER js %],
+ percentage: [% closed_bugs_percentage FILTER js %],
+ link: '<a href="[% bug_link_closed FILTER js %]">Link</a>',
+ }
+ ];
+
+ // Status counts
+ PD.summary.status_counts = [
+ [% FOREACH col = by_status %]
+ [% NEXT IF col.0 == 'CLOSED' %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link_all FILTER js %]&amp;bug_status=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Priority counts
+ PD.summary.priority_counts = [
+ [% FOREACH col = by_priority %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER js %]&amp;priority=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Severity counts
+ PD.summary.severity_counts = [
+ [% FOREACH col = by_severity %]
+ {
+ name: "[% col.0 FILTER js %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '<a href="[% bug_link FILTER js %]&amp;bug_severity=[% col.0 FILTER uri FILTER js %]">Link</a>'
+ },
+ [% END %]
+ ];
+
+ // Assignee counts
+ PD.summary.assignee_counts = [
+ [% FOREACH col = by_assignee %]
+ {
+ name: "[% IF user.id %][% col.0.email FILTER js %][% ELSE %][% col.0.realname || 'No Name' FILTER js %][% END %]",
+ count: [% col.1 || 0 FILTER js %],
+ percentage: [% col.2 || 0 FILTER js %],
+ link: '[% IF user.id %]<a href="[% bug_link FILTER js %]&amp;emailassigned_to1=1&amp;emailtype1=exact&amp;email1=[% col.0.email FILTER uri FILTER js %]">Link</a>[% END %]'
+ },
+ [% END %]
+ ];
+</script>
+
+<h3>Summary of [% terms.bug %] counts</h3>
+
+<p>
+ <a href="#counts">Counts</a>
+ <span class="separator"> | </span>
+ <a href="#status">Status</a>
+ <span class="separator"> | </span>
+ <a href="#priority">Priority</a>
+ <span class="separator"> | </span>
+ <a href="#severity">Severity</a>
+ <span class="separator"> | </span>
+ <a href="#assignee">Assignee</a>
+</p>
+
+<div class="yui3-skin-sam">
+ <a name="counts"></a>
+ <b>[% terms.Bug %] Counts</b>
+ <div id="bug_counts"></div>
+ <br>
+ <a name="status"></a>
+ <b>Status</b>
+ (<a href="#top">back to top</a>)
+ <div id="status_counts"></div>
+ <br>
+ <a name="priority"></a>
+ <b>Priority</b>
+ (<a href="#top">back to top</a>)
+ <div id="priority_counts"></div>
+ <br>
+ <a name="severity"></a>
+ <b>Severity</b>
+ (<a href="#top">back to top</a>)
+ <div id="severity_counts"></div>
+ <br>
+ <a name="assignee"></a>
+ <b>Assignee</b>
+ (<a href="#top">back to top</a>)
+ <div id="assignee_counts"></div>
+</div>
diff --git a/extensions/ProductDashboard/web/images/spacer.gif b/extensions/ProductDashboard/web/images/spacer.gif
new file mode 100644
index 000000000..fc2560981
--- /dev/null
+++ b/extensions/ProductDashboard/web/images/spacer.gif
Binary files differ
diff --git a/extensions/ProductDashboard/web/js/components.js b/extensions/ProductDashboard/web/js/components.js
new file mode 100644
index 000000000..538b15457
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/components.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function(Y) {
+ if (typeof PD.updated_recently != 'undefined') {
+ var columns = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var updatedRecentlyDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.updated_recently
+ });
+ updatedRecentlyDataTable.render("#updated_recently");
+
+ if (typeof PD.past_due != 'undefined') {
+ var pastDueDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.past_due
+ });
+ pastDueDataTable.render('#past_due');
+ }
+ }
+
+ if (typeof PD.component_counts != 'undefined') {
+ var summary_url = '<a href="page.cgi?id=productdashboard.html&amp;product=' +
+ encodeURIComponent(PD.product_name) + '&bug_status=' +
+ encodeURIComponent(PD.bug_status) + '&tab=components';
+
+ var columns = [
+ { key:"name", label:"Name", sortable:true, allowHTML: true,
+ formatter: summary_url + '&component={value}">{value}</a>' },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key:"link", label:"Link", sortable:false, allowHTML: true }
+ ];
+
+ var componentsDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.component_counts
+ });
+ componentsDataTable.render("#component_counts");
+
+ columns[0].formatter = summary_url + '&version={value}">{value}</a>';
+ var versionsDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.version_counts
+ });
+ versionsDataTable.render('#version_counts');
+
+ if (typeof PD.milestone_counts != 'undefined') {
+ columns[0].formatter = summary_url + '&target_milestone={value}">{value}</a>';
+ var milestonesDataTable = new Y.DataTable({
+ columns: columns,
+ data: PD.milestone_counts
+ });
+ milestonesDataTable.render('#milestone_counts');
+ }
+ }
+});
diff --git a/extensions/ProductDashboard/web/js/duplicates.js b/extensions/ProductDashboard/web/js/duplicates.js
new file mode 100644
index 000000000..5e3193a65
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/duplicates.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"count", label:"Count", sortable:true },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var duplicatesDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.duplicates
+ }).render('#duplicates');
+});
diff --git a/extensions/ProductDashboard/web/js/popularity.js b/extensions/ProductDashboard/web/js/popularity.js
new file mode 100644
index 000000000..b78b67867
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/popularity.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"count", label:"Count", sortable:true },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var popularityDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.popularity
+ }).render('#popularity');
+});
diff --git a/extensions/ProductDashboard/web/js/recents.js b/extensions/ProductDashboard/web/js/recents.js
new file mode 100644
index 000000000..84e1758b6
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/recents.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, allowHTML: true,
+ formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' },
+ { key:"status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"severity", label:"Severity", sortable:true },
+ { key:"summary", label:"Summary", sortable:false },
+ ];
+
+ var recentlyOpenedDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.recents.opened
+ }).render('#recently_opened');
+
+ var recentlyClosedDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.recents.closed
+ }).render('#recently_closed');
+});
diff --git a/extensions/ProductDashboard/web/js/roadmap.js b/extensions/ProductDashboard/web/js/roadmap.js
new file mode 100644
index 000000000..1bef5b091
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/roadmap.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'percentage', label: 'Percentage', sortable: false, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key: 'link', label: 'Links', allowHTML: true, sortable: false }
+ ];
+
+ var roadmapDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.roadmap,
+ }).render('#bug_milestones');
+});
diff --git a/extensions/ProductDashboard/web/js/summary.js b/extensions/ProductDashboard/web/js/summary.js
new file mode 100644
index 000000000..59d000d7b
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/summary.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("datatable", "datatable-sort", function (Y) {
+ var column_defs = [
+ { key: 'name', label: 'Name', sortable: true },
+ { key: 'count', label: 'Count', sortable: true },
+ { key: 'percentage', label: 'Percentage', sortable: true, allowHTML: true,
+ formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' },
+ { key: 'link', label: 'Link', allowHTML: true }
+ ];
+
+ var bugsCountDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.bug_counts
+ }).render('#bug_counts');
+
+ var statusCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.status_counts
+ }).render('#status_counts');
+
+ var priorityCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.priority_counts
+ }).render('#priority_counts');
+
+ var severityCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.severity_counts
+ }).render('#severity_counts');
+
+ var assigneeCountsDataTable = new Y.DataTable({
+ columns: column_defs,
+ data: PD.summary.assignee_counts
+ }).render('#assignee_counts');
+});
diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css
new file mode 100644
index 000000000..c0c45cf38
--- /dev/null
+++ b/extensions/ProductDashboard/web/styles/productdashboard.css
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#product_dashboard_links {
+ float: right;
+ padding-right: 25px;
+ border: 1px solid rgb(116, 126, 147);
+}
+
+.product_name {
+ font-size: 2em;
+ margin: 10px 0 10px 0;
+ color: rgb(109, 117, 129);
+}
+
+.product_description {
+ font-size: 90%;
+ font-style: italic;
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+}
+
+.percentage {
+ position:relative;
+ width: 200px;
+ border: 1px solid rgb(203, 203, 203);
+ position: relative;
+ padding: 3px;
+}
+
+.bar{
+ background-color: #00ff00;
+ height: 20px;
+}
+
+.percent{
+ position: absolute;
+ display: inline-block;
+ top: 3px;
+ left: 48%;
+}