Skip to content

Commit

Permalink
feat(netflix): Availability Gauges
Browse files Browse the repository at this point in the history
  • Loading branch information
jrsquared committed Mar 6, 2017
1 parent bfe8d9e commit fca33e6
Show file tree
Hide file tree
Showing 12 changed files with 679 additions and 2 deletions.
@@ -0,0 +1,45 @@
<li class="availability-nav" uib-dropdown ng-class="{open: state.showAvailability}" auto-close="outsideClick">
<a href uib-dropdown-toggle class="dropdown-toggle remove-border-top aggregate-score-{{aggregate.score}}">
<span class="hidden-xs hidden-sm">Availability</span>
</a>

<div class="dropdown-menu availability-graphs" uib-dropdown-menu>
<div class="heading">
<h4>Availability Status <help-field content="The availability trends for the Netflix service represented as 'nines' and availability percentage."></help-field></h4>
</div>
<div class="row">
<div class="section current">
<h5>Recent</h5>
<h6>Current</h6>
<div class="current-score aggregate-score-{{aggregate.score}}"><span ng-bind-html="aggregate.reason"></span></div>
<h6>Yesterday <help-field content="The availability of the Netflix service from {{yesterday.date_range[0] | date:'short'}} to {{yesterday.date_range[1] | date:'short'}} has a target of {{ninetyonedays.target_nines}} nines."></help-field></h6>
<div class="section-title">Average</div>
<availability-donut nines="yesterday.nines" target-nines="yesterday.target_nines" score="yesterday.score" availability="yesterday.availability" outer-radius="100"></availability-donut>
</div>
<div class="section past">
<div class="row">
<h5>Last 28 Days <help-field content="The availability of the Netflix service from {{twentyeightdays.date_range[0] | date:'shortDate'}} to {{twentyeightdays.date_range[1] | date:'shortDate'}} has a target of {{twentyeightdays.target_nines}} nines."></h5>
<div class="sub-section average">
<div class="section-title">Average</div>
<availability-donut nines="twentyeightdays.nines" target-nines="twentyeightdays.target_nines" score="twentyeightdays.score" availability="twentyeightdays.availability" outer-radius="60"></availability-donut>
</div>
<div class="sub-section trend">
<div class="section-title">Trend</div>
<availability-trend availability-window="twentyeightdays" width="200" height="80"></availability-trend>
</div>
</div>
<div class="row">
<h5>Last 91 Days <help-field content="The availability of the Netflix service from {{ninetyonedays.date_range[0] | date:'shortDate'}} to {{ninetyonedays.date_range[1] | date:'shortDate'}} has a target of {{ninetyonedays.target_nines}} nines."></h5>
<div class="sub-section average">
<div class="section-title">Average</div>
<availability-donut nines="ninetyonedays.nines" target-nines="ninetyonedays.target_nines" score="ninetyonedays.score" availability="ninetyonedays.availability" outer-radius="60"></availability-donut>
</div>
<div class="sub-section trend">
<div class="section-title">Trend</div>
<availability-trend availability-window="ninetyonedays" width="200" height="80"></availability-trend>
</div>
</div>
</div>
</div>
</div>
</li>
111 changes: 111 additions & 0 deletions app/scripts/modules/netflix/availability/availability.directive.ts
@@ -0,0 +1,111 @@
import { module } from 'angular';

import { DirectiveFactory } from 'core/utils/tsDecorators/directiveFactoryDecorator';
import { IAvailabilityData, IAvailabilityWindow, AVAILABILITY_READER_SERVICE, AvailabilityReaderService } from './availability.read.service';
import { AVAILABILITY_DONUT_COMPONENT } from './availability.donut.component';
import { AVAILABILITY_TREND_COMPONENT } from './availability.trend.component';

import './availability.less';

interface IAggregateDetails {
score: number;
reason: string;
}

