Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions lib/CPAN/Testers/Web.pm
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,44 @@ sub startup ( $app ) {
catdir( dist_dir( 'CPAN-Testers-Web' ), 'public' );
unshift @{ $app->commands->namespaces }, 'CPAN::Testers::Web::Command';


$app->moniker( 'web' );
# This application has no configuration yet
$app->plugin( Config => {
default => {
api_host => 'api.cpantesters.org',
dist_modules => [qw/
Mojolicious
Moose
Moo
DBIx-Class
App-cpanminus
DBI
Plack
DateTime
Devel-NYTProf
Test-Simple
Path-Tiny
Dist-Zilla
Scalar-List-Utils
App-perlbrew
Try-Tiny
libwww-perl
AnyEvent
Catalyst-Runtime
Data-Printer
Dancer
Template-Toolkit
Type-Tiny
Perl-Tidy
Dancer2
Perl-Critic
ack
Carton
Getopt-Long
List-MoreUtils
Task-Kensho
/]
},
} );

Expand Down Expand Up @@ -222,12 +255,9 @@ sub startup ( $app ) {
->name( 'release.dist' )
->to( 'reports#dist_versions' );

$r->get( '/dist' )
->name( 'dist-search' )
->to( cb => sub {
my ( $c ) = @_;
$c->render( 'dist-search' );
} );
$r->get( '/dist' )->name( 'dist-search' )->to( 'dist#search' );
$r->post( '/dist' )->name( 'dist-recent' )->to( 'dist#recent' );
$r->post( '/dist/valid' )->name( 'dist-valid' )->to( 'dist#valid' );

$r->get( '/author/:author', [ format => [qw( html rss json)] ] )
->name( 'reports.author' )
Expand Down
92 changes: 92 additions & 0 deletions lib/CPAN/Testers/Web/Controller/Dist.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package CPAN::Testers::Web::Controller::Dist;
our $VERSION = '0.001';
# ABSTRACT: Endpoints for viewing and managing distributions

=head1 DESCRIPTION

=head1 SEE ALSO

=cut

use Mojo::Base 'Mojolicious::Controller';
use CPAN::Testers::Web::Base;
use CPAN::Testers::Web::Controller::Legacy;

=method search

Landing page for /dist

Lists modules configured in the config

=cut

sub search ( $c ) {
$c->render('dist/search',
dists => $c->config->{dist_modules}
);
}

=method recent

Expects a list of dists and will return the most recent for each.

=cut

sub recent ( $c ) {
my $join = $c->schema->perl5->resultset('Release')->search({
dist => { -in => $c->req->json->{dists} }
}, {
group_by => [qw( me.dist )],
select => [qw/dist/, \('MAX(version)')],
as => [qw/dist version/]
});

if (!$join->count) {
return $c->render( json => { dists => [] } );
}

my $rs = $c->schema->perl5->resultset('Release')->search({
-or => [
map +{ 'me.dist' => $_->dist, 'me.version' => $_->version }, $join->all()
],
}, {
join => 'upload'
});

$c->render( json => {
dists => [
map +{
$_->get_inflated_columns,
released => $_->upload->released->datetime( ' ' ),
},
$rs->all
]

} );
}

=method valid

validate that a dist exists

=cut

sub valid ( $c ) {
my $rs = $c->schema->perl5->resultset('Release')->search({
dist => { like => $c->req->json->{dist} . '%' }
}, {
distinct => 1,
select => ['dist'],
as => ['dist'],
rows => 50
});

$c->render( json => {
dists => [
map { $_->dist }
$rs->all
]
} );
}

1;
10 changes: 10 additions & 0 deletions share/public/css/cpantesters.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@ body {
margin-bottom: 1em;
}


#popular tr th:first-child {
min-width: 150px;
}

@media only screen and (max-width: 600px) {
#popular tr th:first-child {
min-width: auto;
}
}
207 changes: 207 additions & 0 deletions share/templates/dist/search.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@

% title 'Search Distributions';

<div class="container">
<div class="row">
<div class="col-md-12">

<h1>Search Distributions</h1>

<form id="search-form" class="form-inline">
<div class="form-group">
<label for="search">Search:</label>
<input id="search" class="form-control"
placeholder="Search Distributions"
>
</div>
<button class="btn btn-primary">Search</button>
</form>

<h2 id="search-title">Popular Distributions</h2>

