Split dashboard into widgets. Add build errors widget.

This commit is contained in:
Stepan Strelets 2017-11-06 00:34:11 +03:00
parent 257aabc113
commit 11f58d7c2b
24 changed files with 793 additions and 328 deletions

View file

@ -66,4 +66,23 @@ php-censor:
port: 389
base_dn: 'dc=php-censor,dc=local'
mail_attribute: mail
dashboard_widgets:
all_projects:
side: left
last_builds:
side: right
```
Dashboard widgets
-----------------
* `all_projects` - all projects build status
* `last_builds` - last builds
* `build_errors` - not successful builds
Each widget can be located in the left or right column, use the `side` option for this:
```yml
all_projects:
side: left
```

View file

@ -599,3 +599,24 @@ h6,
.timeline > .time-label > span {
padding: 4px 8px;
}
.loader {
margin: 10px auto;
border: 8px solid #f3f3f3;
border-radius: 50%;
border-top: 8px solid #8AA4AF;
width: 32px;
height: 32px;
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View file

@ -1,5 +1,6 @@
var PHPCensor = {
intervals: {},
widgets: {},
init: function () {
$(document).ready(function () {
@ -11,11 +12,6 @@ var PHPCensor = {
if (typeof PROJECT_ID != 'undefined') {
PHPCensor.intervals.getProjectBuilds = setInterval(PHPCensor.getProjectBuilds, 10000);
}
if (typeof DASHBOARD != 'undefined') {
PHPCensor.intervals.getDashboard = setInterval(PHPCensor.getDashboard, 10000);
PHPCensor.intervals.getTimeline = setInterval(PHPCensor.getTimeline, 10000);
}
});
$(window).on('builds-updated', function (e, data) {
@ -47,34 +43,6 @@ var PHPCensor = {
});
},
getDashboard: function () {
$('.project-box').each(function (index) {
var projectId = this.id.substring(12);
$.ajax({
url: APP_URL + 'project/ajax-dashboard-project/' + projectId,
success: function (data) {
$(('#project-box-' + projectId)).html(data);
},
error: PHPCensor.handleFailedAjax
});
});
},
getTimeline: function () {
$.ajax({
url: APP_URL + 'build/ajax-timeline',
success: function (data) {
$('#timeline-box').html(data);
},
error: PHPCensor.handleFailedAjax
});
},
updateHeaderBuilds: function (data) {
$('.app-pending-list').empty();
$('.app-running-list').empty();

View file

@ -0,0 +1,40 @@
PHPCensor.widgets.allProjects = {
interval: null,
init: function () {
$(document).ready(function () {
PHPCensor.widgets.allProjects.load();
});
},
load: function() {
$.ajax({
url: APP_URL + 'widget-all-projects',
success: function (data) {
$(('#widget-all_projects-container')).html(data);
PHPCensor.widgets.allProjects.interval = setInterval(PHPCensor.widgets.allProjects.update, 10000);
},
error: PHPCensor.handleFailedAjax
});
},
update: function () {
$('.project-box').each(function (index) {
var projectId = this.id.substring(12);
$.ajax({
url: APP_URL + 'widget-all-projects/update/' + projectId,
success: function (data) {
$(('#project-box-' + projectId)).html(data);
},
error: PHPCensor.handleFailedAjax
});
});
}
};
PHPCensor.widgets.allProjects.init();

View file

@ -0,0 +1,36 @@
PHPCensor.widgets.buildErrors = {
interval: null,
init: function () {
$(document).ready(function () {
PHPCensor.widgets.buildErrors.load();
});
},
load: function() {
$.ajax({
url: APP_URL + 'widget-build-errors',
success: function (data) {
$(('#widget-build_errors-container')).html(data);
PHPCensor.widgets.buildErrors.interval = setInterval(PHPCensor.widgets.buildErrors.update, 10000);
},
error: PHPCensor.handleFailedAjax
});
},
update: function () {
$.ajax({
url: APP_URL + 'widget-build-errors/update',
success: function (data) {
$(('#dashboard-build-errors')).html(data);
},
error: PHPCensor.handleFailedAjax
});
}
};
PHPCensor.widgets.buildErrors.init();

View file

@ -0,0 +1,36 @@
PHPCensor.widgets.lastBuilds = {
interval: null,
init: function () {
$(document).ready(function () {
PHPCensor.widgets.lastBuilds.load();
});
},
load: function() {
$.ajax({
url: APP_URL + 'widget-last-builds',
success: function (data) {
$(('#widget-last_builds-container')).html(data);
PHPCensor.widgets.lastBuilds.interval = setInterval(PHPCensor.widgets.lastBuilds.update, 10000);
},
error: PHPCensor.handleFailedAjax
});
},
update: function () {
$.ajax({
url: APP_URL + 'widget-last-builds/update',
success: function (data) {
$('#timeline-box').html(data);
},
error: PHPCensor.handleFailedAjax
});
}
};
PHPCensor.widgets.lastBuilds.init();

View file

@ -293,6 +293,14 @@ class InstallCommand extends Command
],
],
],
'dashboard_widgets' => [
'all_projects' => [
'side' => 'left',
],
'last_builds' => [
'side' => 'right',
],
],
];
}

View file

@ -368,20 +368,4 @@ class BuildController extends Controller
return $response;
}
public function ajaxTimeline()
{
$builds = $this->buildStore->getLatestBuilds(null, 10);
foreach ($builds as &$build) {
$build = BuildFactory::getBuild($build);
}
$view = new b8\View('Home/ajax-timeline');
$view->builds = $builds;
$this->response->disableLayout();
$this->response->setContent($view->render());
return $this->response;
}
}

View file

@ -3,120 +3,40 @@
namespace PHPCensor\Controller;
use b8;
use PHPCensor\BuildFactory;
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
use PHPCensor\Controller;
/**
* Home Controller - Displays the Dashboard.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class HomeController extends Controller
{
/**
* @var \PHPCensor\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCensor\Store\ProjectStore
*/
protected $projectStore;
/**
* @var \PHPCensor\Store\ProjectGroupStore
*/
protected $groupStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
$this->projectStore = b8\Store\Factory::getStore('Project');
$this->groupStore = b8\Store\Factory::getStore('ProjectGroup');
}
/**
* Display dashboard:
*/
public function index()
{
$this->layout->title = Lang::get('dashboard');
$builds = $this->buildStore->getLatestBuilds(null, 10);
foreach ($builds as &$build) {
$build = BuildFactory::getBuild($build);
$widgets = [
'left' => [],
'right' => [],
];
$widgets_config = b8\Config::getInstance()->get('php-censor.dashboard_widgets', [
'all_projects' => [
'side' => 'left',
],
'last_builds' => [
'side' => 'right',
],
]);
foreach($widgets_config as $name => $params) {
$side = (isset($params['side']) and ($params['side'] == 'right')) ? 'right' : 'left';
$widgets[$side][$name] = $params;
}
$this->view->builds = $builds;
$this->view->groups = $this->getGroupInfo();
$this->view->widgets = $widgets;
return $this->view->render();
}
/**
* Generate the HTML for the project overview section of the dashboard.
* @param $projects
* @return string
*/
protected function getSummaryHtml($projects)
{
$summaryBuilds = [];
$successes = [];
$failures = [];
$counts = [];
foreach ($projects as $project) {
$summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId());
$count = $this->buildStore->getWhere(
['project_id' => $project->getId()],
1,
0,
[],
['id' => 'DESC']
);
$counts[$project->getId()] = $count['count'];
$success = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_SUCCESS);
$failure = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_FAILED);
$successes[$project->getId()] = $success;
$failures[$project->getId()] = $failure;
}
$view = new b8\View('Home/dashboard-projects');
$view->projects = $projects;
$view->builds = $summaryBuilds;
$view->successful = $successes;
$view->failed = $failures;
$view->counts = $counts;
return $view->render();
}
/**
* Get a summary of the project groups we have, and what projects they have in them.
*
* @return array
*/
protected function getGroupInfo()
{
$rtn = [];
$groups = $this->groupStore->getWhere([], 100, 0, [], ['title' => 'ASC']);
foreach ($groups['items'] as $group) {
$thisGroup = ['title' => $group->getTitle()];
$projects = $this->projectStore->getByGroupId($group->getId(), false);
$thisGroup['projects'] = $projects['items'];
$thisGroup['summary'] = $this->getSummaryHtml($thisGroup['projects']);
$rtn[] = $thisGroup;
}
return $rtn;
}
}

View file

@ -287,35 +287,6 @@ class ProjectController extends PHPCensor\Controller
];
}
/**
* Render latest builds for project as HTML table.
*
* @param int $projectId
*
* @return array
*/
protected function getDashboardProjectHtml($projectId)
{
$count = $this->buildStore->getWhere(
['project_id' => $projectId],
1,
0,
[],
['id' => 'DESC']
);
$counts = $count['count'];
$view = new b8\View('Home/ajax-dashboard-project');
$view->project = $this->projectStore->getById($projectId);
$view->builds = $this->buildStore->getLatestBuilds($projectId);
$view->successful = $this->buildStore->getLastBuildByStatus($projectId, PHPCensor\Model\Build::STATUS_SUCCESS);
$view->failed = $this->buildStore->getLastBuildByStatus($projectId, PHPCensor\Model\Build::STATUS_FAILED);
$view->counts = $counts;
return $view->render();
}
/**
* Add a new project. Handles both the form, and processing.
*/
@ -605,21 +576,6 @@ class ProjectController extends PHPCensor\Controller
};
}
/**
* @param int $projectId
*
* @return b8\Http\Response
*/
public function ajaxDashboardProject($projectId)
{
$builds = $this->getDashboardProjectHtml($projectId);
$this->response->disableLayout();
$this->response->setContent($builds);
return $this->response;
}
/**
* Get an array of repositories from Github's API.
*/

View file

@ -0,0 +1,143 @@
<?php
namespace PHPCensor\Controller;
use b8;
use PHPCensor\BuildFactory;
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
use PHPCensor\Controller;
/**
* Widget All Projects Controller
*/
class WidgetAllProjectsController extends Controller
{
/**
* @var \PHPCensor\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCensor\Store\ProjectStore
*/
protected $projectStore;
/**
* @var \PHPCensor\Store\ProjectGroupStore
*/
protected $groupStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
$this->projectStore = b8\Store\Factory::getStore('Project');
$this->groupStore = b8\Store\Factory::getStore('ProjectGroup');
}
/**
* Display dashboard:
*/
public function index()
{
$this->view->groups = $this->getGroupInfo();
$this->response->disableLayout();
$this->response->setContent($this->view->render());
return $this->response;
}
/**
* Generate the HTML for the project overview section of the dashboard.
* @param $projects
* @return string
*/
protected function getSummaryHtml($projects)
{
$summaryBuilds = [];
$successes = [];
$failures = [];
$counts = [];
foreach ($projects as $project) {
$summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId());
$count = $this->buildStore->getWhere(
['project_id' => $project->getId()],
1,
0,
[],
['id' => 'DESC']
);
$counts[$project->getId()] = $count['count'];
$success = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_SUCCESS);
$failure = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_FAILED);
$successes[$project->getId()] = $success;
$failures[$project->getId()] = $failure;
}
$view = new b8\View('WidgetAllProjects/index-projects');
$view->projects = $projects;
$view->builds = $summaryBuilds;
$view->successful = $successes;
$view->failed = $failures;
$view->counts = $counts;
return $view->render();
}
/**
* Get a summary of the project groups we have, and what projects they have in them.
*
* @return array
*/
protected function getGroupInfo()
{
$rtn = [];
$groups = $this->groupStore->getWhere([], 100, 0, [], ['title' => 'ASC']);
foreach ($groups['items'] as $group) {
$thisGroup = ['title' => $group->getTitle()];
$projects = $this->projectStore->getByGroupId($group->getId(), false);
$thisGroup['projects'] = $projects['items'];
$thisGroup['summary'] = $this->getSummaryHtml($thisGroup['projects']);
$rtn[] = $thisGroup;
}
return $rtn;
}
/**
* @param int $projectId
*
* @return b8\Http\Response
*/
public function update($projectId)
{
$count = $this->buildStore->getWhere(
['project_id' => $projectId],
1,
0,
[],
['id' => 'DESC']
);
$counts = $count['count'];
$this->view->project = $this->projectStore->getById($projectId);
$this->view->builds = $this->buildStore->getLatestBuilds($projectId);
$this->view->successful = $this->buildStore->getLastBuildByStatus($projectId, Build::STATUS_SUCCESS);
$this->view->failed = $this->buildStore->getLastBuildByStatus($projectId, Build::STATUS_FAILED);
$this->view->counts = $counts;
$this->response->disableLayout();
$this->response->setContent($this->view->render());
return $this->response;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace PHPCensor\Controller;
use b8;
use PHPCensor\BuildFactory;
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
use PHPCensor\Controller;
/**
* Widget Build Errors Controller
*/
class WidgetBuildErrorsController extends Controller
{
/**
* @var \PHPCensor\Store\BuildStore
*/
protected $buildStore;
/**
* @var \PHPCensor\Store\ProjectStore
*/
protected $projectStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
$this->projectStore = b8\Store\Factory::getStore('Project');
}
/**
* Display dashboard:
*/
public function index()
{
$view = new b8\View('WidgetBuildErrors/update');
$this->view->projects = $this->renderAllProjectsLatestBuilds($view);
$this->response->disableLayout();
$this->response->setContent($this->view->render());
return $this->response;
}
/**
* @return b8\Http\Response
*/
public function update()
{
$this->response->disableLayout();
$this->response->setContent($this->renderAllProjectsLatestBuilds($this->view));
return $this->response;
}
/**
* @param b8\View $view
* @return string
*/
protected function renderAllProjectsLatestBuilds($view)
{
$builds = $this->buildStore->getAllProjectsLatestBuilds();
$view->builds = $builds['projects'];
$projects = $this->projectStore->getByIds(array_keys($builds['projects']));
$view_projects = [];
foreach($projects as $id => $project) {
if (!$project->getArchived()) {
$view_projects[$id] = $project;
} else {
unset($builds['projects'][$id]);
}
}
$view->projects = $view_projects;
return $view->render();
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace PHPCensor\Controller;
use b8;
use PHPCensor\BuildFactory;
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
use PHPCensor\Controller;
/**
* Widget Last Builds Controller
*/
class WidgetLastBuildsController extends Controller
{
/**
* @var \PHPCensor\Store\BuildStore
*/
protected $buildStore;
/**
* Initialise the controller, set up stores and services.
*/
public function init()
{
$this->buildStore = b8\Store\Factory::getStore('Build');
}
/**
* Display dashboard:
*/
public function index()
{
$builds = $this->buildStore->getLatestBuilds(null, 10);
foreach ($builds as &$build) {
$build = BuildFactory::getBuild($build);
}
$view = new b8\View('WidgetLastBuilds/update');
$view->builds = $builds;
$this->view->timeline = $view->render();
$this->response->disableLayout();
$this->response->setContent($this->view->render());
return $this->response;
}
public function update()
{
$builds = $this->buildStore->getLatestBuilds(null, 10);
foreach ($builds as &$build) {
$build = BuildFactory::getBuild($build);
}
$this->view->builds = $builds;
$this->response->disableLayout();
$this->response->setContent($this->view->render());
return $this->response;
}
}

View file

@ -77,6 +77,7 @@ PHP Censor',
'last_failed_build' => ' The last failed build was %s.',
'never_failed_build' => ' This project has never failed a build.',
'view_project' => 'View Project',
'projects_with_build_errors' => 'Build errors',
// Timeline:
'latest_builds' => 'Latest Builds',

View file

@ -224,6 +224,92 @@ class BuildStore extends Store
}
}
/**
* Return an array of the latest builds for all projects.
* @param int $limit_by_project
* @param int $limit_all
* @return array
*/
public function getAllProjectsLatestBuilds($limit_by_project = 5, $limit_all = 10)
{
// dont fetch log field - contain many data
$query = '
SELECT
{{id}},
{{project_id}},
{{commit_id}},
{{status}},
{{branch}},
{{create_date}},
{{start_date}},
{{finish_date}},
{{committer_email}},
{{commit_message}},
{{extra}},
{{environment}},
{{tag}}
FROM {{build}}
ORDER BY {{id}} DESC
LIMIT 10000
';
$stmt = Database::getConnection('read')->prepareCommon($query);
if ($stmt->execute()) {
$res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$projects = [];
$latest = [];
foreach($res as $item) {
$project_id = $item['project_id'];
$environment = $item['environment'];
if (empty($projects[$project_id])) {
$projects[$project_id] = [];
}
if (empty($projects[$project_id][$environment])) {
$projects[$project_id][$environment] = [
'latest' => [],
'success' => null,
'failed' => null,
];
}
$build = null;
if (count($projects[$project_id][$environment]['latest']) < $limit_by_project) {
$build = new Build($item);
$projects[$project_id][$environment]['latest'][] = $build;
}
if (count($latest) < $limit_all) {
if (is_null($build)) {
$build = new Build($item);
}
$latest[] = $build;
}
if (empty($projects[$project_id][$environment]['success']) and ($item['status'] == Build::STATUS_SUCCESS)) {
if (is_null($build)) {
$build = new Build($item);
}
$projects[$project_id][$environment]['success'] = $build;
}
if (empty($projects[$project_id][$environment]['failed']) and ($item['status'] == Build::STATUS_FAILED)) {
if (is_null($build)) {
$build = new Build($item);
}
$projects[$project_id][$environment]['failed'] = $build;
}
}
foreach($projects as $idx => $project) {
$projects[$idx] = array_filter($project, function($val) {
return ($val['latest'][0]->getStatus() != Build::STATUS_SUCCESS);
});
}
$projects = array_filter($projects);
return ['projects' => $projects, 'latest' => $latest];
} else {
return [];
}
}
/**
* Return an array of builds for a given project and commit ID.
*

View file

@ -58,6 +58,30 @@ class ProjectStore extends Store
return null;
}
/**
* Get a single Project by Ids.
* @param int[]
* @return Project[]
*/
public function getByIds($values, $useConnection = 'read')
{
if (empty($values)) {
throw new HttpException('Values passed to ' . __FUNCTION__ . ' cannot be empty.');
}
$query = 'SELECT * FROM {{project}} WHERE {{id}} IN ('.implode(', ', array_map('intval', $values)).')';
$stmt = Database::getConnection($useConnection)->prepareCommon($query);
$rtn = [];
if ($stmt->execute()) {
while ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$rtn[$data['id']] = new Project($data);
}
}
return $rtn;
}
/**
* Get multiple Project by Title.
*

View file

@ -1,153 +1,29 @@
<?php
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
/**
* @var Build[] $builds
* @var array $widgets
*/
$all_widgets = array_merge(array_keys($widgets['left']), array_keys($widgets['right']));
foreach($all_widgets as $widget) {
?><script src="<?= APP_URL ?>assets/js/dashboard-widgets/<?= $widget ?>.js" type="text/javascript"></script><?php
}
?>
<script>
var DASHBOARD = true;
</script>
<div class="row">
<div class="col-sm-5">
<?php foreach ($groups as $group): ?>
<div class="box">
<div class="box-header">
<h3 class="box-title"><?php print $group['title']; ?></h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="Collapse">
<i class="fa fa-minus"></i>
</button>
</div>
<?php foreach ($widgets['left'] as $widget => $params) { ?>
<div id="widget-<?= $widget ?>-container">
<div class="loader"></div>
</div>
<div class="box-body">
<?php print $group['summary']; ?>
</div>
</div>
<?php endforeach; ?>
<?php } ?>
</div>
<?php if (!empty($widgets['right'])) { ?>
<div class="col-sm-7 pull-left">
<div class="box">
<div class="box-header">
<h3 class="box-title"><?php Lang::out('latest_builds'); ?></h3>
<?php foreach ($widgets['right'] as $widget => $params) { ?>
<div id="widget-<?= $widget ?>-container">
<div class="loader"></div>
</div>
<div class="box-body" id="timeline-box">
<ul class="timeline">
<?php $last = new \DateTime('-1 Year'); ?>
<?php
foreach ($builds as $build):
$environment = $build->getEnvironment();
$branches = $build->getExtra('branches');
switch ($build->getStatus()) {
case Build::STATUS_PENDING:
$updated = $build->getCreateDate();
$label = Lang::get('pending');
$color = 'blue';
break;
case Build::STATUS_RUNNING:
$updated = $build->getStartDate();
$label = Lang::get('running');
$color = 'yellow';
break;
case Build::STATUS_SUCCESS:
$updated = $build->getFinishDate();
$label = Lang::get('success');
$color = 'green';
break;
case Build::STATUS_FAILED:
$updated = $build->getFinishDate();
$label = Lang::get('failed');
$color = 'red';
break;
}
if (!$updated) {
$updated = $build->getCreateDate();
}
if ($updated->format('Y-m-d') != $last->format('Y-m-d')): $last = $updated;
?>
<li class="time-label">
<span class="bg-gray">
<?= $last->format('Y-m-d'); ?>
</span>
</li>
<?php endif; ?>
<!-- /.timeline-label -->
<!-- timeline item -->
<li>
<i class="fa fa-<?php print $build->getProject()->getIcon(); ?> bg-<?php print $color; ?>"></i>
<div class="timeline-item">
<span class="time"><i class="fa fa-clock-o"></i>
<?php
echo $updated->format('H:i:s');
if ($build->getStatus() != Build::STATUS_PENDING) {
echo ' &mdash; ' . $build->getDuration(); ?> <?= Lang::get('seconds');
}
?>
</span>
<h3 class="timeline-header">
<a href="<?= APP_URL; ?>project/view/<?= $build->getProjectId(); ?>">
<?= $build->getProject()->getTitle(); ?>
</a>
<span><?= $environment; ?></span>
&mdash;
<a href="<?= APP_URL; ?>build/view/<?= $build->getId(); ?>">
Build #<?= $build->getId(); ?>
</a>
&mdash;
<?php Lang::out($build->getSourceHumanize()); ?>
</h3>
<div class="timeline-body">
<a href="<?= $build->getBranchLink();?>"><i class="fa fa-code-fork"></i> <?php echo $build->getBranch(); ?></a>
<?= $branches ? ' + '.implode(', ', $branches) : ''; ?>
<?php if ($tag = $build->getTag()): ?> /
<a href="<?= $build->getTagLink(); ?>" target="_blank">
<i class="fa fa-tag"></i> <?= $tag; ?>
</a>
<?php endif; ?>
<?php
if (!empty($build->getCommitId())) {
echo ' &mdash; ';
echo sprintf(
'<a href="%s" target="_blank">%s %s</a>',
$build->getCommitLink(),
substr($build->getCommitId(), 0, 7),
$build->getCommitterEmail() ? ('(' . $build->getCommitterEmail() . ')') : ''
);
if (!empty($build->getCommitMessage())) {
echo ' &mdash; ';
print $build->getCommitMessage();
}
}
?>
</div>
</div>
</li>
<!-- END timeline item -->
<?php endforeach; ?>
<li>
<i class="fa fa-clock-o"></i>
</li>
</ul>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>

View file

@ -0,0 +1,15 @@
<?php foreach ($groups as $group): ?>
<div class="box">
<div class="box-header">
<h3 class="box-title"><?php print $group['title']; ?></h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="Collapse">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
<div class="box-body">
<?php print $group['summary']; ?>
</div>
</div>
<?php endforeach; ?>

View file

@ -0,0 +1,13 @@
<?php
use PHPCensor\Helper\Lang;
?>
<div class="box">
<div class="box-header">
<h3 class="box-title"><?= Lang::out('projects_with_build_errors') ?></h3>
</div>
<div class="box-body" id="dashboard-build-errors">
<?= $projects ?>
</div>
</div>

View file

@ -0,0 +1,157 @@
<?php
use PHPCensor\Helper\Lang;
use PHPCensor\Model\Build;
/**
* @var Build[] $builds
*/
foreach($builds as $project_id => $project_envs):
if (!isset($projects[$project_id])) {
echo '<!-- project '.$project_id.' not set -->';
continue;
}
$project = $projects[$project_id];
foreach($project_envs as $environment => $project_env):
$statuses = [];
$failures = 0;
$subcls = 'gray';
$cls = '';
$success = null;
$failure = null;
// Get the most recent build status to determine the main block colour.
$last_build = $project_env['latest'][0];
$status = $last_build->getStatus();
switch($status) {
case 0:
$subcls = 'blue';
break;
case 1:
$subcls = 'yellow';
break;
case 2:
$subcls = 'green';
break;
case 3:
$subcls = 'red';
break;
}
// Use the last 5 builds to determine project health:
$failures = 0;
foreach ($project_env['latest'] as $build) {
switch ($build->getStatus()) {
case 0:
$statuses[] = 'pending';
break;
case 1:
$statuses[] = 'running';
break;
case 2:
$statuses[] = 'ok';
$success = is_null($success) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success;
break;
case 3:
$failures++;
$statuses[] = 'failed';
$failure = is_null($failure) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure;
break;
}
}
$buildCount = count($project_env['latest']);
$lastSuccess = $project_env['success'];
$lastFailure = $project_env['failed'];
$message = Lang::get('no_builds_yet');
$shortMessage = Lang::get('no_builds_yet');
if ($buildCount > 0) {
if ($failures > 0) {
$shortMessage = Lang::get('x_of_x_failed_short', $failures, $buildCount);
$message = Lang::get('x_of_x_failed', $failures, $buildCount);
if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinishDate())) {
$message .= Lang::get('last_successful_build', $lastSuccess->getFinishDate()->format('Y-m-d H:i:s'));
} else {
$message .= Lang::get('never_built_successfully');
}
} else {
$message = Lang::get('all_builds_passed', $buildCount);
$shortMessage = Lang::get('all_builds_passed_short', $buildCount, $buildCount);
if (!is_null($lastFailure) && !is_null($lastFailure->getFinishDate())) {
$message .= Lang::get('last_failed_build', $lastFailure->getFinishDate()->format('Y-m-d H:i:s'));
} else {
$message .= Lang::get('never_failed_build');
}
}
}
?>
<div class="project-box">
<div class="small-box small-box-full bg-<?= $subcls; ?>">
<div class="inner">
<h3>
<a href="<?= APP_URL; ?>project/view/<?= $project->getId(); ?>">
<?= $project->getTitle(); ?>
</a>
<?php if (!empty($environment)) { ?>
<sup title="<?php Lang::out('environment'); ?>"><?= $environment ?></sup>
<?php } ?>
</h3>
<p>
<?= $message; ?>
</p>
</div>
<div class="icon">
<i class="fa fa-<?php print $project->getIcon(); ?>"></i>
</div>
<a href="<?= APP_URL; ?>project/view/<?= $project->getId(); ?>" class="small-box-footer small-box-footer-project">
<div class="pull-left" style="margin-left: 10px">
<?php if ($project->getAllowPublicStatus()): ?>
<i class="fa fa-unlock"></i>
<?php else: ?>
<i class="fa fa-lock"></i>
<?php endif; ?>
</div>
<?php Lang::out('view_project'); ?> <i class="fa fa-arrow-circle-right"></i>
</a>
<?php for ($idx=0; $idx < 5; $idx++) {
if (empty($project_env['latest'][$idx])) {
echo '<span class="small-box-footer-build small-box-footer bg-gray"><i class="fa fa-minus"></i></span>';
} else {
$build = $project_env['latest'][$idx];
$link = APP_URL . 'build/view/' . $build->id;
switch ($build->getStatus()) {
case 0:
$class = 'bg-blue';
$icon = 'fa-clock-o';
break;
case 1:
$class = 'bg-yellow';
$icon = 'fa-cogs';
break;
case 2:
$class = 'bg-green';
$icon = 'fa-check';
break;
case 3:
$class = 'bg-red';
$icon = 'fa-times';
break;
}
echo '<a href="' . $link .'" class="small-box-footer-build small-box-footer ' . $class . '"><i class="fa ' . $icon . '"></i></a>';
}
} ?>
<div style="clear: both;"></div>
</div>
</div>
<?php endforeach; ?>
<?php endforeach; ?>

View file

@ -0,0 +1,13 @@
<?php
use PHPCensor\Helper\Lang;
?>
<div class="box">
<div class="box-header">
<h3 class="box-title"><?php Lang::out('latest_builds'); ?></h3>
</div>
<div class="box-body" id="timeline-box">
<?= $timeline ?>
</div>
</div>