export class AvailabilityController implements ng.IComponentController {
private activeRefresher: any;

static get $inject() {
return ['$scope', 'availabilityReaderService', 'schedulerFactory'];
}

public constructor (private $scope: any, private availabilityReaderService: AvailabilityReaderService, private schedulerFactory: any) {}

private getWindowScore (window: IAvailabilityWindow): number {
if (window.nines >= window.target_nines) { return 1; }
if (window.nines >= window.target_nines * 0.95) { return 2; }
return 4;
}

private getAggregateScore (result: IAvailabilityData): IAggregateDetails {
let score = 1;
let reason = 'ALL AVAILABILITY<br/>GOALS MET!';

// Figure out score
if (result.override.value === true) {
score = 4;
reason = result.override.reason;
} else if (result.trends.yesterday.score > 1) {
// If there were recent incidents yesterday
score = 4;
reason = 'Yesterday\'s GOAL <strong>NOT</strong> MET';
// TODO: Add incident links to details
} else if (result.trends['28days'].score > 1 && result.trends['91days'].score > 1) {
// If we have not acheived 28 day availability goals
score = 3;
reason = '28 DAY GOAL AND <br/>91 DAY GOAL <strong>NOT</strong> MET';
} else if (result.trends['28days'].score > 1) {
// If we have not acheived 28 day availability goals (but rest are acheived)
score = 2;
reason = '28 DAY GOAL <strong>NOT</strong> MET';
} else if (result.trends['91days'].score > 1) {
// If we have not acheived 90 day availability goals (but rest are acheived)
score = 2;
reason = '91 DAY GOAL <strong>NOT</strong> MET';
}

return { score, reason };
}

public refreshData(): void {
this.availabilityReaderService.getAvailabilityData().then((result) => {
if (result) {
// Build composite availability score for main button
this.$scope.lastUpdated = result.trends.last_updated;
this.$scope.yesterday = result.trends.yesterday;
this.$scope.yesterday.score = this.getWindowScore(this.$scope.yesterday);
this.$scope.twentyeightdays = result.trends['28days'];
this.$scope.twentyeightdays.score = this.getWindowScore(this.$scope.twentyeightdays);
this.$scope.ninetyonedays = result.trends['91days'];
this.$scope.ninetyonedays.score = this.getWindowScore(this.$scope.ninetyonedays);

const aggregate: IAggregateDetails = this.getAggregateScore(result);
this.$scope.aggregate = aggregate;
}
});
}

public initialize(): void {
this.activeRefresher = this.schedulerFactory.createScheduler();
this.activeRefresher.subscribe(() => {
this.refreshData();
});
this.refreshData();
}

public $onDestroy(): void {
this.activeRefresher.unsubscribe();
}
}

@DirectiveFactory('availabilityReaderService')
class AvailabilityDirective implements ng.IDirective {
public restrict = 'E';
public controller: any = AvailabilityController;
public controllerAs = '$ctrl';
public templateUrl: string = require('./availability.directive.html');
public replace = true;

link($scope: ng.IScope, _$element: JQuery) {
const $ctrl: AvailabilityController = $scope['$ctrl'];
$ctrl.initialize();
}
}

export const AVAILABILITY_DIRECTIVE = 'spinnaker.netflix.availability.directive';
module(AVAILABILITY_DIRECTIVE, [
AVAILABILITY_READER_SERVICE,
AVAILABILITY_DONUT_COMPONENT,
AVAILABILITY_TREND_COMPONENT,
require('core/scheduler/scheduler.factory')
]).directive('availability', <any>AvailabilityDirective);
@@ -0,0 +1,118 @@
import { module } from 'angular';
import { Arc, arc, DefaultArcObject, Pie, pie } from 'd3-shape';

import './availability.less';

interface ArcData {
path: string;
score: number;
}

interface IDonutGraphData {
arcs: ArcData[];
total: string;
width: number;
height: number;
}

