summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extensions/ProductDashboard/Config.pm14
-rw-r--r--extensions/ProductDashboard/Extension.pm189
-rw-r--r--extensions/ProductDashboard/lib/Queries.pm463
-rw-r--r--extensions/ProductDashboard/lib/Util.pm96
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl9
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl209
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl259
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl73
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl73
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl132
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl54
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl205
-rw-r--r--extensions/ProductDashboard/web/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--extensions/ProductDashboard/web/js/productdashboard.js94
-rw-r--r--extensions/ProductDashboard/web/styles/productdashboard.css12
-rw-r--r--template/en/default/global/common-links.html.tmpl2
16 files changed, 1884 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..08755da2d
--- /dev/null
+++ b/extensions/ProductDashboard/Extension.pm
@@ -0,0 +1,189 @@
+# 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.
+ my @enterable_products = @{$user->get_enterable_products};
+ ThrowUserError('no_products') unless scalar(@enterable_products);
+
+ my $classification = Bugzilla->params->{'useclassification'} ?
+ $input->{'classification'} : '__all';
+
+ # 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 (!$user->can_enter_product($product ? $product->name : $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');
+
+ 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);
+ $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);
+ 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..43d799111
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Queries.pm
@@ -0,0 +1,463 @@
+# 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::Search;
+use Bugzilla::Util;
+use Bugzilla::Component;
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+
+use Bugzilla::Extension::ProductDashboard::Util qw(open_states 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 (" . 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 ('CLOSED')
+ 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 (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY version
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_milestone {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY target_milestone
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_priority {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY priority
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_severity {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY bug_severity
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_component {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id)
+ 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);
+}
+
+sub by_value_summary {
+ my ($product, $type, $value, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ 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 (" . open_states() . ") " if $bug_status eq 'open';
+ $query .= "AND bugs.bug_status IN (" . 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) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1] ] }
+ @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id)
+ 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", undef, $product->id)};
+
+ return \@result;
+}
+
+sub by_status {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id)
+ FROM bugs
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY bugs.bug_status
+ ORDER BY COUNT(bugs.bug_id) DESC", undef, $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 (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . 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 = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my $extra;
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . 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 = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my $extra;
+ $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . 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 = 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, $date_from, $date_to);
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?');
+ 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 (" . 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 = 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, $date_from, $date_to);
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?');
+ 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 (" . closed_states() . ")
+ AND bugs.bug_id = bugs_activity.bug_id
+ AND bugs_activity.added IN (" . 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..ad46ef8f6
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Util.pm
@@ -0,0 +1,96 @@
+# 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
+ filter_bugs
+ bug_milestone_link_total
+ bug_milestone_link_open
+ bug_milestone_link_closed
+);
+
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+sub open_states {
+ my $dbh = Bugzilla->dbh;
+ return join(",", map { $dbh->quote($_) } BUG_STATE_OPEN);
+}
+
+sub closed_states {
+ my $dbh = Bugzilla->dbh;
+ return join(",", map { $dbh->quote($_->name) } closed_bug_statuses());
+}
+
+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__";
+}
+
+sub filter_bugs {
+ my ($unfiltered_bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # Filter out which bugs that cannot be viewed
+ my $params = Bugzilla::CGI->new({ bug_id => [ map { $_->{'id'} } @$unfiltered_bugs ] });
+ my $search = Bugzilla::Search->new(fields => ['bug_id' ], params => $params );
+ my %filtered_bug_ids = map { $_ => 1 } @{$dbh->selectcol_arrayref($search->getSQL())};
+
+ 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/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/pages/productdashboard.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
new file mode 100644
index 000000000..a319060d1
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
@@ -0,0 +1,209 @@
+[%# 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 %]
+
+[% 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" ]
+ yui = [ "datatable", "paginator", "calendar" ]
+ javascript_urls = [ "js/util.js", "js/field.js",
+ "extensions/ProductDashboard/web/js/productdashboard.js" ]
+%]
+
+<script type="text/javascript">
+<!--
+ [%# Set up severities list for proper sorting %]
+ var severities = new Array();
+ [% sort_count = 0 %]
+ [% FOREACH s = severities %]
+ 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&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=summary"
+ },
+ {
+ name => "recents",
+ label => "Recents",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=recents"
+ },
+ {
+ name => "components",
+ label => "Components/Versions",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=components"
+ },
+ {
+ name => "duplicates",
+ label => "Duplicates",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=duplicates"
+ },
+ {
+ name => "popularity",
+ label => "Popularity",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity"
+ },
+ {
+ name => "roadmap",
+ label => "Road Map",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap"
+ },
+ ]
+%]
+
+[% 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="yui-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 %]
+
+ <h3>Product: [% product.name FILTER html %]</h3>
+ <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 %]
+
+ <p><i>[% product.description FILTER none %]</i></p>
+
+ [% WRAPPER global/tabs.html.tmpl
+ tabs = tabs
+ current_tab = current_tab
+ %]
+
+ <h3>[% current_tab.label FILTER html %]</h3>
+
+ [% 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..7f5a05568
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
@@ -0,0 +1,259 @@
+[%# 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>
+
+ <style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+ </style>
+
+ <script type="text/javascript">
+ <!--
+ var column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+ ];
+ var fields = [
+ { key:"id" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+ ];
+ [% IF user.is_timetracker %]
+ addStatListener("past_due", "past_due_table", column_defs, fields, {
+ [% IF summary.past_due.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %]
+ });
+ [% END %]
+ addStatListener("updated_recently", "updated_recently_table", column_defs, fields, {
+ [% IF summary.updated_recently.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% 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="yui-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">
+ <table id="past_due_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = summary.past_due %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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">
+ <table id="updated_recently_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = summary.updated_recently %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+[% ELSE %]
+
+ [% summary_url = "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=components" %]
+
+ <script type="text/javascript">
+ <!--
+ var column_defs = [
+ { key:"name", label:"Name", sortable:true },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false },
+ { key:"bug_list", label:"[% terms.Bug %] List", sortable:false }
+ ];
+ var fields = [
+ { key:"name" },
+ { key:"count", parser:"number" },
+ { key:"percentage" },
+ { key:"bug_list" }
+ ];
+ addStatListener("component_counts", "component_counts_table", column_defs, fields, {});
+ addStatListener("version_counts", "version_counts_table", column_defs, fields, {});
+ addStatListener("milestone_counts", "milestone_counts_table", column_defs, fields, {});
+ -->
+ </script>
+
+ <p>
+ <a href="#component">Component</a> |
+ <a href="#version">Version</a> |
+ <a href="#milestone">Milestone</a>
+ </p>
+
+ <p>Click on value to show additional information</p>
+
+ <div class="yui-skin-sam">
+ <a name="component"></a>
+ <b>Component</b>
+ <div id="component_counts">
+ <table id="component_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_component %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&component=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;component=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="version"></a>
+ <b>Version</b>
+ (<a href="#top">back to top</a>)
+ <div id="version_counts">
+ <table id="version_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_version %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&version=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;version=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+
+ [% IF Param('usetargetmilestone') %]
+ <br>
+ <a name="milestone"></a>
+ <b>Milestone</b>
+ (<a href="#top">back to top</a>)
+ <div id="milestone_counts">
+ <table id="milestone_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_milestone %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&target_milestone=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;target_milestone=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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..36a820300
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
@@ -0,0 +1,73 @@
+[%# 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">
+<!--
+var column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } },
+ { key:"count", label:"Count", sortable:true },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+var fields = [
+ { key:"id" },
+ { key:"count", parser:"number" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+addStatListener("duplicate_counts", "duplicate_counts_table", column_defs, fields, {
+ [% IF by_duplicate.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %]
+});
+-->
+</script>
+
+[% IF by_duplicate.size %]
+ <b>[% by_duplicate.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui-skin-sam">
+ <div id="duplicate_counts">
+ <table id="duplicate_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Dupe Count", "Status", "Version"
+ "Component", "Severity" "Summary" ] %]
+
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = by_duplicate %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.dupe_count FILTER html %]</td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+[% ELSE %]
+ <h3>No duplicate [% terms.bugs %] found.</h3>
+[% 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..9c7c42563
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
@@ -0,0 +1,73 @@
+[%# 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">
+<!--
+var column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } },
+ { key:"count", label:"Count", sortable:true },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+var fields = [
+ { key:"id" },
+ { key:"count", parser:"number" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+addStatListener("popularity_counts", "popularity_counts_table", column_defs, fields, {
+ [% IF by_popularity.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %]
+});
+-->
+</script>
+
+[% IF by_popularity.size %]
+ <b>[% by_popularity.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui-skin-sam">
+ <div id="popularity_counts">
+ <table id="popularity_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Count", "Status", "Version"
+ "Component", "Severity" "Summary" ] %]
+
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = by_popularity %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.votes FILTER html %]</td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+[% ELSE %]
+ <h3>No [% terms.bugs %] found.</h3>
+[% 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..4350e7472
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
@@ -0,0 +1,132 @@
+[%# 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">
+<!--
+var column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+var fields = [
+ { key:"id" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+addStatListener("recently_opened", "recently_opened_table", column_defs, fields, {
+ [% IF recently_opened.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %]
+});
+addStatListener("recently_closed", "recently_closed_table", column_defs, fields, {
+ [% IF recently_closed.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %]
+});
+-->
+</script>
+
+<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">
+ <table id="recently_opened_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = recently_opened %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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">
+ <table id="recently_closed_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = recently_closed %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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..fad1cafa5
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
@@ -0,0 +1,54 @@
+[%# 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">
+<!--
+var column_defs = [
+ { key:"milestone", label:"Milestone", sortable:true },
+ { key:"percentage complete", label:"Percentage Complete", sortable:false },
+ { key:"links", label:"Links", sortable:false },
+];
+var fields = [
+ { key:"milestone" },
+ { key:"percentage complete" },
+ { key:"links" }
+];
+
+addStatListener("bug_milestones", "bug_milestones_table", column_defs, fields, {});
+-->
+</script>
+
+<div class="yui-skin-sam">
+<div id="bug_milestones">
+ <table id="bug_milestones_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Milestone</th>
+ <th>Percentage Complete</th>
+ <th>Links</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH milestone = by_roadmap %]
+ <tr>
+ <td>[% milestone.name FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = milestone.closed_bugs full_bug_count = milestone.total_bugs %]
+ </td>
+ <td>
+ <a href="[% milestone.link_closed FILTER html %]">
+ [% milestone.closed_bugs FILTER html %]</a>&nbsp;of&nbsp;
+ <a href="[% milestone.link_total FILTER html %]">
+ [% milestone.total_bugs FILTER html %]</a>&nbsp;bugs have been closed
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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..c1b0a8854
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
@@ -0,0 +1,205 @@
+[%# 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">
+<!--
+var column_defs = [
+ { key:"name", label:"Name", sortable:true },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false }
+];
+var fields = [
+ { key:"name" },
+ { key:"count", parser:"number" },
+ { key:"percentage" }
+];
+addStatListener("bug_counts", "bug_counts_table", column_defs, fields, {});
+addStatListener("status_counts", "status_counts_table", column_defs, fields, {});
+addStatListener("priority_counts", "priority_counts_table", column_defs, fields, {});
+addStatListener("severity_counts", "severity_counts_table", column_defs, fields, {});
+addStatListener("assignee_counts", "assignee_counts_table", column_defs, fields, {});
+-->
+</script>
+
+<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="yui-skin-sam">
+ <a name="counts"></a>
+ <b>[% terms.Bug %] Counts</b>
+ <div id="bug_counts">
+ <table id="bug_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><a href="[% bug_link_all FILTER html %]">Total [% terms.Bugs %]</a></td>
+ <td>[% total_bugs FILTER html %]</td>
+ <td>&nbsp;</td>
+ </tr>
+ <tr>
+ <td><a href="[% bug_link_open FILTER html %]">Open [% terms.Bugs %]</a></td>
+ <td>[% total_open_bugs FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = total_open_bugs full_bug_count = total_bugs %]
+ </td>
+ </tr>
+ <tr>
+ <td><a href="[% bug_link_closed FILTER html %]">Closed [% terms.Bugs %]</a></td>
+ <td>[% total_closed_bugs FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = total_closed_bugs full_bug_count = total_bugs %]
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="status"></a>
+ <b>Status</b>
+ (<a href="#top">back to top</a>)
+ <div id="status_counts">
+ <table id="status_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_status %]
+ [% NEXT IF col.0 == 'CLOSED' %]
+ <tr>
+ <td>
+ <a href="[% bug_link_all FILTER html %]&amp;bug_status=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="priority"></a>
+ <b>Priority</b>
+ (<a href="#top">back to top</a>)
+ <div id="priority_counts">
+ <table id="priority_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ </tbody>
+ [% FOREACH col = by_priority %]
+ <tr>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;priority=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="severity"></a>
+ <b>Severity</b>
+ (<a href="#top">back to top</a>)
+ <div id="severity_counts">
+ <table id="severity_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_severity %]
+ <tr>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;bug_severity=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="assignee"></a>
+ <b>Assignee</b>
+ (<a href="#top">back to top</a>)
+ <div id="assignee_counts">
+ <table id="assignee_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_assignee %]
+ <tr>
+ <td>
+ [% IF user.id %]
+ <a href="[% bug_link FILTER html %]&amp;emailassigned_to1=1&amp;emailtype1=exact&amp;email1=[% col.0.email FILTER uri %]">
+ [% col.0.email FILTER html %]</a>
+ [% ELSE %]
+ [% col.0.realname || "No Name" FILTER html %]
+ [% END %]
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </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/productdashboard.js b/extensions/ProductDashboard/web/js/productdashboard.js
new file mode 100644
index 000000000..e7276b7b2
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/productdashboard.js
@@ -0,0 +1,94 @@
+/* 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.
+ */
+
+function addStatListener (div_name, table_name, column_defs, fields, options) {
+ YAHOO.util.Event.addListener(window, "load", function() {
+ YAHOO.example.StatsFromMarkup = new function() {
+ this.myDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get(table_name));
+ this.myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
+ this.myDataSource.responseSchema = { fields:fields };
+ this.myDataTable = new YAHOO.widget.DataTable(div_name, column_defs, this.myDataSource, options);
+ this.myDataTable.subscribe("rowMouseoverEvent", this.myDataTable.onEventHighlightRow);
+ this.myDataTable.subscribe("rowMouseoutEvent", this.myDataTable.onEventUnhighlightRow);
+ };
+ });
+}
+
+// Custom sort handler to sort by bug id inside an anchor tag
+var sortBugIdLinks = function(a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+ // Now we need to pull out the ID text and convert to Numbers
+ // First we do 'a'
+ var container = document.createElement("bug_id_link");
+ container.innerHTML = a.getData("id");
+ var anchors = container.getElementsByTagName("a");
+ var text = anchors[0].textContent;
+ if (text === undefined) text = anchors[0].innerText;
+ var new_a = new Number(text);
+ // Then we do 'b'
+ container.innerHTML = b.getData("id");
+ anchors = container.getElementsByTagName("a");
+ text = anchors[0].textContent;
+ if (text == undefined) text = anchors[0].innerText;
+ var new_b = new Number(text);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug severities
+var sortBugSeverity = function(a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(severities[YAHOO.lang.trim(a.getData('bug_severity'))]);
+ var new_b = new Number(severities[YAHOO.lang.trim(b.getData('bug_severity'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug priorities
+var sortBugPriority = function(a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(priorities[YAHOO.lang.trim(a.getData('priority'))]);
+ var new_b = new Number(priorities[YAHOO.lang.trim(b.getData('priority'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css
new file mode 100644
index 000000000..8f431006a
--- /dev/null
+++ b/extensions/ProductDashboard/web/styles/productdashboard.css
@@ -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. */
+
+#product_dashboard_links {
+ float: right;
+ border: 1px solid;
+ padding-right: 25px;
+}
diff --git a/template/en/default/global/common-links.html.tmpl b/template/en/default/global/common-links.html.tmpl
index 769d41e7e..ec8608eed 100644
--- a/template/en/default/global/common-links.html.tmpl
+++ b/template/en/default/global/common-links.html.tmpl
@@ -55,6 +55,8 @@
[% END %]
[%-# Work around FF bug: keep this on one line %]</li>
+ [% Hook.process('action-links') %]
+
[% IF user.login %]
<li><span class="separator">| </span><a href="userprefs.cgi">Preferences</a></li>
[% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless