Merge pull request #485 from cachethq/schedules

Added scheduled maintenance. Closes #112 (again)
This commit is contained in:
Graham Campbell
2015-02-28 20:51:20 +00:00
36 changed files with 1034 additions and 115 deletions
+18 -1
View File
@@ -109,6 +109,23 @@ $(function() {
}
});
// Date picker.
$('input[rel=datepicker]').datetimepicker({
format: "DD/MM/YYYY HH:mm",
minDate: new Date(), // Don't allow dates before today.
sideBySide: true,
icons: {
time: 'ion-clock',
date: 'ion-android-calendar',
up: 'ion-ios-arrow-up',
down: 'ion-ios-arrow-down',
previous: 'ion-ios-arrow-left',
next: 'ion-ios-arrow-right',
today: 'ion-android-home',
clear: 'ion-trash-a',
}
});
// Sortable components.
var componentList = document.getElementById("component-list");
if (componentList) {
@@ -171,7 +188,7 @@ $(function() {
},
url: '/dashboard/api/incidents/templates',
success: function(tpl) {
var $form = $('form[name=IncidentForm]');
var $form = $('form[role=form]');
$form.find('input[name=incident\\[name\\]]').val(tpl.name);
$form.find('textarea[name=incident\\[message\\]]').val(tpl.template);
},
+9 -2
View File
@@ -144,8 +144,12 @@ body.status-page {
top: 14px;
.icon {
position: absolute;
&.ion-android-calendar {
top: 7px;
left: 11px;
}
&.ion-flag {
top: 10px;
top: 7px;
left: 13px;
}
&.ion-alert {
@@ -157,10 +161,13 @@ body.status-page {
left: 10px;
}
&.ion-checkmark {
top: 10px;
top: 7px;
left: 11px;
}
}
&.status-0 {
color: $cachet_pink;
}
&.status-1 {
color: $cachet_orange;
}
+1
View File
@@ -32,6 +32,7 @@ html, body {
// Styles for plugins
@import "plugins/messenger";
@import "plugins/animate";
@import "plugins/bootstrap-datetimepicker/bootstrap-datetimepicker";
// Status Page will need to override certain styles.
@import "status-page";
@@ -0,0 +1,301 @@
// Import boostrap variables including default color palette and fonts
@import "../bower_components/bootstrap-sass/assets/stylesheets/bootstrap/_variables";
.bootstrap-datetimepicker-widget {
top: 0;
left: 0;
width: 250px;
padding: 4px;
margin-top: 1px;
z-index: 99999 !important;
border-radius: $border-radius-base;
&.timepicker-sbs {
width: 600px;
}
&.bottom {
&:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0,0,0,.2);
position: absolute;
top: -7px;
left: 7px;
}
&:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
position: absolute;
top: -6px;
left: 8px;
}
}
&.top {
&:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid #ccc;
border-top-color: rgba(0,0,0,.2);
position: absolute;
bottom: -7px;
left: 6px;
}
&:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid white;
position: absolute;
bottom: -6px;
left: 7px;
}
}
& .dow {
width: 14.2857%;
}
&.pull-right {
&:before {
left: auto;
right: 6px;
}
&:after {
left: auto;
right: 7px;
}
}
>ul {
list-style-type: none;
margin: 0;
}
a[data-action] {
padding: 6px 0;
}
a[data-action]:active {
box-shadow: none;
}
.timepicker-hour, .timepicker-minute, .timepicker-second {
width: 54px;
font-weight: bold;
font-size: 1.2em;
margin: 0;
}
button[data-action] {
padding: 6px;
}
table[data-hour-format="12"] .separator {
width: 4px;
padding: 0;
margin: 0;
}
.datepicker > div {
display: none;
}
.picker-switch {
text-align: center;
}
table {
width: 100%;
margin: 0;
}
td,
th {
text-align: center;
border-radius: $border-radius-base;
}
td {
height: 54px;
line-height: 54px;
width: 54px;
&.cw {
font-size: 10px;
height: 20px;
line-height: 20px;
color: $gray-light;
}
&.day {
height: 20px;
line-height: 20px;
width: 20px;
}
&.day:hover,
&.hour:hover,
&.minute:hover,
&.second:hover {
background: $gray-lighter;
cursor: pointer;
}
&.old,
&.new {
color: $gray-light;
}
&.today {
position: relative;
&:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-bottom: 7px solid $btn-primary-bg;
border-top-color: rgba(0, 0, 0, 0.2);
position: absolute;
bottom: 4px;
right: 4px;
}
}
&.active,
&.active:hover {
background-color: $btn-primary-bg;
color: $btn-primary-color;
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}
&.active.today:before {
border-bottom-color: #fff;
}
&.disabled,
&.disabled:hover {
background: none;
color: $gray-light;
cursor: not-allowed;
}
span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: $border-radius-base;
&:hover {
background: $gray-lighter;
}
&.active {
background-color: $btn-primary-bg;
color: $btn-primary-color;
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}
&.old {
color: $gray-light;
}
&.disabled,
&.disabled:hover {
background: none;
color: $gray-light;
cursor: not-allowed;
}
}
}
th {
height: 20px;
line-height: 20px;
width: 20px;
&.picker-switch {
width: 145px;
}
&.next,
&.prev {
font-size: $font-size-base * 1.5;
}
&.disabled,
&.disabled:hover {
background: none;
color: $gray-light;
cursor: not-allowed;
}
}
thead tr:first-child th {
cursor: pointer;
&:hover {
background: $gray-lighter;
}
}
}
.input-group {
&.date {
.input-group-addon span {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
}
}
.bootstrap-datetimepicker-widget.left-oriented {
&:before {
left: auto;
right: 6px;
}
&:after {
left: auto;
right: 7px;
}
}
.bootstrap-datetimepicker-widget ul.list-unstyled li div.timepicker div.timepicker-picker table.table-condensed tbody > tr > td {
padding: 0px !important;
}
@media screen and (max-width: 767px) {
.bootstrap-datetimepicker-widget.timepicker-sbs {
width: 283px;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
@@ -20,6 +20,7 @@ class CreateIncidentsTable extends Migration
$table->integer('status');
$table->longText('message');
$table->integer('user_id');
$table->timestamp('scheduled_at');
$table->timestamps();
$table->softDeletes();
+3
View File
@@ -18,7 +18,10 @@ return [
'previous_week' => 'Previous week',
'next_week' => 'Next week',
'none' => 'Nothing to report',
'scheduled' => 'Scheduled Maintenance',
'scheduled_at' => ', scheduled :timestamp',
'status' => [
0 => 'Scheduled', // TODO: Hopefully remove this.
1 => 'Investigating',
2 => 'Identified',
3 => 'Watching',
+21
View File
@@ -6,6 +6,7 @@ return [
// Incidents
'incidents' => [
'title' => 'Incidents & Schedule',
'incidents' => 'Incidents',
'logged' => '{0} There are no incidents, good work.|You have logged one incident.|You have reported <strong>:count</strong> incidents.',
'incident-create-template' => 'Create Template',
@@ -37,6 +38,26 @@ return [
],
],
// Incident Maintenance
'schedule' => [
'schedule' => 'Scheduled Maintenance',
'scheduled_at' => 'Scheduled at :timestamp',
'add' => [
'title' => 'Add Scheduled Maintenance',
'success' => 'Schedule added.',
'failure' => 'Something went wrong adding the schedule.',
],
'edit' => [
'title' => 'Edit Scheduled Maintenance',
'success' => 'Schedule has been updated!',
'failure' => 'Something went wrong editing the schedule.',
],
'delete' => [
'success' => 'The schedule has been deleted and will not show on your status page.',
'failure' => 'The schedule could not be deleted. Please try again.',
],
],
// Components
'components' => [
'components' => 'Components',
+1
View File
@@ -30,6 +30,7 @@ return [
'component' => 'Component',
'message' => 'Message',
'message-help' => 'You may also use Markdown.',
'scheduled_at' => 'When to schedule the maintenance for?',
'templates' => [
'name' => 'Name',
+27 -1
View File
@@ -1,6 +1,10 @@
<?php
Route::group(['before' => 'auth', 'prefix' => 'dashboard', 'namespace' => 'CachetHQ\Cachet\Http\Controllers'], function () {
Route::group([
'before' => 'auth',
'prefix' => 'dashboard',
'namespace' => 'CachetHQ\Cachet\Http\Controllers',
], function () {
// Dashboard
Route::get('/', [
'as' => 'dashboard',
@@ -55,6 +59,28 @@ Route::group(['before' => 'auth', 'prefix' => 'dashboard', 'namespace' => 'Cache
Route::post('{incident}/edit', 'DashIncidentController@editIncidentAction');
});
// Scheduled Maintenance
Route::group(['prefix' => 'schedule'], function () {
Route::get('/', ['as' => 'dashboard.schedule', 'uses' => 'DashScheduleController@showIndex']);
Route::get('add', [
'as' => 'dashboard.schedule.add',
'uses' => 'DashScheduleController@showAddSchedule',
]);
Route::post('add', 'DashScheduleController@addScheduleAction');
Route::get('{incident}/edit', [
'as' => 'dashboard.schedule.edit',
'uses' => 'DashScheduleController@showEditSchedule',
]);
Route::post('{incident}/edit', 'DashScheduleController@editScheduleAction');
Route::delete('{incident}/delete', [
'as' => 'dashboard.schedule.delete',
'uses' => 'DashScheduleController@deleteScheduleAction',
]);
});
// Incident Templates
Route::group(['prefix' => 'templates'], function () {
Route::get('/', [
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="row">
<div class="col-md-12">
@include('partials.dashboard.errors')
<form class='form-vertical' name='IncidentForm' role='form' method='POST'>
<form class="form-vertical" name="IncidentForm" role="form" method="POST" autocomplete="off">
{{ Form::token() }}
<fieldset>
@if($incidentTemplates->count() > 0)
+1 -1
View File
@@ -14,7 +14,7 @@
<div class="row">
<div class="col-md-12">
@include('partials.dashboard.errors')
<form class='form-vertical' name='IncidentForm' role='form' method='POST'>
<form class="form-vertical" name="IncidentForm" role="form" method="POST" autocomplete="off">
{{ Form::token() }}
<fieldset>
<div class="form-group">
+32 -30
View File
@@ -1,39 +1,41 @@
@extends('layout.dashboard')
@section('content')
<div class="header fixed">
<div class="sidebar-toggler visible-xs">
<i class="icon ion-navicon"></i>
</div>
<span class="uppercase">
<i class="icon ion-android-alert"></i> {{ trans('dashboard.incidents.incidents') }}
</span>
<a class="btn btn-sm btn-success pull-right" href="{{ route('dashboard.incidents.add') }}">
{{ trans('dashboard.incidents.add.title') }}
</a>
<div class="clearfix"></div>
</div>
<div class="content-wrapper header-fixed">
<div class="row">
<div class="col-sm-12">
@include('partials.dashboard.errors')
<p class="lead">{{ trans_choice('dashboard.incidents.logged', $incidents->count(), ['count' => $incidents->count()]) }}</p>
<div class="content-panel">
@if(isset($subMenu))
@include('partials.dashboard.sub-sidebar')
@endif
<div class="content-wrapper">
<div class="header sub-header">
<span class="uppercase">
<i class="icon ion-android-alert"></i> {{ trans('dashboard.incidents.incidents') }}
</span>
<a class="btn btn-sm btn-success pull-right" href="{{ route('dashboard.incidents.add') }}">
{{ trans('dashboard.incidents.add.title') }}
</a>
<div class="clearfix"></div>
</div>
<div class="row">
<div class="col-sm-12">
@include('partials.dashboard.errors')
<p class="lead">{{ trans_choice('dashboard.incidents.logged', $incidents->count(), ['count' => $incidents->count()]) }}</p>
<div class="striped-list">
@foreach($incidents as $incident)
<div class="row striped-list-item">
<div class="col-md-6">
<i class="{{ $incident->icon }}"></i> <strong>{{ $incident->name }}</strong>
@if($incident->message)
<p><small>{{ Str::words($incident->message, 5) }}</small></p>
@endif
</div>
<div class="col-md-6 text-right">
<a href="/dashboard/incidents/{{ $incident->id }}/edit" class="btn btn-default">{{ trans('forms.edit') }}</a>
<a href="/dashboard/incidents/{{ $incident->id }}/delete" class="btn btn-danger confirm-action" data-method='DELETE'>{{ trans('forms.delete') }}</a>
<div class="striped-list">
@foreach($incidents as $incident)
<div class="row striped-list-item">
<div class="col-md-6">
<i class="{{ $incident->icon }}"></i> <strong>{{ $incident->name }}</strong>
@if($incident->message)
<p><small>{{ Str::words($incident->message, 5) }}</small></p>
@endif
</div>
<div class="col-md-6 text-right">
<a href="/dashboard/incidents/{{ $incident->id }}/edit" class="btn btn-default">{{ trans('forms.edit') }}</a>
<a href="/dashboard/incidents/{{ $incident->id }}/delete" class="btn btn-danger confirm-action" data-method='DELETE'>{{ trans('forms.delete') }}</a>
</div>
</div>
@endforeach
</div>
@endforeach
</div>
</div>
</div>
@@ -14,7 +14,7 @@
<div class="row">
<div class="col-md-12">
@include('partials.dashboard.errors')
<form class='form-vertical' name='IncidentTemplateForm' role='form' method='POST'>
<form class="form-vertical" name="IncidentForm" role="form" method="POST" autocomplete="off">
{{ Form::token() }}
<fieldset>
<div class="form-group">
@@ -0,0 +1,59 @@
@extends('layout.dashboard')
@section('content')
<div class="header">
<div class="sidebar-toggler visible-xs">
<i class="icon ion-navicon"></i>
</div>
<span class="uppercase">
<i class="icon ion-android-calendar"></i> {{ trans('dashboard.schedule.schedule') }}
</span>
&gt; <small>{{ trans('dashboard.schedule.add.title') }}</small>
</div>
<div class="content-wrapper">
<div class="row">
<div class="col-md-12">
@include('partials.dashboard.errors')
<form class='form-vertical' name='ScheduleForm' role='form' method='POST' autocomplete="off">
{{ Form::token() }}
<fieldset>
@if($incidentTemplates->count() > 0)
<div class="form-group">
<label for="incident-template">{{ trans('forms.incidents.templates.template') }}</label>
<select class="form-control" name="template">
<option selected></option>
@foreach($incidentTemplates as $tpl)
<option value="{{ $tpl->slug }}">{{ $tpl->name }}</option>
@endforeach
</select>
</div>
@endif
<div class="form-group">
<label for="incident-name">{{ trans('forms.incidents.name') }}</label>
<input type="text" class="form-control" name="incident[name]" id="incident-name" required value="{{ Input::old('incident.name') }}">
</div>
<div class="form-group">
<label>{{ trans('forms.incidents.message') }}</label>
<div class='markdown-control'>
<textarea name="incident[message]" class="form-control" rows="5" required>{{ Input::old('incident.message') }}</textarea>
</div>
</div>
<div class="form-group">
<label>{{ trans('forms.incidents.scheduled_at') }}</label>
<input type="text" name="incident[scheduled_at]" class="form-control" rel="datepicker" required>
</div>
</fieldset>
<input type="hidden" name="incident[user_id]" value="{{ $loggedUser->id }}">
<div class="form-group">
<div class="btn-group">
<button type="submit" class="btn btn-success">{{ trans('forms.add') }}</button>
<a class="btn btn-default" href="{{ route('dashboard.schedule') }}">{{ trans('forms.cancel') }}</a>
</div>
</div>
</form>
</div>
</div>
</div>
@stop
@@ -0,0 +1,59 @@
@extends('layout.dashboard')
@section('content')
<div class="header">
<div class="sidebar-toggler visible-xs">
<i class="icon ion-navicon"></i>
</div>
<span class="uppercase">
<i class="icon ion-android-calendar"></i> {{ trans('dashboard.schedule.schedule') }}
</span>
&gt; <small>{{ trans('dashboard.schedule.edit.title') }}</small>
</div>
<div class="content-wrapper">
<div class="row">
<div class="col-md-12">
@include('partials.dashboard.errors')
<form class='form-vertical' name='ScheduleForm' role='form' method='POST' autocomplete="off">
{{ Form::token() }}
<fieldset>
@if($incidentTemplates->count() > 0)
<div class="form-group">
<label for="incident-template">{{ trans('forms.incidents.templates.template') }}</label>
<select class="form-control" name="template">
<option selected></option>
@foreach($incidentTemplates as $tpl)
<option value="{{ $tpl->slug }}">{{ $tpl->name }}</option>
@endforeach
</select>
</div>
@endif
<div class="form-group">
<label for="incident-name">{{ trans('forms.incidents.name') }}</label>
<input type="text" class="form-control" name="incident[name]" id="incident-name" required value="{{ $schedule->name }}">
</div>
<div class="form-group">
<label>{{ trans('forms.incidents.message') }}</label>
<div class='markdown-control'>
<textarea name="incident[message]" class="form-control" rows="5" required>{{ $schedule->message }}</textarea>
</div>
</div>
<div class="form-group">
<label>{{ trans('forms.incidents.scheduled_at') }}</label>
<input type="text" name="incident[scheduled_at]" class="form-control" rel="datepicker" value="{{ $schedule->scheduled_at_datetimepicker }}" required>
</div>
</fieldset>
<input type="hidden" name="incident[user_id]" value="{{ $loggedUser->id }}">
<div class="form-group">
<div class="btn-group">
<button type="submit" class="btn btn-success">{{ trans('forms.save') }}</button>
<a class="btn btn-default" href="{{ route('dashboard.schedule') }}">{{ trans('forms.cancel') }}</a>
</div>
</div>
</form>
</div>
</div>
</div>
@stop
@@ -0,0 +1,44 @@
@extends('layout.dashboard')
@section('content')
<div class="content-panel">
@if(isset($subMenu))
@include('partials.dashboard.sub-sidebar')
@endif
<div class="content-wrapper">
<div class="header sub-header">
<span class="uppercase">
<i class="icon ion-android-alert"></i> {{ trans('dashboard.schedule.schedule') }}
</span>
<a class="btn btn-sm btn-success pull-right" href="{{ route('dashboard.schedule.add') }}">
{{ trans('dashboard.schedule.add.title') }}
</a>
<div class="clearfix"></div>
</div>
<div class="row">
<div class="col-sm-12">
@include('partials.dashboard.errors')
<div class="striped-list">
@foreach($schedule as $incident)
<div class="row striped-list-item">
<div class="col-md-6">
<strong>{{ $incident->name }}</strong>
<br>
{{ trans('dashboard.schedule.scheduled_at', ['timestamp' => $incident->scheduled_at_iso]) }}
@if($incident->message)
<p><small>{{ Str::words($incident->message, 5) }}</small></p>
@endif
</div>
<div class="col-md-6 text-right">
<a href="{{ route('dashboard.schedule.edit', [$incident->id]) }}" class="btn btn-default">{{ trans('forms.edit') }}</a>
<a href="{{ route('dashboard.schedule.delete', [$incident->id]) }}" class="btn btn-danger confirm-action" data-method='DELETE'>{{ trans('forms.delete') }}</a>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
@stop
+4
View File
@@ -25,6 +25,10 @@
@include('partials.graphs')
@endif
@if(!$scheduledMaintenance->isEmpty())
@include('partials.schedule')
@endif
<h1>{{ trans('cachet.incidents.past') }}</h1>
@foreach($allIncidents as $incidents)
@include('partials.incidents', $incidents)
@@ -35,7 +35,7 @@
<span>{{ trans('dashboard.dashboard') }}</span>
</a>
</li>
<li {{ set_active('dashboard/incidents*') }}>
<li {{ set_active('dashboard/incidents*') }} {{ set_active('dashboard/schedule*') }}>
<a href="{{ route('dashboard.incidents') }}">
<i class="icon ion-android-alert"></i>
<span>{{ trans('dashboard.incidents.incidents') }}</span>
+1 -1
View File
@@ -12,7 +12,7 @@
<div class="col-xs-10 col-xs-offset-2 col-sm-11 col-sm-offset-0">
<div class="panel panel-message">
<div class="panel-heading">
<strong>{{ $incident->name }}</strong>
<strong>{{ $incident->name }}</strong>{{ $incident->isScheduled ? trans("cachet.incidents.scheduled_at", ["timestamp" => $incident->scheduled_at->diffForHumans()]) : null }}
<br>
<small class="date">
<abbr class="timeago" data-toggle="tooltip" data-placement="right" title="{{ $incident->created_at_formatted }}" data-timeago="{{ $incident->created_at_iso }}">
+19
View File
@@ -0,0 +1,19 @@
<h1>{{ trans('cachet.incidents.scheduled') }}</h1>
<div class="timeline">
@foreach($scheduledMaintenance as $schedule)
<div class="panel panel-message">
<div class="panel-heading">
<strong>{{ $schedule->name }}</strong>
<br>
<small class="date">
<abbr class="timeago" data-toggle="tooltip" data-placement="right" title="{{ $schedule->scheduled_at_formatted }}" data-timeago="{{ $schedule->scheduled_at_iso }}">
</abbr>
</small>
</div>
<div class="panel-body">
<p>{{ $schedule->formattedMessage }}</p>
</div>
</div>
@endforeach
</div>