export class AvailabilityDonutController implements ng.IComponentController {
public availability: number;
public donut: IDonutGraphData;
public ninesSize: number;
public percentSize: number;
public score: number;
public displayNines: string;
private nines: number;
private targetNines: number;
private outerRadius: number;
private arc: Arc<any, DefaultArcObject> = arc();
private pie: Pie<any, number> = pie<number>().sort(null).sortValues(null);
private donutWidthPercent = 0.7;

private updateData(): void {
if (this.targetNines && this.nines && this.outerRadius) {
this.donut = this.buildDonutGraph();
this.ninesSize = this.outerRadius * 0.46;
this.percentSize = this.outerRadius * 0.2;
this.displayNines = (this.nines >= 7) ? `${this.nines}+` : String(this.nines);
}
}

public $onInit(): void {
this.updateData();
}

public $onChanges(): void {
this.updateData();
}

private buildDonutGraph(): IDonutGraphData {
const totalNines = Math.min(this.nines, this.targetNines);
const pieData = [totalNines, this.targetNines - totalNines];
const availabilityPie = this.pie(pieData);
const arcs: ArcData[] = [
// Availability arc
{
path: this.arc({
startAngle: availabilityPie[0].startAngle,
endAngle: availabilityPie[0].endAngle,
innerRadius: this.outerRadius * this.donutWidthPercent,
outerRadius: this.outerRadius,
padAngle: 0
}),
score: this.score
},
// Empty arc
{
path: this.arc({
startAngle: availabilityPie[1].startAngle,
endAngle: availabilityPie[1].endAngle,
innerRadius: this.outerRadius * this.donutWidthPercent,
outerRadius: this.outerRadius,
padAngle: 0
}),
score: 0
}
];

// Total donut (needed for a clean border)
// Build a donut graph with one slice for the availability and one slice
// for the empty space, then create a separate whole donut to fake a border.
// If we add strokes to the paths, then there's a stroke on the connecting sides,
// which we don't want. If I create a donut with the empty color that fills the
// whole donut, then give _that_ a stroke, anti-aliasing ruins the border near
// the availability score.
const total = this.arc({
startAngle: 0,
endAngle: 2 * Math.PI,
innerRadius: this.outerRadius * this.donutWidthPercent - 2,
outerRadius: this.outerRadius + 2,
padAngle: 0
});

return {
arcs: arcs,
total: total,
width: this.outerRadius * 2.5,
height: this.outerRadius * 2.5
};
}
}

class AvailabilityDonutComponent implements ng.IComponentOptions {
public bindings: any = {
availability: '<',
nines: '<',
targetNines: '<',
outerRadius: '<',
score: '<'
};

public controller: any = AvailabilityDonutController;
public templateUrl: string = require('./availability.donut.html');
}

export const AVAILABILITY_DONUT_COMPONENT = 'spinnaker.netflix.availability.donut.component';

module(AVAILABILITY_DONUT_COMPONENT, [])
.component('availabilityDonut', new AvailabilityDonutComponent());
12 changes: 12 additions & 0 deletions app/scripts/modules/netflix/availability/availability.donut.html
@@ -0,0 +1,12 @@
<svg ng-attr-width="{{$ctrl.donut.width}}" ng-attr-height="{{$ctrl.donut.height}}">
<g ng-attr-transform="translate({{$ctrl.donut.width/2 || 0}},{{$ctrl.donut.height/2 || 0}})">
<g class="arc total">
<path ng-attr-d="{{$ctrl.donut.total}}" class="score-border"></path>
</g>
<g class="arc availability" ng-repeat="arc in $ctrl.donut.arcs">
<path ng-attr-d="{{arc.path}}" class="score-fill-{{arc.score}}"></path>
</g>
<text text-anchor="middle" alignment-baseline="baseline" x="0" y="0" style="font-size: {{$ctrl.ninesSize}}px" class="nines score-fill-{{$ctrl.score}}">{{$ctrl.displayNines}}</text>
<text text-anchor="middle" alignment-baseline="hanging" x="0" y="5" style="font-size: {{$ctrl.percentSize}}px" class="percent score-fill-{{$ctrl.score}}">{{$ctrl.availability}}%</text>
</g>
</svg>

0 comments on commit fca33e6

Please sign in to comment.