Added scheduled maintenance. Closes #112 (again)

This commit is contained in:
James Brooks
2015-02-28 20:18:30 +00:00
parent cf6f38d6d1
commit 3268924d99
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_formated }}" 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>
+2 -1
View File
@@ -13,7 +13,8 @@
"animate-sass": "~0.6.2",
"moment": "~2.9",
"livestampjs": "~1.1.2",
"chartjs": "~1.0.1"
"chartjs": "~1.0.1",
"eonasdan-bootstrap-datetimepicker": "~4.0.0"
},
"resolutions": {
"jquery": "~2.1.1"
+4 -2
View File
@@ -3,7 +3,8 @@ var elixir = require('laravel-elixir');
require('laravel-elixir-jshint');
elixir(function (mix) {
mix.sass('app/assets/sass/main.scss')
mix
.sass('app/assets/sass/main.scss')
.jshint('app/assets/js/*.js')
.styles([
'app/assets/bower_components/ionicons/css/ionicons.css',
@@ -13,10 +14,11 @@ elixir(function (mix) {
.scripts([
'bower_components/jquery/dist/jquery.js',
'bower_components/bootstrap-sass/assets/javascripts/bootstrap.js',
'bower_components/moment/min/moment-with-locales.js',
'bower_components/eonasdan-bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js',
'bower_components/lodash/dist/lodash.js',
'bower_components/messenger/build/js/messenger.js',
'bower_components/Sortable/Sortable.js',
'bower_components/moment/min/moment-with-locales.js',
'bower_components/livestampjs/livestamp.js',
'bower_components/jquery-minicolors/jquery.minicolors.js',
'bower_components/jquery-serialize-object/jquery.serialize-object.js',
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
{
"dist/css/all.css": "dist/css/all-aeb15992.css",
"dist/js/all.js": "dist/js/all-3f815dae.js"
"dist/css/all.css": "dist/css/all-0b452dcd.css",
"dist/js/all.js": "dist/js/all-d4baa2f5.js"
}
Binary file not shown.
+36 -38
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2014-12-3: Created.
2014-12-4: Created.
-->
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
Created by FontForge 20120731 at Wed Dec 3 12:38:33 2014
Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014
By Adam Bradley
Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net)
</metadata>
@@ -1046,19 +1046,16 @@ d="M103 243c11 0 19 -8 19 -19s-8 -19 -19 -19s-19 8 -19 19s8 19 19 19zM434 199c21
s43 136 88 149c10 3 18 4 26 4c28 0 47 -15 96 -15s68 15 96 15c8 0 16 -1 26 -4c45 -13 67 -61 88 -149zM103 184c22 0 39 18 39 40s-17 40 -39 40s-39 -18 -39 -40s17 -40 39 -40zM276 204c11 0 20 9 20 20s-9 20 -20 20s-20 -9 -20 -20s9 -20 20 -20zM320 160
c11 0 20 9 20 20s-9 20 -20 20s-20 -9 -20 -20s9 -20 20 -20zM320 248c11 0 20 9 20 20s-9 20 -20 20s-20 -9 -20 -20s9 -20 20 -20zM364 204c11 0 20 9 20 20s-9 20 -20 20s-20 -9 -20 -20s9 -20 20 -20z" />
<glyph glyph-name="ion-ios-gear-outline" unicode="&#xf43c;" horiz-adv-x="384"
d="M193 288c26 0 50 -10 68 -28s28 -42 28 -68s-10 -50 -28 -68s-42 -28 -68 -28s-50 10 -68 28s-28 42 -28 68s10 50 28 68s42 28 68 28zM193 112c44 0 80 36 80 80s-36 80 -80 80s-80 -36 -80 -80s36 -80 80 -80zM360 232c14 0 24 -11 24 -25v-15v-15
c0 -14 -10 -24 -24 -24h-15c-12 0 -23 -11 -23 -24c0 -6 3 -13 8 -17l10 -9c10 -10 10 -25 0 -35l-22 -22c-4 -4 -11 -7 -18 -7s-14 3 -18 7l-9 9c-5 5 -11 8 -17 8c-13 0 -23 -11 -23 -23v-15c0 -14 -11 -25 -25 -25h-30c-14 0 -25 11 -25 25v15c0 12 -10 23 -23 23
c-6 0 -13 -3 -17 -8l-10 -9c-4 -4 -10 -7 -17 -7s-14 3 -18 7l-22 22c-10 10 -10 25 0 35l9 9c5 4 8 11 8 17c0 13 -11 23 -23 23h-15c-14 0 -25 11 -25 25v15v15c0 14 11 25 25 25h15c12 0 23 10 23 23c0 6 -3 13 -8 17l-9 9c-10 10 -10 25 0 35l22 22c4 4 11 7 18 7
s13 -3 17 -7l10 -10c4 -5 11 -7 17 -7c13 0 23 11 23 23v15c0 14 11 25 25 25h30c14 0 24 -11 24 -25v-15c0 -12 11 -23 24 -23c6 0 13 3 17 8l9 9c4 4 11 7 18 7s14 -3 18 -7l22 -22c10 -10 10 -25 0 -35l-10 -9c-5 -4 -8 -11 -8 -17c0 -13 11 -23 23 -23h15zM368 192v15
c0 4 -3 9 -8 9h-15c-10 0 -20 4 -27 12s-12 17 -12 27c0 11 5 21 13 28l9 9c2 2 3 5 3 7s-1 4 -3 6l-22 22c-2 1 -4 2 -6 2s-4 0 -6 -2l-10 -9c-8 -8 -17 -13 -28 -13c-10 0 -20 4 -28 11s-12 18 -12 28v15c0 5 -4 9 -8 9h-30c-4 0 -9 -4 -9 -9v-15c0 -10 -4 -21 -12 -28
s-17 -11 -27 -11c-11 0 -21 4 -28 12l-10 10v0v0c-1 1 -4 2 -6 2s-4 0 -6 -2l-23 -22c-2 -2 -2 -5 -2 -6s0 -5 2 -7l9 -9c8 -8 13 -17 13 -28c0 -10 -4 -19 -11 -27s-18 -12 -28 -12h-15c-5 0 -9 -5 -9 -9v-15v-15c0 -4 4 -9 9 -9h15c10 0 21 -4 28 -12s11 -17 11 -27
c0 -11 -4 -21 -12 -29l-9 -8c-3 -3 -3 -10 0 -13l22 -22v0v0c1 -1 4 -2 6 -2s4 0 6 2l10 9c7 8 17 13 28 13c10 0 19 -4 27 -11s12 -18 12 -28v-15c0 -5 4 -9 9 -9h30c5 0 9 4 9 9v15c0 10 4 21 12 28s17 11 27 11c11 0 21 -5 29 -13l9 -9c2 -2 4 -2 6 -2s4 0 6 2l23 22
c3 3 3 10 0 13l-10 9c-8 7 -12 17 -12 28c0 10 4 20 11 28s18 12 28 12h15c5 0 7 4 7 8v15z" />
d="M193 288c26 0 50 -10 68 -28s28 -42 28 -68s-10 -50 -28 -68s-42 -28 -68 -28s-50 10 -68 28s-28 42 -28 68s10 50 28 68s42 28 68 28zM193 112c44 0 80 36 80 80s-36 80 -80 80s-80 -36 -80 -80s36 -80 80 -80zM138 365c-10 -3 -20 -7 -30 -12c2 -8 1 -16 0 -24
c-2 -13 -8 -25 -18 -35c-12 -12 -29 -19 -46 -19c-4 0 -9 0 -13 1c-5 -10 -9 -20 -12 -30c7 -4 12 -10 17 -17c8 -11 12 -24 12 -37s-4 -26 -12 -37c-5 -7 -10 -13 -17 -17c3 -10 7 -20 12 -30c4 1 9 1 13 1c17 0 34 -7 46 -19c10 -10 16 -22 18 -35c1 -8 2 -16 0 -24
c10 -5 20 -9 30 -12c4 7 10 12 17 17c11 8 24 12 37 12s26 -4 37 -12c7 -5 13 -10 17 -17c10 3 20 7 30 12c-2 8 -1 16 0 24c2 13 8 25 18 35c12 12 29 19 46 19c4 0 9 0 13 -1c5 10 9 20 12 30c-7 4 -12 10 -17 17c-8 11 -12 24 -12 37s4 26 12 37c5 7 10 13 17 17
c-3 10 -7 20 -12 30c-4 -1 -9 -1 -13 -1c-17 0 -34 7 -46 19c-10 10 -16 22 -18 35c-1 8 -2 16 0 24c-10 5 -20 9 -30 12c-4 -7 -10 -12 -17 -17c-11 -8 -24 -12 -37 -12s-26 4 -37 12c-7 5 -13 10 -17 17zM238 384v0c20 -5 40 -13 57 -24c-8 -18 -5 -40 10 -55
c10 -10 22 -14 35 -14c7 0 14 1 20 4c11 -17 19 -37 24 -57c-19 -7 -32 -25 -32 -46s14 -39 32 -46c-5 -20 -13 -40 -24 -57c-6 3 -13 4 -20 4c-13 0 -25 -4 -35 -14c-15 -15 -18 -37 -10 -55c-17 -11 -37 -19 -57 -24c-7 18 -25 32 -46 32s-39 -14 -46 -32
c-20 5 -40 13 -57 24c8 18 5 40 -10 55c-10 10 -22 14 -35 14c-7 0 -14 -1 -20 -4c-11 17 -19 37 -24 57c18 7 32 25 32 46s-13 39 -32 46c5 20 13 40 24 57c6 -3 13 -4 20 -4c13 0 25 4 35 14c15 15 18 37 10 55c17 11 37 19 57 24c7 -19 25 -32 46 -32s39 13 46 32z" />
<glyph glyph-name="ion-ios-gear" unicode="&#xf43d;" horiz-adv-x="384"
d="M360 232c14 0 24 -11 24 -25v-15v-15c0 -14 -10 -25 -24 -25h-15c-12 0 -23 -10 -23 -23c0 -6 3 -13 8 -17l10 -9c10 -10 10 -25 0 -35l-23 -22c-4 -4 -10 -7 -17 -7s-14 3 -18 7l-9 9c-5 5 -11 8 -17 8c-13 0 -24 -11 -24 -23v-15c0 -14 -10 -25 -24 -25h-30
c-14 0 -25 11 -25 25v15c0 12 -10 23 -23 23c-6 0 -13 -2 -17 -7l-10 -10c-4 -4 -10 -7 -17 -7s-14 3 -18 7l-22 22c-10 10 -10 25 0 35l9 9c5 4 8 11 8 17c0 13 -11 23 -23 23h-15c-14 0 -25 11 -25 25v15v15c0 14 11 25 25 25h15c12 0 23 10 23 23c0 6 -3 13 -8 17l-9 9
c-10 10 -10 25 0 35l22 22c4 4 11 7 18 7s14 -3 18 -7l9 -10c4 -5 11 -7 17 -7c13 0 23 11 23 23v15c0 14 11 25 25 25h30c14 0 25 -11 25 -25v-15c0 -12 10 -23 23 -23c6 0 13 3 17 8l9 9c4 4 11 7 18 7s14 -3 18 -7l22 -22c10 -10 10 -25 0 -35l-10 -10
c-5 -4 -8 -10 -8 -16c0 -13 11 -23 23 -23h15zM273 192v0c0 44 -36 80 -80 80s-80 -36 -80 -80v0v0c0 -44 36 -80 80 -80s80 36 80 80v0z" />
d="M352 192c0 -21 14 -39 32 -46c-5 -20 -13 -40 -24 -57c-6 3 -13 4 -20 4c-13 0 -25 -4 -35 -14c-15 -15 -18 -37 -10 -55c-17 -11 -37 -19 -57 -24c-7 18 -25 32 -46 32s-39 -14 -46 -32c-20 5 -40 13 -57 24c8 18 5 40 -10 55c-10 10 -22 14 -35 14c-7 0 -14 -1 -20 -4
c-11 17 -19 37 -24 57c18 7 32 25 32 46s-13 39 -32 46c5 20 13 40 24 57c6 -3 13 -4 20 -4c13 0 25 4 35 14c15 15 18 37 10 55c17 11 37 19 57 24c7 -19 25 -32 46 -32s39 13 46 32c20 -5 40 -13 57 -24c-8 -18 -5 -40 10 -55c10 -10 22 -14 35 -14c7 0 14 1 20 4
c11 -17 19 -37 24 -57c-19 -7 -32 -25 -32 -46zM193 112c44 0 80 36 80 80s-36 80 -80 80s-80 -36 -80 -80s36 -80 80 -80z" />
<glyph glyph-name="ion-ios-glasses-outline" unicode="&#xf43e;"
d="M433 201v0h15v-18h-15c-2 -22 -13 -43 -29 -58c-17 -16 -39 -25 -62 -25c-51 0 -92 41 -92 92v0v0c0 10 -12 22 -26 22s-26 -12 -26 -22v0v0c0 -51 -41 -92 -92 -92c-23 0 -45 9 -62 25c-16 15 -27 36 -29 58h-15v18h15c2 22 13 43 29 59c17 16 39 24 62 24
c42 0 78 -27 89 -67c7 7 18 12 29 12s22 -5 29 -12c11 40 47 67 89 67c23 0 45 -9 62 -25c16 -15 27 -36 29 -58zM342 115c42 0 77 35 77 77s-35 77 -77 77s-77 -35 -77 -77s35 -77 77 -77zM106 115c42 0 77 35 77 77s-35 77 -77 77s-77 -35 -77 -77s35 -77 77 -77z" />
@@ -1084,15 +1081,15 @@ c1 28 7 43 30 64c11 10 19 23 19 38c0 24 -19 40 -42 40c-32 0 -49 -16 -48 -46h-19c
d="M208 400c115 0 208 -93 208 -208s-93 -208 -208 -208s-208 93 -208 208s93 208 208 208zM212 82c9 0 17 8 17 17s-8 17 -17 17s-17 -8 -17 -17s8 -17 17 -17zM246 197c15 14 26 29 26 51c0 34 -27 54 -60 54c-43 0 -68 -20 -68 -62h19c-1 30 16 46 48 46
c23 0 42 -15 42 -39c0 -15 -8 -29 -19 -39c-23 -21 -29 -36 -30 -64h19c1 26 0 31 23 53z" />
<glyph glyph-name="ion-ios-home-outline" unicode="&#xf447;" horiz-adv-x="384"
d="M192 336l160 -128v-208h-112v128h-96v-128h-112v208zM336 16v184l-144 116l-144 -116v-184h80v128h128v-128h80zM192 384l192 -154v-20l-192 154l-192 -154v20l32 26v96h64v-45zM80 294v42h-32v-67z" />
d="M192 336l160 -128v-208h-112v128h-96v-128h-112v208zM336 16v184l-144 116l-144 -116v-184h80v128h128v-128h80zM192 384l192 -153l-12 -12l-180 145l-180 -145l-12 12l32 25v96h64v-45zM80 294v42h-32v-67z" />
<glyph glyph-name="ion-ios-home" unicode="&#xf448;" horiz-adv-x="384"
d="M192 336l160 -128v-208h-112v128h-96v-128h-112v208zM192 384l192 -154v-20l-192 154l-192 -154v20l32 26v96h64v-45z" />
<glyph glyph-name="ion-ios-infinite-outline" unicode="&#xf449;" horiz-adv-x="384"
d="M359 249c33 -31 33 -82 0 -113c-16 -15 -36 -24 -59 -24c-22 0 -44 9 -60 24l-108 102c-13 12 -30 18 -48 18s-35 -6 -48 -18c-26 -25 -26 -66 0 -91c13 -12 30 -19 48 -19s35 7 48 19l36 34l12 -11l-36 -34c-16 -15 -37 -24 -60 -24s-43 9 -59 24c-33 31 -33 82 0 113
c16 15 36 23 59 23c22 0 44 -8 60 -23l108 -102c13 -12 30 -19 48 -19s34 7 47 19c26 25 26 66 0 91c-13 12 -29 18 -47 18s-35 -6 -48 -18l-37 -35l-11 11l36 35c16 15 37 23 60 23s43 -8 59 -23z" />
<glyph glyph-name="ion-ios-infinite" unicode="&#xf44a;" horiz-adv-x="384"
d="M358 249c17 -15 26 -36 26 -57c0 -22 -9 -42 -26 -57s-39 -23 -63 -23s-45 8 -62 23l-103 93c-11 10 -26 15 -41 15s-30 -5 -41 -15s-16 -22 -16 -36s5 -26 16 -36s26 -15 41 -15s30 5 41 15l30 28l23 -20l-32 -29c-17 -15 -38 -23 -62 -23s-46 8 -63 23s-26 35 -26 57
s9 42 26 57s39 23 63 23s45 -8 62 -23l104 -93c11 -10 25 -15 40 -15s30 5 41 15s17 22 17 36s-6 26 -17 36s-26 15 -41 15s-29 -5 -40 -15l-31 -28l-23 20l32 29c17 15 38 23 62 23s46 -8 63 -23z" />
d="M192 336l160 -128v-208h-112v128h-96v-128h-112v208zM192 384l192 -153l-12 -12l-180 145l-180 -145l-12 12l32 25v96h64v-45z" />
<glyph glyph-name="ion-ios-infinite-outline" unicode="&#xf449;"
d="M419 260c19 -19 29 -43 29 -68s-10 -49 -29 -68c-19 -18 -44 -28 -70 -28s-50 10 -69 28l-126 123c-15 15 -35 22 -56 22s-40 -7 -55 -22c-31 -30 -31 -80 0 -110c15 -15 34 -22 55 -22s41 7 56 22l43 42l13 -14l-42 -41c-19 -18 -44 -28 -70 -28s-50 10 -69 28
c-19 19 -29 43 -29 68s10 49 29 68c19 18 43 28 69 28s51 -10 70 -28l126 -123c15 -15 34 -22 55 -22s41 7 56 22c31 30 31 80 0 110c-15 15 -35 22 -56 22s-40 -7 -55 -22l-43 -42l-13 14l42 41c19 18 44 28 70 28s50 -10 69 -28z" />
<glyph glyph-name="ion-ios-infinite" unicode="&#xf44a;" horiz-adv-x="464"
d="M433 266c20 -20 31 -46 31 -74s-11 -54 -31 -74s-48 -30 -76 -30s-55 10 -75 30l-125 123c-13 13 -32 20 -51 20s-37 -7 -50 -20s-21 -31 -21 -49c0 -19 8 -36 21 -49s31 -20 50 -20s38 7 51 20l39 38l25 -25l-39 -38c-20 -20 -48 -30 -76 -30s-55 10 -75 30
s-31 46 -31 74s11 54 31 74s47 30 75 30s56 -10 76 -30l125 -123c13 -13 31 -20 50 -20s38 7 51 20s20 31 20 49c0 19 -7 36 -20 49s-32 20 -51 20s-37 -7 -50 -20l-39 -38l-25 25l39 38c20 20 48 30 76 30s55 -10 75 -30z" />
<glyph glyph-name="ion-ios-information-empty" unicode="&#xf44b;" horiz-adv-x="64"
d="M8 276c0 13 7 20 20 20s20 -7 20 -20s-7 -20 -20 -20s-20 7 -20 20zM48 104h16v-8h-64v8h16v120h-16v8h48v-128z" />
<glyph glyph-name="ion-ios-information-outline" unicode="&#xf44c;" horiz-adv-x="416"
@@ -1128,11 +1125,11 @@ s-16 5 -16 16zM64 96c0 11 5 16 16 16s16 -5 16 -16s-5 -16 -16 -16s-16 5 -16 16z"
<glyph glyph-name="ion-ios-list" unicode="&#xf454;" horiz-adv-x="384"
d="M0 384h384v-384h-384v384zM80 80c9 0 16 7 16 16s-7 16 -16 16s-16 -7 -16 -16s7 -16 16 -16zM80 176c9 0 16 7 16 16s-7 16 -16 16s-16 -7 -16 -16s7 -16 16 -16zM80 272c9 0 16 7 16 16s-7 16 -16 16s-16 -7 -16 -16s7 -16 16 -16zM320 88v16h-192v-16h192zM320 184v16
h-192v-16h192zM320 280v16h-192v-16h192z" />
<glyph glyph-name="ion-ios-location-outline" unicode="&#xf455;" horiz-adv-x="268"
d="M134 416c74 0 134 -59 134 -133c0 -17 -3 -34 -9 -50l-1 -1c0 -1 -1 -3 -1 -4l-1 -1l-122 -259l-122 260c0 1 -2 3 -2 4v0v1v0c-6 16 -10 34 -10 50c0 74 60 133 134 133zM243 239c6 14 9 29 9 44c0 65 -53 117 -118 117s-118 -52 -118 -117c0 -15 3 -31 9 -45
c0 -1 1 -1 1 -2l1 -2l107 -227l107 227l1 2c0 1 1 1 1 2v1zM134 352c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM134 241c26 0 47 21 47 47s-21 47 -47 47s-47 -21 -47 -47s21 -47 47 -47z" />
<glyph glyph-name="ion-ios-location" unicode="&#xf456;" horiz-adv-x="268"
d="M134 416c74 0 134 -59 134 -133c0 -17 -3 -34 -9 -50l-1 -1c0 -1 -1 -3 -1 -4l-1 -1l-122 -259l-122 260c0 1 -2 3 -2 4v1v0c-6 16 -10 34 -10 50c0 74 60 133 134 133zM134 241c26 0 47 21 47 47s-21 47 -47 47s-47 -21 -47 -47s21 -47 47 -47z" />
<glyph glyph-name="ion-ios-location-outline" unicode="&#xf455;" horiz-adv-x="288"
d="M144 400c-34 0 -67 -13 -91 -37s-37 -57 -37 -91c0 -43 24 -107 70 -186c22 -38 44 -72 58 -91c14 19 36 53 58 91c46 79 70 143 70 186c0 34 -13 67 -37 91s-57 37 -91 37zM144 416v0c80 0 144 -64 144 -144c0 -112 -144 -304 -144 -304s-144 192 -144 304
c0 80 64 144 144 144zM144 336c35 0 64 -29 64 -64s-29 -64 -64 -64s-64 29 -64 64s29 64 64 64zM144 225c26 0 47 21 47 47s-21 47 -47 47s-47 -21 -47 -47s21 -47 47 -47z" />
<glyph glyph-name="ion-ios-location" unicode="&#xf456;" horiz-adv-x="288"
d="M144 416c80 0 144 -64 144 -144c0 -112 -144 -304 -144 -304s-144 192 -144 304c0 80 64 144 144 144zM144 225c26 0 47 21 47 47s-21 47 -47 47s-47 -21 -47 -47s21 -47 47 -47z" />
<glyph glyph-name="ion-ios-locked-outline" unicode="&#xf457;" horiz-adv-x="320"
d="M264 224h56v-240h-320v240h56v72c0 57 47 104 104 104s104 -47 104 -104v-72zM72 296v-72h176v72c0 49 -39 88 -88 88s-88 -39 -88 -88zM304 0v208h-288v-208h288zM160 160c18 0 32 -14 32 -32c0 -15 -10 -27 -24 -31v-33h-16v33c-14 4 -24 16 -24 31c0 18 14 32 32 32z
M160 112c9 0 16 7 16 16s-7 16 -16 16s-16 -7 -16 -16s7 -16 16 -16z" />
@@ -1268,12 +1265,10 @@ d="M64 320h384v-320h-384v320zM432 16v288h-352v-288h352zM0 384h384v-48h-16v32h-35
<glyph glyph-name="ion-ios-photos" unicode="&#xf482;"
d="M64 320h384v-320h-384v320zM384 384v-48h-336v-272h-48v320h384z" />
<glyph glyph-name="ion-ios-pie-outline" unicode="&#xf483;"
d="M32 176h208v208c5 0 11 -1 16 -1c17 -1 33 -4 49 -9c24 -8 46 -21 65 -36c47 -38 78 -96 78 -162c0 -115 -93 -208 -208 -208c-66 0 -124 31 -162 78c-15 19 -28 41 -36 65c-5 16 -8 32 -9 49c0 5 -1 11 -1 16zM49 160c2 -20 6 -40 14 -59c7 -16 15 -30 26 -44
c5 -6 10 -12 15 -17c12 -12 24 -22 38 -30c7 -4 15 -8 23 -11c24 -10 49 -15 75 -15s51 5 75 15c23 10 43 23 61 41s31 38 41 61c10 24 15 49 15 75s-5 51 -15 75c-3 8 -8 15 -12 22c-8 14 -17 27 -29 39c-5 5 -11 10 -17 15c-14 11 -28 19 -44 26c-19 8 -39 12 -59 14v-191
v-16h-16h-191zM0 208c0 115 93 208 208 208c5 0 11 -1 16 -1v-207v-16h-16h-207c0 5 -1 11 -1 16zM16 208h192v192c-26 0 -51 -5 -75 -15c-23 -10 -43 -23 -61 -41s-31 -38 -41 -61c-10 -24 -15 -49 -15 -75z" />
d="M256 367v0v-207v-12l-12 -4l-180 -45c6 -13 13 -25 21 -36c12 -16 25 -30 41 -42c33 -24 73 -37 114 -37c26 0 51 5 75 15c23 10 43 23 61 41s31 38 41 61c10 24 15 49 15 75c0 51 -20 100 -56 136c-32 32 -75 51 -120 55zM240 384v0c115 0 208 -93 208 -208
s-93 -208 -208 -208c-92 0 -171 60 -198 143l198 49v224zM208 400c-31 -1 -60 -7 -85 -18c-24 -11 -44 -26 -61 -46c-29 -34 -46 -80 -46 -127v0v0c0 -13 3 -46 12 -70l180 46v215zM224 416v0v-244l-205 -52c-19 32 -19 89 -19 89c0 91 58 207 218 207h6z" />
<glyph glyph-name="ion-ios-pie" unicode="&#xf484;"
d="M32 176h208v208c5 0 11 -1 16 -1c17 -1 33 -4 49 -9c24 -8 46 -21 65 -36c47 -38 78 -96 78 -162c0 -115 -93 -208 -208 -208c-66 0 -124 31 -162 78c-15 19 -28 41 -36 65c-5 16 -8 32 -9 49c0 5 -1 11 -1 16zM0 208c0 115 93 208 208 208c5 0 11 -1 16 -1v-207v-16h-16
h-207c0 5 -1 11 -1 16z" />
d="M240 384v0c115 0 208 -93 208 -208s-93 -208 -208 -208c-92 0 -171 60 -198 143l198 49v224zM224 416v0v-244l-205 -52c-19 32 -19 89 -19 89c0 91 58 207 218 207h6z" />
<glyph glyph-name="ion-ios-pint-outline" unicode="&#xf485;" horiz-adv-x="224"
d="M224 278c0 -98 -32 -101 -32 -181c0 -40 16 -71 16 -99c0 -27 -9 -30 -32 -30h-128c-23 0 -32 2 -32 29c0 28 16 60 16 100c0 80 -32 83 -32 181c0 21 1 89 19 125c4 9 13 13 32 13h122c19 0 28 -4 32 -13c18 -36 19 -104 19 -125zM34 396c-8 -16 -14 -43 -17 -76h190
c-3 33 -9 60 -17 76c-1 2 -1 1 -2 2c-2 1 -6 2 -15 2h-122c-9 0 -13 -1 -15 -2c-1 -1 -1 0 -2 -2zM191 -15c0 1 1 5 1 13c0 11 -4 23 -7 37c-4 18 -9 39 -9 62c0 41 8 64 16 86c8 23 16 46 16 95c0 9 -1 18 -1 26h-190c0 -8 -1 -17 -1 -26c0 -49 8 -72 16 -95
@@ -1392,13 +1387,16 @@ c-8 -9 -18 -15 -30 -19c-4 -1 -9 0 -10 4s1 10 5 11c8 3 15 6 21 12l-29 16c-4 2 -5
s8 -4 8 -8v-33c8 2 15 7 21 12c3 3 8 2 11 -1s2 -8 -1 -11c-9 -8 -20 -13 -31 -16v-73l64 36c-3 11 -4 24 -2 36c1 4 6 7 10 6s7 -6 6 -10c-2 -8 -1 -15 1 -23l29 16c4 2 9 1 11 -3s1 -9 -3 -11l-29 -16c6 -6 13 -10 21 -13c4 -1 6 -6 5 -10s-6 -6 -10 -5
c-12 4 -23 10 -31 19l-64 -36l64 -36c8 8 19 15 31 19c4 1 9 -1 10 -5s-1 -9 -5 -10c-8 -3 -15 -7 -21 -13z" />
<glyph glyph-name="ion-ios-speedometer-outline" unicode="&#xf4af;"
d="M448 163v0v0c0 -63 -27 -120 -70 -160l-2 -3l-37 37l11 11l26 -25c16 17 28 36 38 57c10 23 17 47 18 72h-32v16h32c-1 26 -6 51 -16 75c-9 22 -22 42 -38 59l-28 -28l-6 6l-5 5v0h-1l28 28c-18 17 -38 30 -61 40s-48 14 -73 15v-37h-16v37c-25 -1 -50 -5 -73 -15
s-43 -23 -61 -40l27 -28v0v0l-5 -5l-6 -6l-28 28c-16 -17 -29 -37 -38 -59c-10 -24 -15 -49 -16 -75h32v-16h-32c1 -25 8 -49 18 -72c9 -21 22 -40 38 -57l25 25l12 -11l-37 -37v1c-44 40 -72 97 -72 161v1v0c0 122 100 221 224 221v0v0h1c124 0 223 -99 223 -221zM208 193
l110 68l10 -11l-82 -94l-1 -1c-5 -6 -13 -10 -21 -10c-15 0 -27 11 -27 26c0 8 4 15 9 20z" />
d="M224 384c124 0 224 -100 224 -224c0 -57 -21 -108 -56 -148c-4 -4 -7 -8 -11 -12l-10 10l-1 2c-19 18 -41 33 -65 43c-26 11 -53 16 -81 16s-55 -5 -81 -16c-24 -10 -46 -25 -65 -43l-1 -2l-10 -10c-4 4 -7 8 -11 12c-35 40 -56 91 -56 148c0 124 100 224 224 224z
M416 79c10 23 15 48 16 73h-32v16h32c-1 25 -6 50 -16 73c-9 22 -23 42 -39 60l-27 -27l-6 6l-5 5v0h-1l27 27c-18 17 -38 31 -60 40c-23 10 -48 15 -74 16v-38h-16v38c-25 -1 -49 -6 -72 -16c-22 -10 -43 -23 -61 -40l27 -27v0v0l-6 -6l-5 -5l-27 27
c-16 -18 -30 -38 -39 -60c-10 -23 -15 -49 -16 -74h32v-16h-32c1 -25 6 -49 16 -72c9 -20 20 -39 35 -56c40 40 96 64 157 64s117 -24 157 -64c15 17 26 36 35 56zM336 273l2 -1l-75 -84c6 -8 9 -18 9 -28c0 -26 -22 -48 -48 -48c-10 0 -19 4 -27 9l-13 -12l-11 11l12 12
c-6 8 -9 18 -9 28c0 26 22 48 48 48c10 0 19 -3 27 -8zM224 128c18 0 32 14 32 32s-14 32 -32 32s-32 -14 -32 -32s14 -32 32 -32z" />
<glyph glyph-name="ion-ios-speedometer" unicode="&#xf4b0;"
d="M224 384c124 0 224 -99 224 -221c0 -64 -28 -122 -72 -163h-304c-44 41 -72 99 -72 163c0 122 100 221 224 221zM246 156l82 94l-10 11l-110 -68l-2 -2c-5 -5 -9 -12 -9 -20c0 -15 12 -26 27 -26c8 0 16 4 21 10zM372 18c35 35 60 87 60 145v0v1c0 84 -52 157 -125 188
c-25 11 -54 16 -83 16v0v0v0c-29 0 -57 -5 -82 -16c-72 -30 -126 -104 -126 -188v0v-1c0 -50 21 -109 62 -146l1 -1l15 15l19 19l-11 10l-23 -23c-15 16 -23 31 -32 50c-10 21 -15 43 -16 67h29v15h-29c0 23 5 47 15 69c9 20 20 38 35 54l26 -26l5 5l5 5v0v0l-25 26
c17 16 36 28 57 37c22 9 44 13 68 14v-35h14v35c24 -1 46 -5 68 -14c21 -9 40 -21 57 -37l-26 -26h1v0l5 -5l5 -5l26 26c15 -16 26 -34 35 -54c10 -22 14 -46 15 -69h-29v-15h29c-1 -24 -6 -46 -16 -67c-9 -19 -17 -34 -32 -50l-23 24l-11 -11l19 -19l15 -15z" />
d="M385 169v-15h30c-1 -22 -6 -44 -15 -65c-8 -18 -19 -35 -32 -50c-39 37 -90 58 -144 58s-105 -21 -144 -58c-13 15 -24 32 -32 50c-9 21 -14 43 -15 65h28h1v16h-29c1 23 6 45 15 66c9 20 20 38 35 54l25 -24l10 10l1 1v0v0l-25 25c16 15 36 26 56 35c21 9 43 14 66 15
v-34h14h1v34c23 -1 46 -6 67 -15s39 -20 55 -35l-24 -25l11 -11l25 25c15 -16 26 -35 35 -55c9 -21 14 -43 15 -66h-30v-1zM272 160c0 10 -3 20 -9 28l63 76l-2 1l-73 -65c-8 5 -17 8 -27 8c-26 0 -48 -22 -48 -48c0 -10 3 -20 9 -28l-12 -12l11 -11l13 12c8 -5 17 -9 27 -9
c26 0 48 22 48 48zM224 384c124 0 224 -100 224 -224c0 -57 -21 -108 -56 -148c-4 -4 -7 -8 -11 -12h-23c-32 39 -80 64 -134 64s-102 -25 -134 -64h-23c-4 4 -7 8 -11 12c-35 40 -56 91 -56 148c0 124 100 224 224 224zM379 27c34 37 52 86 52 136c0 55 -22 106 -61 145
s-91 60 -146 60s-107 -21 -146 -60s-61 -90 -61 -145c0 -50 18 -99 52 -136l10 -10l1 -1v1l9 9l1 1v0c17 17 38 30 60 39c24 10 48 15 74 15s50 -5 74 -15c22 -9 43 -22 60 -39v0l1 -1l9 -9v-1l1 1zM192 160c0 21 11 32 32 32s32 -11 32 -32s-11 -32 -32 -32s-32 11 -32 32z
" />
<glyph glyph-name="ion-ios-star-half" unicode="&#xf4b1;"
d="M140 143l-140 98h171l53 159l53 -159h171l-140 -98l54 -159l-138 99l-138 -99zM224 347v-244l107 -76l-43 122l108 75h-131z" />
<glyph glyph-name="ion-ios-star-outline" unicode="&#xf4b2;"

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -25,7 +25,7 @@ class IndexComposer
if (Component::notStatus(1)->count() === 0) {
// If all our components are ok, do we have any non-fixed incidents?
$incidents = Incident::orderBy('created_at', 'desc')->get();
$incidents = Incident::notScheduled()->orderBy('created_at', 'desc')->get();
$incidentCount = $incidents->count();
if ($incidentCount === 0 || ($incidentCount >= 1 && (int) $incidents->first()->status === 4)) {
@@ -12,6 +12,39 @@ use Illuminate\Support\Facades\View;
class DashIncidentController extends Controller
{
/**
* Stores the sub-sidebar tree list.
*
* @var array
*/
protected $subMenu = [];
/**
* Creates a new DashIncidentController instance.
*
* @return \CachetHQ\Cachet\Http\Controllers\DashScheduleController
*/
public function __construct()
{
$this->subMenu = [
'incidents' => [
'title' => trans('dashboard.incidents.incidents'),
'url' => route('dashboard.incidents'),
'icon' => 'ion-android-checkmark-circle',
'active' => true,
],
'schedule' => [
'title' => trans('dashboard.schedule.schedule'),
'url' => route('dashboard.schedule'),
'icon' => 'ion-android-calendar',
'active' => false,
],
];
View::share('subMenu', $this->subMenu);
View::share('subTitle', trans('dashboard.incidents.title'));
}
/**
* Shows the incidents view.
*
@@ -19,7 +52,7 @@ class DashIncidentController extends Controller
*/
public function showIncidents()
{
$incidents = Incident::orderBy('created_at', 'desc')->get();
$incidents = Incident::notScheduled()->orderBy('created_at', 'desc')->get();
return View::make('dashboard.incidents.index')->with([
'pageTitle' => trans('dashboard.incidents.incidents').' - '.trans('dashboard.dashboard'),
@@ -0,0 +1,228 @@
<?php
namespace CachetHQ\Cachet\Http\Controllers;
use CachetHQ\Cachet\Models\Incident;
use CachetHQ\Cachet\Models\IncidentTemplate;
use Carbon\Carbon;
use GrahamCampbell\Binput\Facades\Binput;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\View;
use Illuminate\Support\MessageBag;
class DashScheduleController extends Controller
{
/**
* Stores the sub-sidebar tree list.
*
* @var array
*/
protected $subMenu = [];
/**
* Creates a new DashScheduleController instance.
*
* @return \CachetHQ\Cachet\Http\Controllers\DashScheduleController
*/
public function __construct()
{
// TODO: Remove this from DashIncidentController, so it's shared?
$this->subMenu = [
'incidents' => [
'title' => trans('dashboard.incidents.incidents'),
'url' => route('dashboard.incidents'),
'icon' => 'ion-android-checkmark-circle',
'active' => false,
],
'schedule' => [
'title' => trans('dashboard.schedule.schedule'),
'url' => route('dashboard.schedule'),
'icon' => 'ion-android-calendar',
'active' => true,
],
];
View::share('subMenu', $this->subMenu);
View::share('subTitle', trans('dashboard.incidents.title'));
}
/**
* Lists all scheduled maintenance.
*
* @return \Illuminate\View\View
*/
public function showIndex()
{
$schedule = Incident::scheduled()->orderBy('created_at')->get();
return View::make('dashboard.schedule.index')->withSchedule($schedule);
}
/**
* Shows the add schedule maintenance form.
*
* @return \Illuminate\View\View
*/
public function showAddSchedule()
{
$incidentTemplates = IncidentTemplate::all();
return View::make('dashboard.schedule.add')->with([
'incidentTemplates' => $incidentTemplates,
]);
}
/**
* Creates a new scheduled maintenance "incident".
*
* @return \Illuminate\Http\RedirectResponse
*/
public function addScheduleAction()
{
$scheduleData = Binput::get('incident');
// Parse the schedule date.
$scheduledAt = Carbon::createFromFormat('d/m/Y H:i', $scheduleData['scheduled_at']);
if ($scheduledAt->isPast()) {
$messageBag = new MessageBag();
$messageBag->add('scheduled_at', trans('validation.date', [
'attribute' => 'scheduled time you supplied',
]));
return Redirect::back()->withErrors($messageBag);
}
$scheduleData['scheduled_at'] = $scheduledAt;
// Bypass the incident.status field.
$scheduleData['status'] = 0;
$incident = Incident::create($scheduleData);
if (! $incident->isValid()) {
segment_track('Dashboard', [
'event' => 'Created Scheduled Maintenance',
'success' => false,
]);
return Redirect::back()->withInput(Binput::all())
->with('success', sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.whoops'),
trans('dashboard.schedule.add.failure')
))
->with('errors', $incident->getErrors());
}
segment_track('Dashboard', [
'event' => 'Created Scheduled Maintenance',
'success' => true,
]);
$successMsg = sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.awesome'),
trans('dashboard.schedule.add.success')
);
return Redirect::back()->with('success', $successMsg);
}
/**
* Shows the edit schedule maintenance form.
*
* @param \CachetHQ\Cachet\Models\Incident $schedule
*
* @return \Illuminate\View\View
*/
public function showEditSchedule(Incident $schedule)
{
$incidentTemplates = IncidentTemplate::all();
return View::make('dashboard.schedule.edit')->with([
'incidentTemplates' => $incidentTemplates,
'schedule' => $schedule,
]);
}
/**
* Updates the given incident.
*
* @param \CachetHQ\Cachet\Models\Incident $schedule
*
* @return \Illuminate\Http\RedirectResponse
*/
public function editScheduleAction(Incident $schedule)
{
$scheduleData = Binput::get('incident');
// Parse the schedule date.
$scheduledAt = Carbon::createFromFormat('d/m/Y H:i', $scheduleData['scheduled_at']);
if ($scheduledAt->isPast()) {
$messageBag = new MessageBag();
$messageBag->add('scheduled_at', trans('validation.date', [
'attribute' => 'scheduled time you supplied',
]));
return Redirect::back()->withErrors($messageBag);
}
$scheduleData['scheduled_at'] = $scheduledAt;
// Bypass the incident.status field.
$scheduleData['status'] = 0;
$schedule->update($scheduleData);
if (! $schedule->isValid()) {
segment_track('Dashboard', [
'event' => 'Edited Schedule',
'success' => false,
]);
return Redirect::back()->withInput(Binput::all())
->with('title', sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.awesome'),
trans('dashboard.schedule.edit.failure')
))
->with('errors', $schedule->getErrors());
}
segment_track('Dashboard', [
'event' => 'Edited Schedule',
'success' => true,
]);
$successMsg = sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.awesome'),
trans('dashboard.schedule.edit.success')
);
return Redirect::to('dashboard/schedule')->with('success', $successMsg);
}
/**
* Deletes a given schedule.
*
* @param \CachetHQ\Cachet\Models\Incident $schedule
*
* @return \Illuminate\Http\RedirectResponse
*/
public function deleteScheduleAction(Incident $schedule)
{
if ($schedule->delete()) {
return Redirect::back()->with('success', sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.awesome'),
trans('dashboard.schedule.delete.success')
));
}
return Redirect::back()->with('warning', sprintf(
'<strong>%s</strong> %s',
trans('dashboard.notifications.whoops'),
trans('dashboard.schedule.delete.failure')
));
}
}
+13 -10
View File
@@ -73,10 +73,12 @@ class HomeController extends Controller
$metrics = Metric::where('display_chart', 1)->get();
}
$scheduledMaintenance = Incident::scheduled()->orderBy('scheduled_at')->get();
foreach (range(0, $incidentDays) as $i) {
$date = $startDate->copy()->subDays($i);
$incidents = Incident::whereBetween('created_at', [
$incidents = Incident::notScheduled()->whereBetween('created_at', [
$date->format('Y-m-d').' 00:00:00',
$date->format('Y-m-d').' 23:59:59',
])->orderBy('created_at', 'desc')->get();
@@ -88,15 +90,16 @@ class HomeController extends Controller
}
return View::make('index', [
'components' => $components,
'displayMetrics' => $displayMetrics,
'metrics' => $metrics,
'allIncidents' => $allIncidents,
'pageTitle' => Setting::get('app_name'),
'aboutApp' => Markdown::render(Setting::get('app_about')),
'canPageForward' => (bool) $today->gt($startDate),
'previousDate' => $startDate->copy()->subWeek()->subDay()->toDateString(),
'nextDate' => $startDate->copy()->addWeek()->addDay()->toDateString(),
'components' => $components,
'displayMetrics' => $displayMetrics,
'metrics' => $metrics,
'allIncidents' => $allIncidents,
'scheduledMaintenance' => $scheduledMaintenance,
'pageTitle' => Setting::get('app_name'),
'aboutApp' => Markdown::render(Setting::get('app_about')),
'canPageForward' => (bool) $today->gt($startDate),
'previousDate' => $startDate->copy()->subWeek()->subDay()->toDateString(),
'nextDate' => $startDate->copy()->addWeek()->addDay()->toDateString(),
]);
}
}
+44 -1
View File
@@ -43,7 +43,7 @@ class Incident extends Model implements TransformableInterface, PresenterInterfa
*
* @var string[]
*/
protected $fillable = ['user_id', 'component_id', 'name', 'status', 'message'];
protected $fillable = ['user_id', 'component_id', 'name', 'status', 'message', 'scheduled_at'];
/**
* The accessors to append to the model's serialized form.
@@ -52,6 +52,39 @@ class Incident extends Model implements TransformableInterface, PresenterInterfa
*/
protected $appends = ['humanStatus'];
/**
* The attributes that should be mutated to dates.
*
* @var string[]
*/
protected $dates = ['scheduled_at', 'deleted_at'];
/**
* Finds all scheduled incidents (maintenance).
*
* @param \Illuminate\Database\Eloquent\Builder $query
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeScheduled($query)
{
return $query->where('status', 0)->whereRaw('scheduled_at >= NOW()');
}
/**
* Finds all non-scheduled incidents.
*
* @param \Illuminate\Database\Eloquent\Builder $query
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotScheduled($query)
{
return $query->where(function ($query) {
return $query->where('scheduled_at', '0000-00-00 00:00:00')->orWhereRaw('scheduled_at <= NOW()');
});
}
/**
* Get presenter class.
*
@@ -84,6 +117,16 @@ class Incident extends Model implements TransformableInterface, PresenterInterfa
return $statuses[$this->status];
}
/**
* Returns whether the "incident" is scheduled or not.
*
* @return bool
*/
public function getIsScheduledAttribute()
{
return $this->getOriginal('scheduled_at') != '0000-00-00 00:00:00';
}
/**
* Get the transformer instance.
*
+52 -6
View File
@@ -59,7 +59,7 @@ class IncidentPresenter extends BasePresenter
{
return ucfirst((new Date($this->resource->created_at))
->setTimezone($this->tz)
->format('l j F Y H:i:s'));
->format('l jS F Y H:i:s'));
}
/**
@@ -72,6 +72,50 @@ class IncidentPresenter extends BasePresenter
return $this->resource->created_at->setTimezone($this->tz)->toISO8601String();
}
/**
* Present diff for humans date time.
*
* @return string
*/
public function scheduled_at_diff()
{
return (new Date($this->resource->scheduled_at))
->setTimezone($this->tz)
->diffForHumans();
}
/**
* Present formated date time.
*
* @return string
*/
public function scheduled_at_formatted()
{
return ucfirst((new Date($this->resource->scheduled_at))
->setTimezone($this->tz)
->format('l jS F Y H:i:s'));
}
/**
* Present formatted date time.
*
* @return string
*/
public function scheduled_at_iso()
{
return $this->resource->scheduled_at->setTimezone($this->tz)->toISO8601String();
}
/**
* Formats the scheduled_at time ready to be used by bootstrap-datetimepicker.
*
* @return string
*/
public function scheduled_at_datetimepicker()
{
return $this->resource->scheduled_at->setTimezone($this->tz)->format('d/m/Y H:i');
}
/**
* Present the status with an icon.
*
@@ -80,15 +124,17 @@ class IncidentPresenter extends BasePresenter
public function icon()
{
switch ($this->resource->status) {
case 1:
case 0: // Scheduled
return 'icon ion-android-calendar';
case 1: // Investigating
return 'icon ion-flag';
case 2:
case 2: // Identified
return 'icon ion-alert';
case 3:
case 3: // Watching
return 'icon ion-eye';
case 4:
case 4: // Fixed
return 'icon ion-checkmark';
default:
default: // Something actually broke, this shouldn't happen.
return '';
}
}