Add a dashboard

Currently the dashboard allows users to get a quick overview of the
status of jobs they're interested in, but more will be added,
e.g. viewing all your jobsets or all jobs of which you're a
maintainer.
This commit is contained in:
Eelco Dolstra 2013-10-14 20:07:26 +02:00
parent 09b5679ee7
commit 2127d133cd
16 changed files with 382 additions and 11 deletions

View file

@ -60,6 +60,12 @@ sub overview : Chained('job') PathPart('') Args(0) {
$c->stash->{aggregates} = $aggregates; $c->stash->{aggregates} = $aggregates;
$c->stash->{constituentJobs} = [sort (keys %constituentJobs)]; $c->stash->{constituentJobs} = [sort (keys %constituentJobs)];
$c->stash->{starred} = $c->user->starredjobs(
{ project => $c->stash->{project}->name
, jobset => $c->stash->{jobset}->name
, job => $c->stash->{job}->name
})->count == 1 if $c->user_exists;
} }
@ -74,4 +80,22 @@ sub get_builds : Chained('job') PathPart('') CaptureArgs(0) {
} }
sub star : Chained('job') PathPart('star') Args(0) {
my ($self, $c) = @_;
requirePost($c);
requireUser($c);
my $args =
{ project => $c->stash->{project}->name
, jobset => $c->stash->{jobset}->name
, job => $c->stash->{job}->name
};
if ($c->request->params->{star} eq "1") {
$c->user->starredjobs->update_or_create($args);
} else {
$c->user->starredjobs->find($args)->delete;
}
$c->stash->{resource}->{success} = 1;
}
1; 1;

View file

@ -269,4 +269,19 @@ sub edit_POST {
} }
sub dashboard :Chained('user') :Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'dashboard.tt';
# Get the N most recent builds for each starred job.
$c->stash->{starredJobs} = [];
foreach my $j ($c->stash->{user}->starredjobs->search({}, { order_by => ['project', 'jobset', 'job'] })) {
my @builds = $j->job->builds->search(
{ },
{ rows => 20, order_by => "id desc" });
push $c->stash->{starredJobs}, { job => $j->job, builds => [@builds] };
}
}
1; 1;

View file

@ -137,8 +137,27 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" }, { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
); );
=head2 starredjobs
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 Type: has_many
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t2CCfUjFEz/lO4szROz1AQ
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{
"foreign.job" => "self.name",
"foreign.jobset" => "self.jobset",
"foreign.project" => "self.project",
},
undef,
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:uYKWjewvKBEAuK53u7vKuw
1; 1;

View file

@ -286,8 +286,26 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" }, { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
); );
=head2 starredjobs
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-09-25 14:10:28 Type: has_many
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:cAZ4+c7OhqGW8ATru8Foiw
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{
"foreign.jobset" => "self.name",
"foreign.project" => "self.project",
},
undef,
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:DTAGxP5RFvcNxP/ciJGo4Q
1; 1;

View file

@ -226,6 +226,21 @@ __PACKAGE__->has_many(
undef, undef,
); );
=head2 starredjobs
Type: has_many
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{ "foreign.project" => "self.name" },
undef,
);
=head2 viewjobs =head2 viewjobs
Type: has_many Type: has_many
@ -267,8 +282,8 @@ Composing rels: L</projectmembers> -> username
__PACKAGE__->many_to_many("usernames", "projectmembers", "username"); __PACKAGE__->many_to_many("usernames", "projectmembers", "username");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RffghAo9jAaqYk41y1Sdqw # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PdNQ2mf5azBB6nI+iAm8fQ
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Projects.pm' found in @INC. # These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Projects.pm' found in @INC.
# They are now part of the custom portion of this file # They are now part of the custom portion of this file
# for you to hand-edit. If you do not either delete # for you to hand-edit. If you do not either delete

View file