<table id="popular" class="table table-striped">
<thead>
<tr>
<th>Distribution</th>
<th>Last Version</th>
<th>Released</th>
<th class="text-center">Reports</th>
<th class="text-center">Pass</th>
<th class="text-center">Fail</th>
<th class="text-center">Unkn</th>
<th class="text-center">NA</th>
</tr>
</thead>
<tbody>
<template>
<tr>
<td>
<a data-id="dist" href="/dist/Statocles">Statocles</a>
</td>
<td data-id="version">0.066</td>
<td>
<time data-id="released">2015-01-11 00:12:34</time>
</td>
<td data-id="total" class="text-center">
12
</td>
<td data-id="pass" class="bg-success text-center">
11
</td>
<td data-id="fail" class="text-center">
1
</td>
<td data-id="unknown" class="text-center">
1
</td>
<td data-id="na" class="text-center">
1
</td>
</tr>
</template>


</tbody>
</table>
<script>
(function () {

let Dist = function (dists) {
this.title = document.querySelector('#search-title');
this.search_form = document.querySelector('#search-form');
this.tbody = document.querySelector('#popular tbody');
this.template = this.tbody.querySelector('template');
this.init(dists);
};

Dist.prototype = {
active_type: 'popular',
init: function (dists) {
this.setupEvents();
this.poll(dists, 'popular', 0);
},
setupEvents: function () {
let self = this;
let input = self.search_form.querySelector('input#search');
let last_search = "";
self.search_form.addEventListener('submit', function (evt) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use arrow functions, you can get rid of the need to do let self = this;:

this.search_form.addEventListener('submit', (evt) => {
   // inside here, `this` is the same as outside
});

evt.preventDefault();

if (!input.value) {
return self.switchPopularMode();
}

if (last_search == input.value) return;

last_search = input.value;

self.request('/dist/valid', { dist: input.value }, function (res) {
if (!res.dists || !res.dists.length) {
alert('Distribution with the name ' + input.value + ' not found');
return;
}

self.switchSearchMode(input.value);

self.tbody.querySelectorAll('.search-dist').forEach(function (n) {
n.remove();
});

self.poll(res.dists, input.value, 0);
});
});

input.addEventListener('keyup', function () {
input.value = input.value.replace(new RegExp("([^a-zA-Z0-9\-]+)", "g"), "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this without JavaScript if you'd like by using the <input pattern="..." /> HTML attribute (MDN docs). If the input doesn't pass validation, the submit event shouldn't(?) happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a preference, I prefer validating the input and preventing the characters this way.

});
},
switchSearchMode: function (type) {
let self = this;

self.active_type = type;

self.title.innerText = 'Search results';

self.tbody.querySelectorAll('.popular-dist').forEach(function (n) {
n.classList.add('hide');;
});

},
switchPopularMode: function () {
let self = this;

self.active_type = 'popular';

self.title.innerText = 'Popular Distributions';

self.tbody.querySelectorAll('.popular-dist').forEach(function (n) {
n.classList.remove('hide');;
});

self.tbody.querySelectorAll('.search-dist').forEach(function (n) {
n.remove();
});
},
poll: function (dists, type, inc) {
let self = this;
if (inc < 10) inc++;
let pdists = dists.splice(0, inc);
self.request('/dist', { dists: pdists }, function (res) {
res.dists.forEach(function (dist) {
self.render_dist(dist, type);
});
if (
(type == 'popular' || type == self.active_type)
&& dists.length > 0
) self.poll(dists, type, inc);
});
},
request: function (url, params, cb) {
fetch(url, {
method: "POST",
body: JSON.stringify(params),
}).then(function (res) {
return res.json();
}).then(function (res) {
cb(res);
}).catch(function (res) {
console.log(res);
});
},
render_dist: function (dist, type) {
let tr = this.template.content.cloneNode(true);
let name = tr.querySelector('[data-id="dist"]');
name.innerText = dist.dist;
name.href = '/dist/' + dist.dist;
dist.total = dist.pass + dist.fail + dist.unknown + dist.na;
['version', 'released', 'total', 'pass'].forEach(function (n) {
tr.querySelector('[data-id="' + n + '"]').innerText = dist[n];
});
let fail = tr.querySelector('[data-id="fail"]');
fail.innerText = dist.fail;
fail.classList.add(dist.fail ? 'bg-danger' : 'bg-success');
['unknown', 'na'].forEach(function (n) {
let x = tr.querySelector('[data-id="' + n + '"]');
x.innerText = dist[n];
x.classList.add(dist[n] ? 'bg-warning' : 'bg-success');
});
tr.firstElementChild.classList.add(type == "popular" ? "popular-dist" : "search-dist");
if (type != this.active_type) {
tr.firstElementChild.classList.add("hide");
}
this.tbody.appendChild(tr);
}
};

let dists = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better as let dists = <%= encode_json $dists %>, but that's just an opinion, not a requirement :)

% for my $dist (@$dists) {
"<%= $dist %>",
% }
];

new Dist(dists);
})();
</script>
</div>
</div>
</div>