Browse Source

Labeling for anomalies #281 (#299)

master
rozetko 6 years ago committed by GitHub
parent
commit
5a86d1723e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      src/panel/graph_panel/controllers/analytic_controller.ts
  2. 5
      src/panel/graph_panel/graph_ctrl.ts
  3. 4
      src/panel/graph_panel/graph_renderer.ts
  4. 17
      src/panel/graph_panel/models/analytic_units/analytic_unit.ts
  5. 49
      src/panel/graph_panel/models/analytic_units/anomaly_analytic_unit.ts
  6. 12
      src/panel/graph_panel/models/analytic_units/pattern_analytic_unit.ts
  7. 6
      src/panel/graph_panel/models/analytic_units/threshold_analytic_unit.ts
  8. 277
      src/panel/graph_panel/partials/tab_analytics.html

22
src/panel/graph_panel/controllers/analytic_controller.ts

@ -106,7 +106,7 @@ export class AnalyticController {
async saveNew(metric: MetricExpanded, datasource: DatasourceRequest) {
this._savingNewAnalyticUnit = true;
const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.serverObject);
const newAnalyticUnit = createAnalyticUnit(this._newAnalyticUnit.toJSON());
newAnalyticUnit.id = await this._analyticService.postNewAnalyticUnit(
newAnalyticUnit, metric, datasource, this._grafanaUrl, this._panelId
);
@ -143,7 +143,8 @@ export class AnalyticController {
await this.disableLabeling();
this._selectedAnalyticUnitId = id;
this.labelingUnit.selected = true;
this.toggleLabelingMode(LabelingMode.LABELING);
const labelingModes = this.labelingUnit.labelingModes;
this.toggleLabelingMode(labelingModes[0].value);
}
async disableLabeling() {
@ -196,9 +197,9 @@ export class AnalyticController {
this.labelingUnit.labelingMode = labelingMode;
}
addLabelSegment(segment: Segment, deleted = false) {
const asegment = this.labelingUnit.addLabeledSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(asegment);
addSegment(segment: Segment, deleted = false) {
const addedSegment = this.labelingUnit.addSegment(segment, deleted);
this._labelingDataAddedSegments.addSegment(addedSegment);
}
get analyticUnits(): AnalyticUnit[] {
@ -453,12 +454,8 @@ export class AnalyticController {
if(!this.inLabelingMode) {
throw new Error(`Can't enter ${labelingMode} mode when labeling mode is disabled`);
}
if(this.labelingUnit.labelingMode === labelingMode) {
this.labelingUnit.labelingMode = LabelingMode.LABELING;
} else {
this.labelingUnit.labelingMode = labelingMode;
}
}
async removeAnalyticUnit(id: AnalyticUnitId, silent: boolean = false): Promise<void> {
if(id === this._selectedAnalyticUnitId) {
@ -484,7 +481,7 @@ export class AnalyticController {
}
analyticUnit.saving = true;
await this._analyticService.updateAnalyticUnit(analyticUnit.serverObject);
await this._analyticService.updateAnalyticUnit(analyticUnit.toJSON());
analyticUnit.saving = false;
}
@ -681,6 +678,11 @@ export class AnalyticController {
.forEach(unit => unit.inspect = false);
}
public async updateSeasonality(id: AnalyticUnitId) {
const analyticUnit = this._analyticUnitsSet.byId(id) as AnomalyAnalyticUnit;
await this.saveAnalyticUnit(analyticUnit);
}
public onAnalyticUnitDetectorChange(analyticUnitTypes: any) {
// TODO: looks bad
this._newAnalyticUnit.type = analyticUnitTypes[this._newAnalyticUnit.detectorType][0].value;

5
src/panel/graph_panel/graph_ctrl.ts

@ -682,6 +682,11 @@ class GraphCtrl extends MetricsPanelCtrl {
this.refresh();
}
onSeasonalityChange(id: AnalyticUnitId) {
this.analyticsController.updateSeasonality(id);
this.refresh();
}
private async _updatePanelInfo() {
let datasource = undefined;
if(this.panel.datasource) {

4
src/panel/graph_panel/graph_renderer.ts

@ -153,10 +153,10 @@ export class GraphRenderer {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(
segment.from, segment.to
);
this._analyticController.addLabelSegment(segment, true);
this._analyticController.addSegment(segment, true);
}
if(this._analyticController.labelingMode === LabelingMode.LABELING) {
this._analyticController.addLabelSegment(segment, false);
this._analyticController.addSegment(segment, false);
}
if(this._analyticController.labelingMode === LabelingMode.UNLABELING) {
this._analyticController.deleteLabelingAnalyticUnitSegmentsInRange(

17
src/panel/graph_panel/models/analytic_units/analytic_unit.ts

@ -33,6 +33,9 @@ export class AnalyticSegment extends Segment {
if(!_.isBoolean(this.labeled)) {
throw new Error('labeled value is not boolean');
}
if(labeled && deleted) {
throw new Error('Segment can`t be both labeled and deleted');
}
}
}
@ -47,6 +50,8 @@ const DEFAULTS = {
visible: true
};
const LABELING_MODES = [];
export class AnalyticUnit {
private _labelingMode: LabelingMode = LabelingMode.LABELING;
@ -122,10 +127,10 @@ export class AnalyticUnit {
this._serverObject.visible = value;
}
addLabeledSegment(segment: Segment, deleted: boolean): AnalyticSegment {
const asegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(asegment);
return asegment;
addSegment(segment: Segment, deleted: boolean): AnalyticSegment {
const addedSegment = new AnalyticSegment(!deleted, segment.id, segment.from, segment.to, deleted);
this._segmentSet.addSegment(addedSegment);
return addedSegment;
}
removeSegmentsInRange(from: number, to: number): AnalyticSegment[] {
@ -172,4 +177,8 @@ export class AnalyticUnit {
get serverObject() { return this._serverObject; }
// TODO: make it abstract
get labelingModes() {
return LABELING_MODES;
}
}

49
src/panel/graph_panel/models/analytic_units/anomaly_analytic_unit.ts

@ -1,14 +1,30 @@
import { AnalyticUnit, DetectorType } from './analytic_unit';
import { AnalyticUnit, DetectorType, LabelingMode } from './analytic_unit';
import _ from 'lodash';
import moment from 'moment';
type TimePeriod = {
value: number,
unit: string
};
const DEFAULTS = {
detectorType: DetectorType.ANOMALY,
type: 'ANOMALY',
alpha: 0.5,
confidence: 1
confidence: 1,
seasonality: 0,
seasonalityPeriod: {
value: 0,
unit: 'seconds'
}
};
const LABELING_MODES = [
{ name: 'Label Negative', value: LabelingMode.DELETING },
{ name: 'Unlabel', value: LabelingMode.UNLABELING }
];
export class AnomalyAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) {
@ -21,7 +37,9 @@ export class AnomalyAnalyticUnit extends AnalyticUnit {
return {
...baseJSON,
alpha: this.alpha,
confidence: this.confidence
confidence: this.confidence,
seasonality: this.seasonality,
seasonalityPeriod: this.seasonalityPeriod
};
}
@ -30,4 +48,29 @@ export class AnomalyAnalyticUnit extends AnalyticUnit {
set confidence(val: number) { this._serverObject.confidence = val; }
get confidence(): number { return this._serverObject.confidence; }
get seasonality(): number {
let seasonalityObj = {};
seasonalityObj[this.seasonalityPeriod.unit] = this.seasonalityPeriod.value;
return moment.duration(seasonalityObj).asMilliseconds();
}
set seasonalityPeriod(val: TimePeriod) { this._serverObject.seasonalityPeriod = val; }
get seasonalityPeriod(): TimePeriod { return this._serverObject.seasonalityPeriod; }
// TODO: merge seasonality and hasSeasonality
set hasSeasonality(val: boolean) {
if(val) {
this.seasonalityPeriod = { value: 1, unit: 'seconds' };
} else {
this.seasonalityPeriod = { value: 0, unit: 'seconds' };
}
}
get hasSeasonality(): boolean {
return this.seasonality > 0;
}
get labelingModes() {
return LABELING_MODES;
}
}

12
src/panel/graph_panel/models/analytic_units/pattern_analytic_unit.ts

@ -1,4 +1,4 @@
import { AnalyticUnit, DetectorType } from './analytic_unit';
import { AnalyticUnit, DetectorType, LabelingMode } from './analytic_unit';
import _ from 'lodash';
@ -7,6 +7,12 @@ const DEFAULTS = {
type: 'GENERAL'
};
const LABELING_MODES = [
{ name: 'Label Positive', value: LabelingMode.LABELING },
{ name: 'Label Negative', value: LabelingMode.DELETING },
{ name: 'Unlabel', value: LabelingMode.UNLABELING }
];
export class PatternAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) {
@ -20,4 +26,8 @@ export class PatternAnalyticUnit extends AnalyticUnit {
...baseJSON
};
}
get labelingModes() {
return LABELING_MODES;
}
}

6
src/panel/graph_panel/models/analytic_units/threshold_analytic_unit.ts

@ -19,6 +19,8 @@ const DEFAULTS = {
condition: Condition.ABOVE_OR_EQUAL
};
const LABELING_MODES = [];
export class ThresholdAnalyticUnit extends AnalyticUnit {
constructor(_serverObject?: any) {
@ -40,4 +42,8 @@ export class ThresholdAnalyticUnit extends AnalyticUnit {
set condition(val: Condition) { this._serverObject.condition = val; }
get condition(): Condition { return this._serverObject.condition; }
get labelingModes() {
return LABELING_MODES;
}
}

277
src/panel/graph_panel/partials/tab_analytics.html

@ -1,7 +1,7 @@
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label">Select Hastic datasource</label>
<select class="gf-form-input max-width-15"
<select class="gf-form-input width-15"
ng-model="ctrl.panel.hasticDatasource"
ng-options="ds.id as ds.name for ds in ctrl.hasticDatasources"
ng-change="ctrl.onHasticDatasourceChange()"
@ -20,20 +20,22 @@
<div ng-if="ctrl.analyticsController.serverStatus === true && !ctrl.analyticsController.loading">
<h5> Analytic Units </h5>
<div class="editor-row">
<div class="gf-form" ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<div ng-repeat="analyticUnit in ctrl.analyticsController.analyticUnits">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-5">
<i class="fa fa-info" bs-tooltip="'Analytic unit id: ' + analyticUnit.id"></i>
&nbsp; Name
</label>
<input
type="text" class="gf-form-input max-width-15"
type="text" class="gf-form-input width-15"
ng-model="analyticUnit.name"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
>
</div>
<label class="gf-form-label width-4"> Type </label>
<div class="gf-form">
<label class="gf-form-label width-5"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-10"
ng-model="analyticUnit.type"
@ -41,16 +43,9 @@
ng-disabled="true"
/>
</div>
</div>
<!--
<label class="gf-form-label width-6"> Confidence </label>
<input
type="number" class="gf-form-input width-5 ng-valid ng-scope ng-empty ng-dirty ng-valid-number ng-touched"
placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'"
data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur="" data-original-title="" title=""
/>
-->
<div class="gf-form">
<label class="gf-form-label width-8"> Positive Color </label>
<span class="gf-form-label">
<color-picker
@ -58,65 +53,60 @@
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, false)"
/>
</span>
</div>
<!-- Hack to avoid Grafana's .gf-form-label + .gf-form-label css. Fix for https://github.com/hastic/hastic-grafana-app/issues/192 -->
<div ng-if="analyticUnit.detectorType === 'pattern'"></div>
<label ng-if="analyticUnit.detectorType === 'pattern'" class="gf-form-label width-8"> Negative Color </label>
<span ng-if="analyticUnit.detectorType === 'pattern'" class="gf-form-label">
<div class="gf-form"
ng-if="analyticUnit.detectorType === 'pattern' || analyticUnit.detectorType === 'anomaly'"
>
<label class="gf-form-label width-8"> Negative Color </label>
<span class="gf-form-label">
<color-picker
color="analyticUnit.deletedColor"
onChange="ctrl.onColorChange.bind(ctrl, analyticUnit.id, true)"
/>
</span>
</div>
<!-- TODO: move analytic-unit-specific fields rendering to class -->
<select class="gf-form-input width-9"
ng-if="analyticUnit.detectorType === 'threshold'"
ng-model="analyticUnit.condition"
ng-options="type for type in ctrl.analyticsController.conditions"
ng-change="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<input class="gf-form-input width-5"
ng-if="
analyticUnit.detectorType === 'threshold' &&
analyticUnit.condition !== 'NO_DATA'
"
type="number"
ng-model="analyticUnit.value"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<label class="gf-form-label width-6" ng-if="analyticUnit.detectorType === 'anomaly'"> Alpha </label>
<input class="gf-form-input width-5"
ng-if="analyticUnit.detectorType === 'anomaly'"
min="0"
max="1"
type="number"
ng-model="analyticUnit.alpha"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<label class="gf-form-label width-6" ng-if="analyticUnit.detectorType === 'anomaly'"> Confidence </label>
<input class="gf-form-input width-5"
ng-if="analyticUnit.detectorType === 'anomaly'"
min="0"
type="number"
ng-model="analyticUnit.confidence"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<div class="gf-form" ng-if="analyticUnit.visible">
<!-- TODO: Remove hack with "margin-bottom: 0" -->
<gf-form-switch ng-if="analyticUnit.visible"
<gf-form-switch
class="gf-form"
style="margin-bottom: 0"
label="Inspect"
label-class="width-5"
label-class="width-7"
on-change="ctrl.onToggleInspect(analyticUnit.id)"
checked="analyticUnit.inspect"
/>
</div>
<div
class="gf-form"
ng-if="
analyticUnit.detectorType === 'pattern' ||
(analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality)
"
>
<label
class="gf-form-label pointer"
ng-if="!analyticUnit.selected"
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }"
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING'"
>
<i class="fa fa-bar-chart" ng-if="!analyticUnit.saving"></i>
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
Label
</label>
<select class="gf-form-input width-12"
ng-if="analyticUnit.selected && !analyticUnit.saving"
ng-model="ctrl.analyticsController.labelingMode"
ng-options="type.value as type.name for type in analyticUnit.labelingModes"
ng-disabled="analyticUnit.status === 'LEARNING'"
/>
</div>
<label class="gf-form-label" ng-hide="analyticUnit.selected">
<div class="gf-form" ng-if="!analyticUnit.selected">
<label class="gf-form-label">
<a
ng-if="analyticUnit.visible"
bs-tooltip="'Hide. It`s visible now.'"
@ -135,45 +125,33 @@
<i class="fa fa-eye-slash"></i>
</a>
</label>
</div>
<label class="gf-form-label"
ng-if="analyticUnit.detectorType === 'threshold' || analyticUnit.detectorType === 'anomaly'"
<div class="gf-form">
<label class="gf-form-label">
<a
ng-if="!analyticUnit.selected"
ng-click="ctrl.onRemove(analyticUnit.id)"
class="pointer"
>
<a class="pointer" ng-click="ctrl.runDetectInCurrentRange(analyticUnit.id)">
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
<b ng-if="!analyticUnit.saving"> Detect </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
<i class="fa fa-trash"></i>
</a>
</label>
<label
class="gf-form-label pointer"
ng-if="analyticUnit.detectorType === 'pattern' && !analyticUnit.selected"
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }"
ng-click="ctrl.onToggleLabelingMode(analyticUnit.id)"
ng-disabled="analyticUnit.status === 'LEARNING'"
<a
ng-if="analyticUnit.selected"
ng-click="ctrl.onCancelLabeling(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-bar-chart" ng-if="!analyticUnit.saving"></i>
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
Label
<i class="fa fa-ban"></i>
</a>
</label>
</div>
<select class="gf-form-input width-12"
ng-model="ctrl.analyticsController.labelingMode"
ng-options="type.value as type.name for type in [
{ name:'Label Positive', value: 'LABELING' },
{ name:'Label Negative', value: 'DELETING' },
{ name:'Unlabel', value: 'UNLABELING' }
]"
ng-if="analyticUnit.selected && !analyticUnit.saving"
ng-disabled="analyticUnit.status === 'LEARNING'"
/>
<div class="gf-form" ng-if="analyticUnit.selected && !analyticUnit.saving">
<!-- Standard way to add conditions to ng-style: https://stackoverflow.com/a/29470439 -->
<label
class="gf-form-label"
ng-style="analyticUnit.status === 'LEARNING' && { 'cursor': 'not-allowed' }"
ng-if="analyticUnit.selected && !analyticUnit.saving"
ng-disabled="analyticUnit.status === 'LEARNING'"
>
<a class="pointer"
@ -185,25 +163,24 @@
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a>
</label>
</div>
<label class="gf-form-label">
<a
ng-if="!analyticUnit.selected"
ng-click="ctrl.onRemove(analyticUnit.id)"
class="pointer"
>
<i class="fa fa-trash"></i>
</a>
<a
ng-if="analyticUnit.selected"
ng-click="ctrl.onCancelLabeling(analyticUnit.id)"
class="pointer"
<div class="gf-form"
ng-if="
analyticUnit.detectorType === 'threshold' ||
(analyticUnit.detectorType === 'anomaly' && !analyticUnit.hasSeasonality)
"
>
<i class="fa fa-ban"></i>
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.runDetectInCurrentRange(analyticUnit.id)">
<i class="fa fa-spinner fa-spin" ng-if="analyticUnit.saving"></i>
<b ng-if="!analyticUnit.saving"> Detect </b>
<b ng-if="analyticUnit.saving" ng-disabled="true"> saving... </b>
</a>
</label>
</div>
<div class="gf-form">
<label>
<i ng-if="analyticUnit.status === 'READY'" class="grafana-tip fa fa-check-circle ng-scope" bs-tooltip="'Ready'"></i>
<i ng-if="analyticUnit.status === 'LEARNING'" class="grafana-tip fa fa-leanpub ng-scope" bs-tooltip="'Learning'"></i>
@ -213,24 +190,112 @@
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form width-20"/>
<!-- TODO: move analytic-unit-specific fields rendering to class -->
<div class="gf-form" ng-if="analyticUnit.detectorType === 'threshold'">
<label class="gf-form-label width-5"> Condition </label>
<select class="gf-form-input width-5"
ng-class="{
'width-5': analyticUnit.condition !== 'NO_DATA',
'width-10': analyticUnit.condition === 'NO_DATA'
}"
ng-model="analyticUnit.condition"
ng-options="type for type in ctrl.analyticsController.conditions"
ng-change="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
<input class="gf-form-input width-5"
ng-if="analyticUnit.condition !== 'NO_DATA'"
type="number"
ng-model="analyticUnit.value"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</div>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly' && !analyticUnit.selected">
<label class="gf-form-label width-5"> Alpha </label>
<input class="gf-form-input width-10"
ng-hide="analyticUnit.selected"
min="0"
max="1"
type="number"
ng-model="analyticUnit.alpha"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</div>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly' && !analyticUnit.selected">
<label class="gf-form-label width-6"> Confidence </label>
<input class="gf-form-input width-5"
min="0"
type="number"
ng-model="analyticUnit.confidence"
ng-blur="ctrl.onAnalyticUnitChange(analyticUnit)"
/>
</div>
<div class="gf-form" ng-if="analyticUnit.detectorType === 'anomaly' && !analyticUnit.selected">
<!-- TODO: Remove hack with "margin-bottom: 0" -->
<gf-form-switch
class="gf-form"
style="margin-bottom: 0"
label="Seasonality"
label-class="width-7"
on-change="ctrl.onAnalyticUnitChange(analyticUnit)"
checked="analyticUnit.hasSeasonality"
/>
</div>
<div
class="gf-form"
ng-if="analyticUnit.detectorType === 'anomaly' && analyticUnit.hasSeasonality && !analyticUnit.selected"
>
<label class="gf-form-label width-9"> Seasonality Period </label>
<input
type="number" class="gf-form-input width-5"
ng-model="analyticUnit.seasonalityPeriod.value"
ng-blur="ctrl.onSeasonalityChange(analyticUnit.id)"
min="0"
>
</div>
<div class="gf-form"
ng-if="
analyticUnit.detectorType === 'anomaly' &&
analyticUnit.hasSeasonality &&
!analyticUnit.selected
"
>
<div class="gf-form-select-wrapper">
<!-- TODO: move periods from ng-options -->
<select class="gf-form-input width-8"
ng-model="analyticUnit.seasonalityPeriod.unit"
ng-change="ctrl.onSeasonalityChange(analyticUnit.id)"
ng-options="type for type in ['seconds', 'minutes', 'days', 'hours', 'days', 'years']"
/>
</div>
</div>
</div>
</div>
<div class="editor-row" ng-if="ctrl.analyticsController.creatingNew">
<div class="gf-form">
<label class="gf-form-label width-4"> Name </label>
<label class="gf-form-label width-5"> Name </label>
<input
type="text" class="gf-form-input max-width-15"
type="text" class="gf-form-input width-15"
ng-model="ctrl.analyticsController.newAnalyticUnit.name"
>
<label class="gf-form-label width-8"> Detector Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-10"
<select class="gf-form-input width-7"
ng-model="ctrl.analyticsController.newAnalyticUnit.detectorType"
ng-options="analyticUnitDetectorType for analyticUnitDetectorType in ctrl.analyticUnitDetectorTypes"
ng-change="ctrl.analyticsController.onAnalyticUnitDetectorChange(ctrl.analyticUnitTypes);"
/>
</div>
<label class="gf-form-label width-8"> Type </label>
<label class="gf-form-label width-5"> Type </label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input width-10"
ng-model="ctrl.analyticsController.newAnalyticUnit.type"
@ -256,7 +321,7 @@
Add Analytic Unit
</button>
</div>
<div class="gf-form-button-row">
<div class="gf-form-button-row" ng-if="ctrl.analyticsController.analyticUnits.length > 0">
<button class="gf-form-label width-12 pointer" ng-click="ctrl.redetectAll()">
Re-detect all analytic units
</button>

Loading…
Cancel
Save