@ -0,0 +1,161 @@
use utf8;
package Hydra::Schema::StarredJobs;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::StarredJobs
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<StarredJobs>
=cut
__PACKAGE__->table("StarredJobs");
=head1 ACCESSORS
=head2 username
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 project
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 jobset
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 job
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"username",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"project",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"jobset",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"job",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</username>
=item * L</project>
=item * L</jobset>
=item * L</job>
=back
=cut
__PACKAGE__->set_primary_key("username", "project", "jobset", "job");
=head1 RELATIONS
=head2 job
Type: belongs_to
Related object: L<Hydra::Schema::Jobs>
=cut
__PACKAGE__->belongs_to(
"job",
"Hydra::Schema::Jobs",
{ jobset => "jobset", name => "job", project => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 jobset
Type: belongs_to
Related object: L<Hydra::Schema::Jobsets>
=cut
__PACKAGE__->belongs_to(
"jobset",
"Hydra::Schema::Jobsets",
{ name => "jobset", project => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 project
Type: belongs_to
Related object: L<Hydra::Schema::Projects>
=cut
__PACKAGE__->belongs_to(
"project",
"Hydra::Schema::Projects",
{ name => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 username
Type: belongs_to
Related object: L<Hydra::Schema::Users>
=cut
__PACKAGE__->belongs_to(
"username",
"Hydra::Schema::Users",
{ username => "username" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:naj5aKWuw8hLE6klmvW9Eg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -135,6 +135,21 @@ __PACKAGE__->has_many(
undef, undef,
); );
=head2 starredjobs
Type: has_many
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{ "foreign.username" => "self.username" },
undef,
);
=head2 userroles =head2 userroles
Type: has_many Type: has_many
@ -161,8 +176,8 @@ Composing rels: L</projectmembers> -> project
__PACKAGE__->many_to_many("projects", "projectmembers", "project"); __PACKAGE__->many_to_many("projects", "projectmembers", "project");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hy3MKvFxfL+1bTc7Hcb1zA # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Users.pm' found in @INC. # These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Users.pm' found in @INC.
# They are now part of the custom portion of this file # They are now part of the custom portion of this file
# for you to hand-edit. If you do not either delete # for you to hand-edit. If you do not either delete

View file

@ -459,4 +459,9 @@ BLOCK menuItem %]
</li> </li>
[% END; [% END;
BLOCK makeStar %]
<span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
[% END;
%] %]

42
src/root/dashboard.tt Normal file
View file

@ -0,0 +1,42 @@
[% WRAPPER layout.tt title="Dashboard" %]
[% PROCESS common.tt %]
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-starred-jobs" data-toggle="tab">Starred jobs</a></li>
</ul>
<div id="generic-tabs" class="tab-content">
<div id="tabs-starred-jobs" class="tab-pane active">
[% IF starredJobs.size > 0 %]
<p>Below are the 20 most recent builds of your starred jobs.</p>
<table class="table table-striped table-condensed">
<thead>
<tr><th>Job</th></tr>
</thead>
<tdata>
[% FOREACH j IN starredJobs %]
<tr>
<td>[% INCLUDE renderFullJobName project=j.job.get_column('project') jobset=j.job.get_column('jobset') job=j.job.name %]</td>
[% FOREACH b IN j.builds %]
<td><a href="[% c.uri_for('/build' b.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=b %]</a></td>
[% END %]
</tr>
[% END %]
</tdata>
</table>
[% ELSE %]
<div class="alert alert-warning">You have no starred jobs. You can add them by visiting a job page and clicking on the ☆ icon.</div>
[% END %]
</div>
</div>
[% END %]

View file

@ -1,4 +1,7 @@
[% WRAPPER layout.tt title="Job $project.name:$jobset.name:$job.name" %] [% WRAPPER layout.tt
title="Job $project.name:$jobset.name:$job.name"
starUri=c.uri_for(c.controller('Job').action_for('star'), c.req.captures)
%]
[% PROCESS common.tt %] [% PROCESS common.tt %]
[% hideProjectName=1 hideJobsetName=1 hideJobName=1 %] [% hideProjectName=1 hideJobsetName=1 hideJobName=1 %]

View file

@ -82,7 +82,7 @@
[% IF !hideHeader %] [% IF !hideHeader %]
<div class="page-header"> <div class="page-header">
<h1><small>[% HTML.escape(title) %]</small></h1> <h1><small>[% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]</small></h1>
</div> </div>
[% ELSE %] [% ELSE %]
<br /> <br />

View file

@ -97,5 +97,14 @@ td.nowrap {
} }
.actions { .actions {
font-weight:bold; font-weight: bold;
} }
.star {
color: black;
font-size: 110%;
}
.star:hover {
cursor: pointer;
}

View file

@ -80,6 +80,23 @@ $(document).ready(function() {
$('div[data-toggle="buttons-radio"] .btn').click(function(){ $('div[data-toggle="buttons-radio"] .btn').click(function(){
$('input', $(this).parent()).val($(this).val()); $('input', $(this).parent()).val($(this).val());
}); });
$(".star").click(function(event) {
var star = $(this);
var active = star.text() != '★';
requestJSON({
url: star.attr("data-post"),
data: active ? "star=1" : "star=0",
type: 'POST',
success: function(res) {
if (active) {
star.text('★');
} else {
star.text('☆');
}
}
});
});
}); });
var tabsLoaded = {}; var tabsLoaded = {};

View file

@ -9,6 +9,10 @@
<ul class="nav pull-left"> <ul class="nav pull-left">
[% IF c.user_exists %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('User').action_for('dashboard'), [c.user.username]) title = "Dashboard" %]
[% END %]
[% WRAPPER makeSubMenu title="Status" %] [% WRAPPER makeSubMenu title="Status" %]
[% INCLUDE menuItem [% INCLUDE menuItem
uri = c.uri_for(c.controller('Root').action_for('queue')) uri = c.uri_for(c.controller('Root').action_for('queue'))

View file

@ -532,6 +532,19 @@ create table AggregateConstituents (
); );
create table StarredJobs (
userName text not null,
project text not null,
jobset text not null,
job text not null,
primary key (userName, project, jobset, job),
foreign key (userName) references Users(userName) on update cascade on delete cascade,
foreign key (project) references Projects(name) on update cascade on delete cascade,
foreign key (project, jobset) references Jobsets(project, name) on update cascade on delete cascade,
foreign key (project, jobset, job) references Jobs(project, jobset, name) on update cascade on delete cascade
);
-- Cache of the number of finished builds. -- Cache of the number of finished builds.
create table NrBuilds ( create table NrBuilds (
what text primary key not null, what text primary key not null,

11
src/sql/upgrade-23.sql Normal file
View file

@ -0,0 +1,11 @@
create table StarredJobs (
userName text not null,
project text not null,
jobset text not null,
job text not null,
primary key (userName, project, jobset, job),
foreign key (userName) references Users(userName) on update cascade on delete cascade,
foreign key (project) references Projects(name) on update cascade on delete cascade,
foreign key (project, jobset) references Jobsets(project, name) on update cascade on delete cascade,
foreign key (project, jobset, job) references Jobs(project, jobset, name) on update cascade on delete cascade
);