Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
679 additions
and
2 deletions.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
app/scripts/modules/netflix/availability/availability.directive.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
111
app/scripts/modules/netflix/availability/availability.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
118 changes: 118 additions & 0 deletions
118
app/scripts/modules/netflix/availability/availability.donut.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
12
app/scripts/modules/netflix/availability/availability.donut.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.