copy client code from the new hastic #1
Merged
rozetko
merged 1 commits from copy-code-from-the-new-hastic
into master
2 years ago
47 changed files with 31826 additions and 3 deletions
@ -0,0 +1,29 @@
|
||||
module.exports = { |
||||
root: true, |
||||
env: { |
||||
node: true |
||||
}, |
||||
'extends': [ |
||||
'plugin:vue/vue3-essential', |
||||
'eslint:recommended', |
||||
'@vue/typescript/recommended' |
||||
], |
||||
parserOptions: { |
||||
ecmaVersion: 2020 |
||||
}, |
||||
rules: { |
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' |
||||
}, |
||||
overrides: [ |
||||
{ |
||||
files: [ |
||||
'**/__tests__/*.{j,t}s?(x)', |
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)' |
||||
], |
||||
env: { |
||||
jest: true |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,23 @@
|
||||
.DS_Store |
||||
node_modules |
||||
/dist |
||||
|
||||
|
||||
# local env files |
||||
.env.local |
||||
.env.*.local |
||||
|
||||
# Log files |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
|
||||
# Editor directories and files |
||||
.idea |
||||
.vscode |
||||
*.suo |
||||
*.ntvs* |
||||
*.njsproj |
||||
*.sln |
||||
*.sw? |
@ -1,5 +1,35 @@
|
||||
# clientx |
||||
|
||||
Attempt to make hastic clieent which works with old version of hastic-server https://code.corpglory.net/hastic/hastic-server |
||||
Standalone Hastic client |
||||
|
||||
Attempt to make hastic client which works with old version of hastic-server https://code.corpglory.net/hastic/hastic-server |
||||
|
||||
based on new version of hastic client from https://code.corpglory.net/hastic/hastic |
||||
|
||||
## Project setup |
||||
``` |
||||
yarn install |
||||
``` |
||||
|
||||
### Compiles and hot-reloads for development |
||||
``` |
||||
yarn serve |
||||
``` |
||||
|
||||
### Compiles and minifies for production |
||||
``` |
||||
yarn build |
||||
``` |
||||
|
||||
### Run your unit tests |
||||
``` |
||||
yarn test:unit |
||||
``` |
||||
|
||||
### Lints and fixes files |
||||
``` |
||||
yarn lint |
||||
``` |
||||
|
||||
### Customize configuration |
||||
See [Configuration Reference](https://cli.vuejs.org/config/). |
||||
|
@ -0,0 +1,6 @@
|
||||
module.exports = { |
||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript', |
||||
transform: { |
||||
'^.+\\.vue$': 'vue-jest' |
||||
} |
||||
} |
@ -0,0 +1,45 @@
|
||||
{ |
||||
"name": "hastic", |
||||
"version": "0.1.0", |
||||
"private": true, |
||||
"scripts": { |
||||
"serve": "vue-cli-service serve", |
||||
"build": "vue-cli-service build", |
||||
"test:unit": "vue-cli-service test:unit", |
||||
"lint": "vue-cli-service lint" |
||||
}, |
||||
"dependencies": { |
||||
"@chartwerk/line-pod": "^0.4.6", |
||||
"@chartwerk/scatter-pod": "^0.2.4", |
||||
"@kyvg/vue3-notification": "^2.3.4", |
||||
"@types/lodash": "^4.14.176", |
||||
"@types/tinycolor2": "^1.4.3", |
||||
"axios": "^0.23.0", |
||||
"lodash": "^4.17.21", |
||||
"tinycolor2": "^1.4.2", |
||||
"vue": "^3.0.0", |
||||
"vue-class-component": "^8.0.0-0", |
||||
"vue-router": "^4.0.0-0", |
||||
"vuex": "^4.0.0-0" |
||||
}, |
||||
"devDependencies": { |
||||
"@types/jest": "^24.0.19", |
||||
"@typescript-eslint/eslint-plugin": "^4.18.0", |
||||
"@typescript-eslint/parser": "^4.18.0", |
||||
"@vue/cli-plugin-eslint": "~4.5.0", |
||||
"@vue/cli-plugin-router": "~4.5.0", |
||||
"@vue/cli-plugin-typescript": "~4.5.0", |
||||
"@vue/cli-plugin-unit-jest": "~4.5.0", |
||||
"@vue/cli-plugin-vuex": "~4.5.0", |
||||
"@vue/cli-service": "~4.5.0", |
||||
"@vue/compiler-sfc": "^3.0.0", |
||||
"@vue/eslint-config-typescript": "^7.0.0", |
||||
"@vue/test-utils": "^2.0.0-0", |
||||
"eslint": "^6.7.2", |
||||
"eslint-plugin-vue": "^7.0.0", |
||||
"sass": "^1.26.5", |
||||
"sass-loader": "^8.0.2", |
||||
"typescript": "~4.1.5", |
||||
"vue-jest": "^5.0.0-0" |
||||
} |
||||
} |
After Width: | Height: | Size: 318 B |
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html> |
||||
<html lang=""> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> |
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> |
||||
<title><%= htmlWebpackPlugin.options.title %></title> |
||||
</head> |
||||
<body> |
||||
<noscript> |
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> |
||||
</noscript> |
||||
<div id="app"></div> |
||||
<!-- built files will be auto injected --> |
||||
</body> |
||||
</html> |
@ -0,0 +1,27 @@
|
||||
<template> |
||||
<notifications /> |
||||
<router-view/> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
#app { |
||||
font-family: Avenir, Helvetica, Arial, sans-serif; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
text-align: center; |
||||
color: #2c3e50; |
||||
} |
||||
|
||||
#nav { |
||||
padding: 30px; |
||||
|
||||
a { |
||||
font-weight: bold; |
||||
color: #2c3e50; |
||||
|
||||
&.router-link-exact-active { |
||||
color: #42b983; |
||||
} |
||||
} |
||||
} |
||||
</style> |
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,23 @@
|
||||
<template> |
||||
<div class="analytic-status"> |
||||
analytic status: <strong> {{ status.message }} </strong> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { AnalyticStatus } from "@/store"; |
||||
|
||||
import { defineComponent } from 'vue'; |
||||
|
||||
|
||||
export default defineComponent({ |
||||
name: 'AnalyticStatus', |
||||
components: { |
||||
}, |
||||
computed: { |
||||
status(): AnalyticStatus { |
||||
return this.$store.state.analyticStatus; |
||||
} |
||||
} |
||||
}); |
||||
</script> |
@ -0,0 +1,245 @@
|
||||
<template> |
||||
<div> |
||||
<div id="chart"></div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
|
||||
import { AnomalyHSR, TimeRange } from "@/types"; |
||||
import { PatternPod } from "./pods/pattern_pod"; |
||||
import { ThresholdPod } from './pods/threshold_pod'; |
||||
import { AnomalyPod } from './pods/anomaly_pod'; |
||||
|
||||
import { LineTimeSerie } from "@chartwerk/line-pod"; |
||||
|
||||
import { SegmentArray } from '@/types/segment_array'; |
||||
import { Segment, SegmentId } from '@/types/segment'; |
||||
|
||||
import { AnalyticUnitType } from '@/types/analytic_units'; |
||||
|
||||
import { getHSRAnomaly } from "@/services/analytics.service"; |
||||
import { getMetrics } from '@/services/metrics.service'; |
||||
import { getSegments, postSegment, deleteSegment } from '@/services/segments.service'; |
||||
|
||||
import { defineComponent } from 'vue'; |
||||
import _ from "lodash"; |
||||
|
||||
// TODO: move to store |
||||
async function resolveDataPatterns(range: TimeRange): Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
segments: Segment[] |
||||
}> { |
||||
|
||||
const endTime = Math.floor(range.to); |
||||
const startTime = Math.floor(range.from); |
||||
|
||||
const step = Math.max(Math.round((endTime - startTime) / 5000), 1); |
||||
|
||||
try { |
||||
// TODO: request in parallel |
||||
let [target, values] = await getMetrics(startTime, endTime, step); |
||||
let segments = await getSegments(startTime, endTime); |
||||
return { |
||||
timeserie: [{ target: target, datapoints: values, color: 'green' }], |
||||
segments: segments |
||||
} |
||||
} catch (e) { |
||||
this.$notify({ |
||||
title: "Error during extracting data", |
||||
text: e, |
||||
type: 'error' |
||||
}); |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
// TODO: move to store |
||||
// TODO: remove code repetition |
||||
async function resolveDataThreshold(range: TimeRange): Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
segments: Segment[] |
||||
}> { |
||||
|
||||
const endTime = Math.floor(range.to); |
||||
const startTime = Math.floor(range.from); |
||||
|
||||
const step = Math.max(Math.round((endTime - startTime) / 5000), 1); |
||||
|
||||
try { |
||||
// TODO: request in parallel |
||||
let [target, values] = await getMetrics(startTime, endTime, step); |
||||
let segments = await getSegments(startTime, endTime, false); |
||||
return { |
||||
timeserie: [{ target: target, datapoints: values, color: 'green' }], |
||||
segments: segments |
||||
} |
||||
} catch (e) { |
||||
this.$notify({ |
||||
title: "Error during extracting data", |
||||
text: e, |
||||
type: 'error' |
||||
}); |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
// TODO: move to store |
||||
// TODO: remove code repetition |
||||
async function resolveDataAnomaly(range: TimeRange): Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
hsr: AnomalyHSR, |
||||
segments: Segment[] |
||||
}> { |
||||
|
||||
const endTime = Math.floor(range.to); |
||||
const startTime = Math.floor(range.from); |
||||
|
||||
const step = Math.max(Math.round((endTime - startTime) / 5000), 1); |
||||
|
||||
try { |
||||
// TODO: request in parallel |
||||
let [target, values] = await getMetrics(startTime, endTime, step); |
||||
let segments = await getSegments(startTime, endTime, false); |
||||
let hsr = await getHSRAnomaly(startTime, endTime); |
||||
return { |
||||
timeserie: [ |
||||
{ target: target, datapoints: values, color: 'green' }, |
||||
], |
||||
hsr, |
||||
segments: segments, |
||||
} |
||||
} catch (e) { |
||||
this.$notify({ |
||||
title: "Error during extracting data", |
||||
text: e, |
||||
type: 'error' |
||||
}); |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
// TODO: move to store |
||||
async function addSegment(segment: Segment): Promise<SegmentId> { |
||||
try { |
||||
const id = await postSegment(segment); |
||||
return id; |
||||
} catch (e) { |
||||
this.$notify({ |
||||
title: "Error during saving segment", |
||||
text: e, |
||||
type: 'error' |
||||
}); |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
// TODO: move to store |
||||
async function _deleteSegment(from: number, to: number): Promise<number> { |
||||
try { |
||||
return await deleteSegment(from, to); |
||||
} catch (e) { |
||||
this.$notify({ |
||||
title: "Error during saving segment", |
||||
text: e, |
||||
type: 'error' |
||||
}); |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
// TODO: convert to class component |
||||
export default defineComponent({ |
||||
name: 'Graph', |
||||
props: {}, |
||||
mounted() { |
||||
this.rebuildGraph(); |
||||
}, |
||||
// TODO: it's a hack: listen real events about analytics update and use store |
||||
watch: { |
||||
analyticUnitConfig(newConfig, prevConfig) { |
||||
if(prevConfig == null) { |
||||
return; |
||||
} |
||||
// TODO: remove this hack |
||||
if(!_.isEqual(_.keys(newConfig),_.keys(prevConfig))) { |
||||
return; |
||||
} |
||||
|
||||
this.rerender(); |
||||
}, |
||||
analyticUnitType(newType, prevType) { |
||||
this.rebuildGraph(); |
||||
} |
||||
}, |
||||
methods: { |
||||
rerender() { |
||||
this.pod.fetchData(); |
||||
}, |
||||
async deleteAllSegments() { |
||||
await _deleteSegment.bind(this)(0, Date.now()); |
||||
this.rerender(); |
||||
}, |
||||
rebuildGraph() { |
||||
let child = document.getElementById('chart').children[0]; |
||||
if(child != undefined) { |
||||
document.getElementById('chart').removeChild(child); |
||||
} |
||||
var sa = new SegmentArray(); |
||||
|
||||
const aut = this.analyticUnitType; |
||||
if(aut == null) { |
||||
return; |
||||
} |
||||
|
||||
if(aut === AnalyticUnitType.PATTERN) { |
||||
this.pod = new PatternPod( |
||||
document.getElementById('chart'), |
||||
resolveDataPatterns.bind(this), |
||||
addSegment.bind(this), |
||||
_deleteSegment.bind(this), |
||||
sa |
||||
); |
||||
} |
||||
if(aut === AnalyticUnitType.THRESHOLD) { |
||||
this.pod = new ThresholdPod( |
||||
document.getElementById('chart'), |
||||
resolveDataThreshold.bind(this), |
||||
sa |
||||
); |
||||
} |
||||
if(aut === AnalyticUnitType.ANOMALY) { |
||||
this.pod = new AnomalyPod( |
||||
document.getElementById('chart'), |
||||
resolveDataAnomaly.bind(this), |
||||
this.setSeasonality.bind(this), |
||||
sa |
||||
); |
||||
} |
||||
this.pod.render(); |
||||
}, |
||||
setSeasonality(from: number, to: number) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
// TODO: get 10 (step) from API config |
||||
cfg.seasonality = Math.ceil(Math.abs(from - to) / 10) * 10; |
||||
this.$store.dispatch('patchConfig', { Anomaly: cfg }); |
||||
} |
||||
}, |
||||
computed: { |
||||
analyticUnitConfig() { |
||||
return this.$store.state.analyticUnitConfig; |
||||
}, |
||||
analyticUnitType() { |
||||
return this.$store.state.analyticUnitType; |
||||
} |
||||
} |
||||
}); |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
#chart { |
||||
margin: auto; |
||||
width: 80%; |
||||
height: 350px; |
||||
} |
||||
</style> |
@ -0,0 +1,102 @@
|
||||
<template> |
||||
<div> |
||||
<h3>MODEL</h3> |
||||
<div id="chart"></div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
||||
import { ChartwerkScatterPod } from "@chartwerk/scatter-pod"; |
||||
|
||||
import _ from "lodash"; |
||||
|
||||
|
||||
export default defineComponent({ |
||||
name: 'ScatterPlot', |
||||
props: {}, |
||||
mounted() { |
||||
// var pod = new ChartwerkScatterPod( |
||||
// document.getElementById('chart'), |
||||
// [ |
||||
// { |
||||
// target: 'test1', |
||||
// datapoints: [ |
||||
// [100, -50, 0], |
||||
// [200, 150, 0], |
||||
// [100, 160, 1], |
||||
// [150, 170, 1], |
||||
// [150, 180, 0], |
||||
// [150, 250, 1] |
||||
// ] as [number, number][], |
||||
// color: 'red', |
||||
// lineType: 'dashed', |
||||
// pointType: 'circle' |
||||
// }, |
||||
// { |
||||
// target: 'test2', |
||||
// datapoints: [ |
||||
// [200, 50, 1], |
||||
// [175, 60, 0], |
||||
// [150, 70, 1] |
||||
// ], |
||||
// color: 'purple', |
||||
// pointType: 'rectangle', |
||||
// pointSize: 5, |
||||
// yOrientation: 'right', |
||||
// } |
||||
// ], |
||||
// { |
||||
// axis: { |
||||
// x: { |
||||
// format: 'numeric', |
||||
// range: [-100, 300] |
||||
// }, |
||||
// y: { |
||||
// invert: true, |
||||
// range: [-100, 250] |
||||
// }, |
||||
// y1: { |
||||
// isActive: true, |
||||
// range: [0, 250] |
||||
// } |
||||
// }, |
||||
// zoomEvents: { |
||||
// mouse: { |
||||
// pan: { isActive: false, orientation: 'both', keyEvent: 'main' }, |
||||
// zoom: { isActive: true, keyEvent: 'shift' }, |
||||
// }, |
||||
// scroll: { |
||||
// pan: { isActive: false }, |
||||
// zoom: { isActive: true, keyEvent: 'main' } |
||||
// } |
||||
// }, |
||||
// crosshair: { |
||||
// orientation: 'both', |
||||
// color: 'gray' |
||||
// }, |
||||
// labelFormat: { |
||||
// yAxis: 'y', |
||||
// xAxis: 'x' |
||||
// }, |
||||
// eventsCallbacks: { |
||||
// zoomOut: () => { pod.render() } |
||||
// }, |
||||
// margin: { top: 30, right: 30, bottom: 40, left: 30 }, |
||||
// circleView: true, |
||||
// } |
||||
// ); |
||||
}, |
||||
methods: { |
||||
|
||||
} |
||||
}); |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
#chart { |
||||
margin: auto; |
||||
width: 80%; |
||||
height: 350px; |
||||
} |
||||
</style> |
@ -0,0 +1,166 @@
|
||||
import { HasticPod } from './hastic_pod'; |
||||
import { AnomalyHSR, TimeRange } from '@/types'; |
||||
|
||||
import { Segment } from "@/types/segment"; |
||||
import { LineTimeSerie } from '@chartwerk/line-pod'; |
||||
import { SegmentsSet } from '@/types/segment_set'; |
||||
|
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
export type UpdateDataCallback = (range: TimeRange) => Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
hsr: AnomalyHSR, |
||||
segments: Segment[] |
||||
}>; |
||||
|
||||
export type SetSeasonalityCallback = (from: number, to: number) => void; |
||||
|
||||
|
||||
export class AnomalyPod extends HasticPod<UpdateDataCallback> { |
||||
|
||||
private _ssc: SetSeasonalityCallback; |
||||
|
||||
private _hsr: AnomalyHSR; |
||||
|
||||
private _zKeyIsDown: boolean; |
||||
|
||||
private _labelSeasonality: boolean; |
||||
|
||||
constructor( |
||||
el: HTMLElement, |
||||
udc: UpdateDataCallback, |
||||
ssc: SetSeasonalityCallback, |
||||
segmentSet: SegmentsSet<Segment> |
||||
) { |
||||
super(el, udc, segmentSet); |
||||
this._zKeyIsDown = false; |
||||
this._ssc = ssc; |
||||
|
||||
window.addEventListener("keydown", e => { |
||||
if(e.code == "KeyZ") { |
||||
this._zKeyIsDown = true; |
||||
} |
||||
}); |
||||
|
||||
window.addEventListener("keyup", (e) => { |
||||
if(e.code == "KeyZ") { |
||||
this._zKeyIsDown = false; |
||||
} |
||||
}); |
||||
|
||||
this.fetchData(); |
||||
} |
||||
|
||||
protected onBrushStart(): void { |
||||
if(this._zKeyIsDown) { |
||||
this._labelSeasonality = true; |
||||
this.svg.select('.selection') |
||||
.attr('fill', 'orange'); |
||||
} |
||||
|
||||
// TODO: move to state
|
||||
this.isBrushing === true; |
||||
const selection = this.d3.event.selection; |
||||
if(selection !== null && selection.length > 0) { |
||||
this.brushStartSelection = this.d3.event.selection[0]; |
||||
} |
||||
this.onMouseOut(); |
||||
} |
||||
|
||||
protected onBrushEnd(): void { |
||||
console.log("END"); |
||||
if(!this._labelSeasonality) { |
||||
super.onBrushEnd(); |
||||
} else { |
||||
const extent = this.d3.event.selection; |
||||
this.isBrushing === false; |
||||
if(extent === undefined || extent === null || extent.length < 2) { |
||||
return; |
||||
} |
||||
this.chartContainer |
||||
.call(this.brush.move, null); |
||||
|
||||
const startTimestamp = this.xScale.invert(extent[0]); |
||||
const endTimestamp = this.xScale.invert(extent[1]); |
||||
|
||||
if(this._labelSeasonality) { |
||||
this._ssc(startTimestamp, endTimestamp); |
||||
this._labelSeasonality = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
public fetchData(): void { |
||||
let to = Math.floor(Date.now() / 1000); |
||||
let from = to - 50000; // -50000 seconds
|
||||
|
||||
if(!(this.state.xValueRange[0] == 0 && this.state.xValueRange[1] == 1)) { |
||||
[from, to] = this.state?.xValueRange; |
||||
} |
||||
|
||||
this.udc({ from, to }) |
||||
.then(resp => { |
||||
this.updateSegments(resp.segments); |
||||
this.updateHSR(resp.hsr); |
||||
this.updateData(resp.timeserie, undefined, true); |
||||
}) |
||||
.catch(() => { /* set "error" message */ }) |
||||
} |
||||
|
||||
renderMetrics() { |
||||
this.renderHSR() |
||||
super.renderMetrics(); |
||||
} |
||||
|
||||
updateHSR(hsr: AnomalyHSR) { |
||||
this._hsr = hsr; |
||||
} |
||||
|
||||
renderHSR() { |
||||
// TODO: check the case when this._bounds == undefined
|
||||
if(this._hsr == undefined) { |
||||
return; |
||||
} |
||||
|
||||
const pointsUp = this._hsr.ts.map(([t, v, [p, q]]) => [t, q]); |
||||
const pointsDown = this._hsr.ts.map(([t, v, [p, q]]) => [t, p]); |
||||
|
||||
const points = pointsUp.reverse().concat(pointsDown) |
||||
.map(([t, v]) => `${this.xScale(t)},${this.yScale(v)}`) |
||||
.join(' ') |
||||
|
||||
this.metricContainer |
||||
.append('g') |
||||
.append('polygon') |
||||
.attr('fill', 'green') |
||||
.attr('stroke', 'none') |
||||
.attr('fill-opacity', 0.2) |
||||
.attr('pointer-events', 'none') |
||||
.attr('points', points); |
||||
|
||||
// seasonality grid
|
||||
let ts = this._hsr.timestamp; |
||||
this._renderHSRGridLine(ts, true); |
||||
ts -= this._hsr.seasonality; |
||||
while(ts > this.state.xValueRange[0]) { |
||||
this._renderHSRGridLine(ts, false); |
||||
ts -= this._hsr.seasonality; |
||||
} |
||||
} |
||||
|
||||
_renderHSRGridLine(timestamp, head) { |
||||
const x = this.xScale(timestamp); |
||||
|
||||
this.metricContainer |
||||
.append('line') |
||||
.attr('x1', x) |
||||
.attr('x2', x) |
||||
.attr('y1', 0) |
||||
.attr('y2', this.height) |
||||
.attr("style", `stroke:blue;stroke-width: ${head ? 2 : 1}`) |
||||
.attr('opacity', head ? 0.5 : 0.3) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,109 @@
|
||||
import { LinePod, LineTimeSerie } from "@chartwerk/line-pod"; |
||||
import { AxisRange } from "@chartwerk/core/dist/types"; |
||||
import { BrushOrientation } from "@chartwerk/core"; |
||||
import { SegmentsSet } from "@/types/segment_set"; |
||||
import { ANALYTIC_UNIT_COLORS } from "@/types/colors" |
||||
import { Segment, SegmentId, SegmentType } from "@/types/segment"; |
||||
|
||||
|
||||
export abstract class HasticPod<T> extends LinePod { |
||||
|
||||
protected segmentsContainer; |
||||
|
||||
constructor( |
||||
el: HTMLElement, |
||||
protected udc: T, |
||||
protected segmentSet: SegmentsSet<Segment> |
||||
) { |
||||
super(el, undefined, { |
||||
renderLegend: true, |
||||
zoomEvents: { |
||||
mouse: { |
||||
zoom: { |
||||
isActive: true, |
||||
orientation: BrushOrientation.HORIZONTAL |
||||
} |
||||
} |
||||
}, |
||||
eventsCallbacks: { |
||||
zoomIn: range => { this.updateRange(range) }, |
||||
zoomOut: ({x, y}) => { this._zoomOut({x, y}) }, |
||||
panningEnd: range => { this.updateRange(range) } |
||||
} |
||||
}); |
||||
|
||||
|
||||
this.fetchData(); |
||||
} |
||||
|
||||
renderMetrics() { |
||||
super.renderMetrics(); |
||||
this.renderSegments(); |
||||
} |
||||
|
||||
protected addEvents(): void { |
||||
this.initBrush(); |
||||
this.initPan(); |
||||
|
||||
this.chartContainer |
||||
.on('mouseover', this.onMouseOver.bind(this)) |
||||
.on('mouseout', this.onMouseOut.bind(this)) |
||||
.on('mousemove', this.onMouseMove.bind(this)) |
||||
.on('dblclick.zoom', this.zoomOut.bind(this)); |
||||
} |
||||
|
||||
|
||||
protected renderSegments(): void { |
||||
const segments = this.segmentSet.getSegments(); |
||||
// TODO: this is a bad hack, don't know why
|
||||
if(this.metricContainer == null) { |
||||
return; |
||||
} |
||||
this.segmentsContainer = this.metricContainer |
||||
.insert('g', ':first-child') |
||||
.attr('class', 'segmentsContainer') |
||||
for (const s in segments) { |
||||
this.renderSegment(segments[s]); |
||||
} |
||||
} |
||||
|
||||
protected renderSegment(segment: Segment): void { |
||||
const x = this.xScale(segment.from); |
||||
const y = 0; |
||||
const w = this.xScale(segment.to) - x; |
||||
const h = this.height |
||||
|
||||
const r = this.segmentsContainer |
||||
.append('rect') |
||||
.attr('x', x) |
||||
.attr('y', y) |
||||
.attr('width', w) |
||||
.attr('height', h) |
||||
.attr('fill', ANALYTIC_UNIT_COLORS[0]) |
||||
.attr('opacity', '0.8') |
||||
.attr('pointer-events', 'none') |
||||
|
||||
if(segment.segmentType == SegmentType.LABEL || segment.segmentType == SegmentType.ANTI_LABEL) { |
||||
r.attr('style', 'stroke:rgb(0,0,0); stroke-width:2') |
||||
} |
||||
if(segment.segmentType == SegmentType.ANTI_LABEL) { |
||||
r.attr('fill', ANALYTIC_UNIT_COLORS[1]) |
||||
} |
||||
} |
||||
|
||||
protected async updateRange(range: AxisRange[]) { |
||||
this.fetchData(); |
||||
} |
||||
|
||||
protected _zoomOut({x, y}): void { |
||||
this.fetchData(); |
||||
} |
||||
|
||||
protected updateSegments(segments: Segment[]): void { |
||||
this.segmentSet.clear(); |
||||
this.segmentSet.setSegments(segments); |
||||
} |
||||
|
||||
abstract fetchData(); |
||||
|
||||
} |
@ -0,0 +1,5 @@
|
||||
import { HasticPod } from './hastic_pod'; |
||||
import { PatternPod } from './pattern_pod'; |
||||
|
||||
|
||||
import { AnalyticUnitType } from '@/types/analytic_units'; |
@ -0,0 +1,187 @@
|
||||
import { HasticPod } from './hastic_pod'; |
||||
import { TimeRange } from '@/types'; |
||||
|
||||
import { Segment, SegmentId, SegmentType } from "@/types/segment"; |
||||
import { LineTimeSerie } from '@chartwerk/line-pod'; |
||||
import { SegmentsSet } from '@/types/segment_set'; |
||||
|
||||
|
||||
export type UpdateDataCallback = (range: TimeRange) => Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
segments: Segment[] |
||||
}>; |
||||
export type CreateSegmentCallback = (segment: Segment) => Promise<SegmentId>; |
||||
export type DeleteSegmentCallback = (from: number, to: number) => Promise<number>; |
||||
|
||||
|
||||
export class PatternPod extends HasticPod<UpdateDataCallback> { |
||||
|
||||
private _csc: CreateSegmentCallback; |
||||
private _dsc: DeleteSegmentCallback; |
||||
|
||||
private _aKeyIsDown: boolean; |
||||
private _sKeyIsDown: boolean; |
||||
private _dKeyIsDown: boolean; |
||||
private _labelBrush: boolean; |
||||
private _antiLabelBrush: boolean; |
||||
private _deleteBrush: boolean; |
||||
|
||||
constructor( |
||||
el: HTMLElement, |
||||
udc: UpdateDataCallback, |
||||
csc: CreateSegmentCallback, |
||||
dsc: DeleteSegmentCallback, |
||||
segmentSet: SegmentsSet<Segment> |
||||
) { |
||||
super(el, udc, segmentSet) |
||||
|
||||
this._csc = csc; |
||||
this._dsc = dsc; |
||||
|
||||
this._sKeyIsDown = false; |
||||
this._aKeyIsDown = false; |
||||
this._dKeyIsDown = false; |
||||
|
||||
this._labelBrush = false; |
||||
this._antiLabelBrush = false; |
||||
|
||||
window.addEventListener("keydown", e => { |
||||
if(e.code == "KeyA") { |
||||
this._aKeyIsDown = true; |
||||
} |
||||
if(e.code == "KeyS") { |
||||
this._sKeyIsDown = true; |
||||
} |
||||
if(e.code == 'KeyD') { |
||||
this._dKeyIsDown = true; |
||||
} |
||||
}); |
||||
window.addEventListener("keyup", (e) => { |
||||
if(e.code == "KeyA") { |
||||
this._aKeyIsDown = false; |
||||
} |
||||
if(e.code == "KeyS") { |
||||
this._sKeyIsDown = false; |
||||
} |
||||
if(e.code == 'KeyD') { |
||||
this._dKeyIsDown = false; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public fetchData(): void { |
||||
let to = Math.floor(Date.now() / 1000); |
||||
let from = to - 50000; // -50000 seconds
|
||||
|
||||
if(!(this.state.xValueRange[0] == 0 && this.state.xValueRange[1] == 1)) { |
||||
[from, to] = this.state?.xValueRange; |
||||
console.log('took from range from state'); |
||||
} else { |
||||
console.log('took from range from default'); |
||||
} |
||||
|
||||
this.udc({ from, to }) |
||||
.then(resp => { |
||||
this.updateSegments(resp.segments); |
||||
this.updateData(resp.timeserie, undefined, true); |
||||
}) |
||||
.catch(() => { /* set "error" message */ }) |
||||
} |
||||
|
||||
|
||||
protected onBrushStart(): void { |
||||
if(this._sKeyIsDown) { |
||||
this._labelBrush = true; |
||||
this.svg.select('.selection') |
||||
.attr('fill', 'red'); |
||||
} else if (this._aKeyIsDown) { |
||||
this._antiLabelBrush = true; |
||||
this.svg.select('.selection') |
||||
.attr('fill', 'blue'); |
||||
} else if (this._dKeyIsDown) { |
||||
this._deleteBrush = true; |
||||
this.svg.select('.selection') |
||||
.attr('fill', 'darkgreen'); |
||||
} |
||||
|
||||
// TODO: move to state
|
||||
this.isBrushing === true; |
||||
const selection = this.d3.event.selection; |
||||
if(selection !== null && selection.length > 0) { |
||||
this.brushStartSelection = this.d3.event.selection[0]; |
||||
} |
||||
this.onMouseOut(); |
||||
} |
||||
|
||||
protected onBrushEnd(): void {
|
||||
if(!this._labelBrush && !this._antiLabelBrush && !this._deleteBrush) { |
||||
super.onBrushEnd(); |
||||
} else { |
||||
const extent = this.d3.event.selection; |
||||
this.isBrushing === false; |
||||
if(extent === undefined || extent === null || extent.length < 2) { |
||||
return; |
||||
} |
||||
this.chartContainer |
||||
.call(this.brush.move, null); |
||||
|
||||
const startTimestamp = this.xScale.invert(extent[0]); |
||||
const endTimestamp = this.xScale.invert(extent[1]); |
||||
|
||||
if(this._labelBrush) { |
||||
this.addSegment(startTimestamp, endTimestamp, SegmentType.LABEL); |
||||
this._labelBrush = false; |
||||
} |
||||
if(this._antiLabelBrush) { |
||||
this.addSegment(startTimestamp, endTimestamp, SegmentType.ANTI_LABEL); |
||||
this._antiLabelBrush = false; |
||||
} |
||||
if(this._deleteBrush) { |
||||
this.deleteSegment(startTimestamp, endTimestamp); |
||||
this._deleteBrush = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
protected async addSegment(from: number, to: number, type: SegmentType): Promise<void> { |
||||
const id = this.getNewTempSegmentId(); |
||||
from = Math.floor(from); |
||||
to = Math.ceil(to); |
||||
|
||||
if (from > to) { |
||||
const t = from; |
||||
from = to; |
||||
to = t; |
||||
} |
||||
|
||||
const segment = new Segment(id, from, to, type); |
||||
|
||||
await this._csc(segment); |
||||
this.fetchData(); |
||||
|
||||
} |
||||
|
||||
protected async deleteSegment(from: number, to: number): Promise<void> { |
||||
from = Math.floor(from); |
||||
to = Math.ceil(to); |
||||
|
||||
if (from > to) { |
||||
const t = from; |
||||
from = to; |
||||
to = t; |
||||
} |
||||
|
||||
await this._dsc(from, to); |
||||
this.fetchData(); |
||||
|
||||
} |
||||
|
||||
// TODO: move to "controller"
|
||||
private _tempIdCounted = -1; |
||||
public getNewTempSegmentId(): SegmentId { |
||||
this._tempIdCounted--; |
||||
return this._tempIdCounted.toString(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,44 @@
|
||||
import { HasticPod } from './hastic_pod'; |
||||
import { TimeRange } from '@/types'; |
||||
|
||||
import { Segment } from "@/types/segment"; |
||||
import { LineTimeSerie } from '@chartwerk/line-pod'; |
||||
import { SegmentsSet } from '@/types/segment_set'; |
||||
|
||||
|
||||
export type UpdateDataCallback = (range: TimeRange) => Promise<{ |
||||
timeserie: LineTimeSerie[], |
||||
segments: Segment[] |
||||
}>; |
||||
|
||||
|
||||
export class ThresholdPod extends HasticPod<UpdateDataCallback> { |
||||
|
||||
constructor( |
||||
el: HTMLElement, |
||||
udc: UpdateDataCallback, |
||||
segmentSet: SegmentsSet<Segment> |
||||
) { |
||||
super(el, udc, segmentSet) |
||||
|
||||
this.fetchData(); |
||||
} |
||||
|
||||
|
||||
public fetchData(): void { |
||||
let to = Math.floor(Date.now() / 1000); |
||||
let from = to - 50000; // -50000 seconds
|
||||
|
||||
if(!(this.state.xValueRange[0] == 0 && this.state.xValueRange[1] == 1)) { |
||||
[from, to] = this.state?.xValueRange; |
||||
} |
||||
|
||||
this.udc({ from, to }) |
||||
.then(resp => { |
||||
this.updateSegments(resp.segments); |
||||
this.updateData(resp.timeserie, undefined, true); |
||||
}) |
||||
.catch(() => { /* set "error" message */ }) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,5 @@
|
||||
export const API_URL = process.env.VUE_APP_API_URL |
||||
|
||||
if(API_URL === undefined) { |
||||
throw new Error("API_URL is undefined!"); |
||||
} |
@ -0,0 +1,12 @@
|
||||
import { createApp } from 'vue' |
||||
import App from './App.vue' |
||||
import router from './router' |
||||
import store from './store' |
||||
|
||||
import Notifications from '@kyvg/vue3-notification' |
||||
|
||||
createApp(App) |
||||
.use(store) |
||||
.use(router) |
||||
.use(Notifications) |
||||
.mount('#app') |
@ -0,0 +1,37 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' |
||||
import Home from '../views/Home.vue' |
||||
import Login from '../views/Login.vue' |
||||
import Model from '../views/Model.vue' |
||||
|
||||
const routes: Array<RouteRecordRaw> = [ |
||||
{ |
||||
path: '/', |
||||
name: 'Home', |
||||
component: Home |
||||
}, |
||||
{ |
||||
path: '/login', |
||||
name: 'Login', |
||||
component: Login |
||||
}, |
||||
{ |
||||
path: '/model', |
||||
name: 'Model', |
||||
component: Model |
||||
}, |
||||
{ |
||||
path: '/about', |
||||
name: 'About', |
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') |
||||
} |
||||
] |
||||
|
||||
const router = createRouter({ |
||||
history: createWebHistory(process.env.BASE_URL), |
||||
routes |
||||
}) |
||||
|
||||
export default router |
@ -0,0 +1,86 @@
|
||||
// TODO: https://github.com/hastic/hastic-grafana-app/blob/c67bd8af140105c36f24c875187929869e48e51e/src/panel/graph_panel/services/analytic_service.ts
|
||||
|
||||
import { API_URL } from "@/config"; |
||||
import axios from 'axios'; |
||||
|
||||
import { getGenerator } from '@/utils'; |
||||
|
||||
import { |
||||
AnalyticUnitType, AnlyticUnitConfig, |
||||
PatternConfig, ThresholdConfig, AnomalyConfig |
||||
} from "@/types/analytic_units"; |
||||
import { AnomalyHSR } from "@/types"; |
||||
import { AnalyticStatus } from "@/store"; |
||||
|
||||
import _ from 'lodash'; |
||||
|
||||
|
||||
const ANALYTICS_API_URL = API_URL + "analytics/"; |
||||
|
||||
export async function getStatus(): Promise<AnalyticStatus> { |
||||
const uri = ANALYTICS_API_URL + `status`; |
||||
try { |
||||
const res = await axios.get<{ status: string }>(uri); |
||||
const data = res.data; |
||||
return { |
||||
available: true, |
||||
message: data.status |
||||
}; |
||||
} catch (e) { |
||||
return { |
||||
available: false, |
||||
message: e.message |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export async function getConfig(): Promise<[AnalyticUnitType, AnlyticUnitConfig]> { |
||||
const uri = ANALYTICS_API_URL + `config`; |
||||
const res = await axios.get(uri); |
||||
|
||||
const data = res['data']; |
||||
|
||||
let analyticUnitType = AnalyticUnitType.ANOMALY; |
||||
let analyticUnitConfig = undefined; |
||||
if(data['Pattern'] !== undefined) { |
||||
analyticUnitType = AnalyticUnitType.PATTERN; |
||||
analyticUnitConfig = data['Pattern'] as PatternConfig |
||||
} |
||||
if(data['Threshold'] !== undefined) { |
||||
analyticUnitType = AnalyticUnitType.THRESHOLD; |
||||
analyticUnitConfig = data['Threshold'] as ThresholdConfig |
||||
} |
||||
if(data['Anomaly'] !== undefined) { |
||||
analyticUnitType = AnalyticUnitType.ANOMALY; |
||||
analyticUnitConfig = data['Anomaly'] as AnomalyConfig |
||||
} |
||||
|
||||
if(analyticUnitConfig === undefined) { |
||||
throw new Error("unknows config type" + _.keys(data)); |
||||
} |
||||
|
||||
return [analyticUnitType, analyticUnitConfig]; |
||||
} |
||||
|
||||
export async function patchConfig(patchObj: any) { |
||||
const uri = ANALYTICS_API_URL + `config`; |
||||
await axios.patch(uri, patchObj); |
||||
} |
||||
|
||||
export function getStatusGenerator(): AsyncIterableIterator<AnalyticStatus> { |
||||
return getGenerator<AnalyticStatus>(100, getStatus); |
||||
} |
||||
|
||||
|
||||
export async function getHSRAnomaly(from: number, to: number): Promise<AnomalyHSR> { |
||||
if(from >= to) { |
||||
throw new Error("`from` can`t be less than `to`"); |
||||
} |
||||
|
||||
const uri = ANALYTICS_API_URL + `hsr/?from=${from}&to=${to}`; |
||||
const res = await axios.get(uri); |
||||
|
||||
const values = res["data"]["AnomalyHSR"]; |
||||
|
||||
return values as AnomalyHSR; |
||||
} |
@ -0,0 +1,37 @@
|
||||
import { User } from "@/types/user"; |
||||
import { API_URL } from "@/config"; |
||||
|
||||
import axios from 'axios'; |
||||
|
||||
// TODO: get it from config
|
||||
const AUTH_API_URL = API_URL + 'auth/'; |
||||
|
||||
class AuthService { |
||||
login(user: User) { |
||||
return axios |
||||
.post(AUTH_API_URL + 'signin', { |
||||
username: user.username, |
||||
password: user.password |
||||
}) |
||||
.then((response: any) => { |
||||
if (response.data.accessToken) { |
||||
localStorage.setItem('user', JSON.stringify(response.data)); |
||||
} |
||||
return response.data; |
||||
}); |
||||
} |
||||
|
||||
logout() { |
||||
localStorage.removeItem('user'); |
||||
} |
||||
|
||||
register(user: User) { |
||||
return axios.post(AUTH_API_URL + 'signup', { |
||||
username: user.username, |
||||
email: user.email, |
||||
password: user.password |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export default new AuthService(); |
@ -0,0 +1,24 @@
|
||||
// TODO: https://github.com/hastic/hastic-grafana-app/blob/c67bd8af140105c36f24c875187929869e48e51e/src/panel/graph_panel/services/analytic_service.ts
|
||||
|
||||
import { API_URL } from "@/config"; |
||||
import axios from 'axios'; |
||||
|
||||
import _ from 'lodash'; |
||||
|
||||
const METRICS_API_URL = API_URL + "metric/"; |
||||
|
||||
export async function getMetrics(from: number, to: number, step: number) { |
||||
if(from >= to) { |
||||
throw new Error("`from` can`t be less than `to`"); |
||||
} |
||||
if(step < 1) { |
||||
throw new Error("`step` can`t be less than 1"); |
||||
} |
||||
|
||||
const uri = METRICS_API_URL + `?from=${from}&to=${to}&step=${step}`; |
||||
const res = await axios.get(uri); |
||||
|
||||
const target = _.keys(res["data"]["data"])[0]; |
||||
const values = res["data"]["data"][target]; |
||||
return [target, values]; |
||||
} |
@ -0,0 +1,42 @@
|
||||
import { API_URL } from "@/config"; |
||||
import { Segment, SegmentId } from "@/types/segment"; |
||||
import axios from 'axios'; |
||||
|
||||
import _ from 'lodash'; |
||||
|
||||
const SEGMENTS_API_URL = API_URL + "segments/"; |
||||
const ANALYTICS_API_URL = API_URL + "analytics/"; |
||||
|
||||
export async function getSegments(from: number, to: number, withLabeling = true): Promise<Segment[]> { |
||||
if(from >= to) { |
||||
throw new Error("`from` can`t be less than `to`"); |
||||
} |
||||
|
||||
let result = []; |
||||
if (withLabeling) { |
||||
const uri = SEGMENTS_API_URL + `?from=${from}&to=${to}`; |
||||
const res = await axios.get(uri); |
||||
result = res["data"] as any[]; |
||||
} |
||||
|
||||
const uriAnalytics = ANALYTICS_API_URL + `?from=${from}&to=${to}`; |
||||
const resAnalytics = await axios.get(uriAnalytics); |
||||
|
||||
const resultAnalytics = resAnalytics["data"] as any[]; |
||||
|
||||
return result.concat(resultAnalytics).map(Segment.fromObject); |
||||
} |
||||
|
||||
export async function postSegment(segment: Segment): Promise<SegmentId> { |
||||
const segObj = segment.toObject(); |
||||
segObj.id = undefined; // because we post a new segment. It's a hack
|
||||
|
||||
const resp = await axios.post(SEGMENTS_API_URL, segObj); |
||||
return resp['data']['id']; |
||||
} |
||||
|
||||
export async function deleteSegment(from: number, to: number): Promise<number> { |
||||
const uri = SEGMENTS_API_URL + `?from=${from}&to=${to}`; |
||||
const resp = await axios.delete(uri); |
||||
return resp['data']['count']; |
||||
} |
@ -0,0 +1,6 @@
|
||||
/* eslint-disable */ |
||||
declare module '*.vue' { |
||||
import type { DefineComponent } from 'vue' |
||||
const component: DefineComponent<{}, {}, any> |
||||
export default component |
||||
} |
@ -0,0 +1,62 @@
|
||||
import AuthService from '@/services/auth.service'; |
||||
import { User } from '@/types/user'; |
||||
|
||||
const user = JSON.parse(localStorage.getItem('user')!); |
||||
const initialState = user |
||||
? { status: { loggedIn: true }, user } |
||||
: { status: { loggedIn: false }, user: null }; |
||||
|
||||
export const auth = { |
||||
namespaced: true, |
||||
state: initialState, |
||||
actions: { |
||||
login({ commit }, user: User) { |
||||
return AuthService.login(user).then( |
||||
user => { |
||||
commit('loginSuccess', user); |
||||
return Promise.resolve(user); |
||||
}, |
||||
error => { |
||||
commit('loginFailure'); |
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
}, |
||||
logout({ commit }) { |
||||
AuthService.logout(); |
||||
commit('logout'); |
||||
}, |
||||
register({ commit }, user) { |
||||
return AuthService.register(user).then( |
||||
response => { |
||||
commit('registerSuccess'); |
||||
return Promise.resolve(response.data); |
||||
}, |
||||
error => { |
||||
commit('registerFailure'); |
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
} |
||||
}, |
||||
mutations: { |
||||
loginSuccess(state, user) { |
||||
state.status.loggedIn = true; |
||||
state.user = user; |
||||
}, |
||||
loginFailure(state) { |
||||
state.status.loggedIn = false; |
||||
state.user = null; |
||||
}, |
||||
logout(state) { |
||||
state.status.loggedIn = false; |
||||
state.user = null; |
||||
}, |
||||
registerSuccess(state) { |
||||
state.status.loggedIn = false; |
||||
}, |
||||
registerFailure(state) { |
||||
state.status.loggedIn = false; |
||||
} |
||||
} |
||||
}; |
@ -0,0 +1,94 @@
|
||||
import { auth } from "./auth.module"; |
||||
import { createStore } from 'vuex' |
||||
import { getConfig, getStatusGenerator, patchConfig } from "@/services/analytics.service"; |
||||
import { AnlyticUnitConfig, AnalyticUnitType } from '@/types/analytic_units' |
||||
// import { notify } from "@kyvg/vue3-notification";
|
||||
|
||||
|
||||
const SET_ANALYTICS_STATUS = 'SET_ANALYTICS_STATUS'; |
||||
const SET_DETECTOR_CONFIG = 'SET_DETECTOR_CONFIG'; |
||||
// const PATCH_CONFIG = 'PATCH_CONFIG';
|
||||
const _SET_STATUS_GENERATOR = '_SET_STATUS_GENERATOR'; |
||||
|
||||
// TODO: consts for actions
|
||||
|
||||
export type AnalyticStatus = { |
||||
available: boolean, |
||||
message: string, |
||||
} |
||||
|
||||
type State = { |
||||
analyticStatus: AnalyticStatus, |
||||
analyticUnitType?: AnalyticUnitType, |
||||
analyticUnitConfig?: AnlyticUnitConfig, |
||||
_statusGenerator: AsyncIterableIterator<AnalyticStatus> |
||||
} |
||||
|
||||
const store = createStore<State>({ |
||||
state: { |
||||
analyticStatus: { |
||||
available: false, |
||||
message: 'loading...', |
||||
}, |
||||
analyticUnitType: null, |
||||
analyticUnitConfig: null, |
||||
_statusGenerator: null |
||||
}, |
||||
mutations: { |
||||
[SET_ANALYTICS_STATUS](state, status: AnalyticStatus) { |
||||
state.analyticStatus = status; |
||||
}, |
||||
[SET_DETECTOR_CONFIG](state, { analyticUnitType, analyticUnitConfig }) {
|
||||
state.analyticUnitType = analyticUnitType; |
||||
state.analyticUnitConfig = analyticUnitConfig; |
||||
}, |
||||
// [PATCH_CONFIG](state, patchObj) {
|
||||
// patchConfig(patchConfig)
|
||||
// }
|
||||
[_SET_STATUS_GENERATOR](state, generator: AsyncIterableIterator<AnalyticStatus>) { |
||||
state._statusGenerator = generator; |
||||
} |
||||
}, |
||||
actions: { |
||||
async initData() { |
||||
this.dispatch('fetchConfig'); |
||||
this.dispatch('_runStatusGenerator'); |
||||
}, |
||||
|
||||
async _runStatusGenerator({commit, state}) { |
||||
// notify({
|
||||
// title: "Authorization",
|
||||
// text: "You have been logged in!",
|
||||
// });
|
||||
if(state._statusGenerator !== null) { |
||||
return; |
||||
} |
||||
|
||||
const g = getStatusGenerator(); |
||||
commit(_SET_STATUS_GENERATOR, g); |
||||
for await (const data of state._statusGenerator) { |
||||
// const st = data.toLocaleLowerCase();
|
||||
// if(state.analyticStatus.toLocaleLowerCase() != 'ready' && st == 'ready') {
|
||||
// TODO: update segments from here
|
||||
// }
|
||||
// this.status = data.toLowerCase();
|
||||
commit(SET_ANALYTICS_STATUS, data); |
||||
} |
||||
}, |
||||
async fetchConfig({commit}) { |
||||
const [analyticUnitType, analyticUnitConfig] = await getConfig(); |
||||
commit(SET_DETECTOR_CONFIG, { analyticUnitType, analyticUnitConfig }); |
||||
}, |
||||
async patchConfig({commit}, payload) { |
||||
await patchConfig(payload); |
||||
this.dispatch('fetchConfig'); |
||||
} |
||||
}, |
||||
modules: { |
||||
auth |
||||
} |
||||
}) |
||||
|
||||
store.dispatch('initData'); |
||||
|
||||
export default store; |
@ -0,0 +1,210 @@
|
||||
import { SegmentsSet } from '@/types/segment_set'; |
||||
import { SegmentArray } from '@/types/segment_array'; |
||||
import { Segment, SegmentId } from '@/types/segment'; |
||||
import { DetectionSpan } from '../detection'; |
||||
|
||||
import { ANALYTIC_UNIT_COLORS, DEFAULT_DELETED_SEGMENT_COLOR } from '@/types/colors'; |
||||
|
||||
import _ from 'lodash'; |
||||
|
||||
|
||||
// TODO: move types to ./types
|
||||
export enum AnalyticUnitType { |
||||
PATTERN = 'Pattern', |
||||
THRESHOLD = 'Threshold', |
||||
ANOMALY = 'Anomaly' |
||||
} |
||||
|
||||
export type PatternConfig = { |
||||
correlation_score: number,
|
||||
model_score: number |
||||
} |
||||
|
||||
export type ThresholdConfig = { |
||||
threashold: number |
||||
} |
||||
|
||||
export type AnomalyConfig = { |
||||
threashold: number |
||||
} |
||||
|
||||
export type AnlyticUnitConfig = PatternConfig | ThresholdConfig; |
||||
|
||||
export enum LabelingMode { |
||||
LABELING = 'LABELING', |
||||
UNLABELING = 'UNLABELING', |
||||
DELETING = 'DELETING', |
||||
NOT_IN_LABELING_MODE = 'NOT_IN_LABELING_MODE' |
||||
} |
||||
|
||||
export type AnalyticSegmentPair = { analyticUnit: AnalyticUnit, segment: AnalyticSegment }; |
||||
export type AnalyticSegmentsSearcher = (point: number, rangeDist: number) => AnalyticSegmentPair[]; |
||||
|
||||
export type AnalyticUnitId = string; |
||||
|
||||
export class AnalyticSegment extends Segment { |
||||
constructor(public labeled: boolean, id: SegmentId, from: number, to: number, public deleted = false) { |
||||
super(id, from, to); |
||||
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'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const DEFAULTS = { |
||||
id: null, |
||||
name: 'AnalyticUnitName', |
||||
type: 'GENERAL', |
||||
detectorType: AnalyticUnitType.PATTERN, |
||||
labeledColor: ANALYTIC_UNIT_COLORS[0], |
||||
deletedColor: DEFAULT_DELETED_SEGMENT_COLOR, |
||||
alert: false, |
||||
visible: true, |
||||
collapsed: false |
||||
}; |
||||
|
||||
const LABELING_MODES = []; |
||||
|
||||
export class AnalyticUnit { |
||||
|
||||
private _labelingMode: LabelingMode = LabelingMode.LABELING; |
||||
private _selected = false; |
||||
private _saving = false; |
||||
private _segmentSet = new SegmentArray<AnalyticSegment>(); |
||||
private _detectionSpans: DetectionSpan[]; |
||||
private _inspect = false; |
||||
private _changed = false; |
||||
private _status: string; |
||||
private _error: string; |
||||
|
||||
// TODO: serverObject -> fields
|
||||
constructor(protected _serverObject?: any) { |
||||
if(_serverObject === undefined) { |
||||
this._serverObject = _.clone(DEFAULTS); |
||||
} |
||||
_.defaults(this._serverObject, DEFAULTS); |
||||
} |
||||
|
||||
toJSON() { |
||||
return { |
||||
id: this.id, |
||||
name: this.name, |
||||
// TODO: enum type
|
||||
// TODO: type -> subType
|
||||
type: this.type, |
||||
// TODO: detectorType -> type
|
||||
detectorType: this.detectorType, |
||||
labeledColor: this.labeledColor, |
||||
deletedColor: this.deletedColor, |
||||
alert: this.alert, |
||||
visible: this.visible, |
||||
collapsed: this.collapsed |
||||
}; |
||||
} |
||||
|
||||
get id(): AnalyticUnitId { return this._serverObject.id; } |
||||
set id(value: AnalyticUnitId) { this._serverObject.id = value; } |
||||
|
||||
set name(value: string) { this._serverObject.name = value; } |
||||
get name(): string { return this._serverObject.name; } |
||||
|
||||
set detectorType(value: AnalyticUnitType) { this._serverObject.detectorType = value; } |
||||
get detectorType(): AnalyticUnitType { return this._serverObject.detectorType; } |
||||
|
||||
set type(value: string) { this._serverObject.type = value; } |
||||
get type(): string { return this._serverObject.type; } |
||||
|
||||
set labeledColor(value: string) { this._serverObject.labeledColor = value; } |
||||
get labeledColor(): string { return this._serverObject.labeledColor; } |
||||
|
||||
set deletedColor(value: string) { this._serverObject.deletedColor = value; } |
||||
get deletedColor(): string { return this._serverObject.deletedColor; } |
||||
|
||||
get collapsed(): boolean { return this._serverObject.collapsed; } |
||||
set collapsed(value: boolean) { this._serverObject.collapsed = value; } |
||||
|
||||
set alert(value: boolean) { this._serverObject.alert = value; } |
||||
get alert(): boolean { return this._serverObject.alert; } |
||||
|
||||
get selected(): boolean { return this._selected; } |
||||
set selected(value: boolean) { this._selected = value; } |
||||
|
||||
get labelingMode(): LabelingMode { return this._labelingMode; } |
||||
set labelingMode(value: LabelingMode) { this._labelingMode = value; } |
||||
|
||||
get saving(): boolean { return this._saving; } |
||||
set saving(value: boolean) { this._saving = value; } |
||||
|
||||
get changed(): boolean { return this._changed; } |
||||
set changed(value: boolean) { this._changed = value; } |
||||
|
||||
get inspect(): boolean { return this._inspect; } |
||||
set inspect(value: boolean) { this._inspect = value; } |
||||
|
||||
get visible(): boolean { |
||||
return (this._serverObject.visible === undefined) ? true : this._serverObject.visible |
||||
} |
||||
set visible(value: boolean) { |
||||
this._serverObject.visible = value; |
||||
} |
||||
|
||||
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[] { |
||||
const deletedSegments = this._segmentSet.removeInRange(from, to); |
||||
return deletedSegments; |
||||
} |
||||
|
||||
get segments(): SegmentsSet<AnalyticSegment> { return this._segmentSet; } |
||||
set segments(value: SegmentsSet<AnalyticSegment>) { |
||||
this._segmentSet.setSegments(value.getSegments()); |
||||
} |
||||
|
||||
get detectionSpans(): DetectionSpan[] { return this._detectionSpans; } |
||||
set detectionSpans(value: DetectionSpan[]) { this._detectionSpans = value; } |
||||
|
||||
get status() { return this._status; } |
||||
set status(value) { |
||||
// TODO: use enum
|
||||
if( |
||||
value !== '404' && |
||||
value !== 'READY' && |
||||
value !== 'LEARNING' && |
||||
value !== 'DETECTION' && |
||||
value !== 'PENDING' && |
||||
value !== 'FAILED' && |
||||
value !== 'SUCCESS' && |
||||
value !== null |
||||
) { |
||||
throw new Error('Unsupported status value: ' + value); |
||||
} |
||||
this._status = value; |
||||
} |
||||
|
||||
get error() { return this._error; } |
||||
set error(value) { this._error = value; } |
||||
|
||||
get isActiveStatus() { |
||||
switch(this.status) { |
||||
case '404': |
||||
case 'READY': |
||||
case 'FAILED': |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
get serverObject() { return this._serverObject; } |
||||
|
||||
// TODO: make it abstract
|
||||
get labelingModes() { |
||||
return LABELING_MODES; |
||||
} |
||||
} |
@ -0,0 +1,105 @@
|
||||
import tinycolor from 'tinycolor2'; |
||||
import { DetectionStatus } from '@/types/detection'; |
||||
|
||||
export const PALETTE_ROWS = 4; |
||||
export const PALETTE_COLUMNS = 14; |
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; |
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)'; |
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; |
||||
export const REGION_FILL_ALPHA = 0.09; |
||||
|
||||
const colors = [ |
||||
'#7EB26D', |
||||
'#EAB839', |
||||
'#6ED0E0', |
||||
'#EF843C', |
||||
'#E24D42', |
||||
'#1F78C1', |
||||
'#BA43A9', |
||||
'#705DA0', |
||||
'#508642', |
||||
'#CCA300', |
||||
'#447EBC', |
||||
'#C15C17', |
||||
'#890F02', |
||||
'#0A437C', |
||||
'#6D1F62', |
||||
'#584477', |
||||
'#B7DBAB', |
||||
'#F4D598', |
||||
'#70DBED', |
||||
'#F9BA8F', |
||||
'#F29191', |
||||
'#82B5D8', |
||||
'#E5A8E2', |
||||
'#AEA2E0', |
||||
'#629E51', |
||||
'#E5AC0E', |
||||
'#64B0C8', |
||||
'#E0752D', |
||||
'#BF1B00', |
||||
'#0A50A1', |
||||
'#962D82', |
||||
'#614D93', |
||||
'#9AC48A', |
||||
'#F2C96D', |
||||
'#65C5DB', |
||||
'#F9934E', |
||||
'#EA6460', |
||||
'#5195CE', |
||||
'#D683CE', |
||||
'#806EB7', |
||||
'#3F6833', |
||||
'#967302', |
||||
'#2F575E', |
||||
'#99440A', |
||||
'#58140C', |
||||
'#052B51', |
||||
'#511749', |
||||
'#3F2B5B', |
||||
'#E0F9D7', |
||||
'#FCEACA', |
||||
'#CFFAFF', |
||||
'#F9E2D2', |
||||
'#FCE2DE', |
||||
'#BADFF4', |
||||
'#F9D9F9', |
||||
'#DEDAF7', |
||||
]; |
||||
|
||||
export const ANALYTIC_UNIT_COLORS = [ |
||||
'#FF99FF', |
||||
'#71b1f9', |
||||
'#aee9fb', |
||||
'#9ce677', |
||||
'#f88990', |
||||
'#f9e26e', |
||||
'#f8c171', |
||||
]; |
||||
|
||||
export const DEFAULT_DELETED_SEGMENT_COLOR = '#00f0ff'; |
||||
export const REGION_UNLABEL_COLOR_LIGHT = '#d1d1d1'; |
||||
export const REGION_UNLABEL_COLOR_DARK = 'white'; |
||||
export const LABELED_SEGMENT_BORDER_COLOR = 'black'; |
||||
export const DELETED_SEGMENT_BORDER_COLOR = 'black'; |
||||
|
||||
export const SEGMENT_FILL_ALPHA = 0.5; |
||||
export const SEGMENT_STROKE_ALPHA = 0.8; |
||||
export const LABELING_MODE_ALPHA = 0.7; |
||||
|
||||
export const DETECTION_STATUS_COLORS = new Map<DetectionStatus, string>([ |
||||
[DetectionStatus.READY, 'green'], |
||||
[DetectionStatus.RUNNING, 'gold'], |
||||
[DetectionStatus.FAILED, 'red'] |
||||
]); |
||||
|
||||
export function hexToHsl(color) { |
||||
return tinycolor(color).toHsl(); |
||||
} |
||||
|
||||
export function hslToHex(color) { |
||||
return tinycolor(color).toHexString(); |
||||
} |
||||
|
||||
|
||||
export default colors; |
@ -0,0 +1,20 @@
|
||||
import { AnalyticUnitId } from '@/types/analytic_units/'; |
||||
|
||||
export enum DetectionStatus { |
||||
READY = 'READY', |
||||
RUNNING = 'RUNNING', |
||||
FAILED = 'FAILED' |
||||
} |
||||
|
||||
export type DetectionSpan = { |
||||
id: AnalyticUnitId, |
||||
status: DetectionStatus, |
||||
from: number, |
||||
to: number |
||||
}; |
||||
|
||||
export const DETECTION_STATUS_TEXT = new Map<DetectionStatus, string>([ |
||||
[DetectionStatus.READY, '[DetectionStatus]: done'], |
||||
[DetectionStatus.RUNNING, '[DetectionStatus]: running...'], |
||||
[DetectionStatus.FAILED, '[DetectionStatus]: failed'] |
||||
]); |
@ -0,0 +1,7 @@
|
||||
export type TimeRange = { from: number, to: number }; |
||||
|
||||
export type AnomalyHSR = { |
||||
seasonality: number, |
||||
timestamp: number, |
||||
ts: [number, number, [number, number]][] |
||||
}; |
@ -0,0 +1,67 @@
|
||||
export type SegmentId = string; |
||||
|
||||
export enum SegmentType { |
||||
LABEL = 'Label', |
||||
ANTI_LABEL = 'AntiLabel', |
||||
DETECTION = 'Detection' |
||||
} |
||||
|
||||
export class Segment { |
||||
constructor(private _id: SegmentId | undefined, public from: number, public to: number, segmentType = SegmentType.LABEL) { |
||||
if(this._id === undefined) { |
||||
throw new Error('id is undefined'); |
||||
} |
||||
if(isNaN(+from)) { |
||||
throw new Error('from can`t be NaN'); |
||||
} |
||||
if(isNaN(+to)) { |
||||
throw new Error('to can`t be NaN'); |
||||
} |
||||
this._segmentType = segmentType; |
||||
} |
||||
|
||||
get id(): SegmentId { return this._id; } |
||||
set id(value) { this._id = value; } |
||||
|
||||
get middle() { return (this.from + this.to) / 2; } |
||||
|
||||
get length() { |
||||
return Math.max(this.from, this.to) - Math.min(this.from, this.to); |
||||
} |
||||
|
||||
expandDist(allDist: number, portion: number): Segment { |
||||
let p = Math.round(this.middle - allDist * portion / 2); |
||||
let q = Math.round(this.middle + allDist * portion / 2); |
||||
p = Math.min(p, this.from); |
||||
q = Math.max(q, this.to); |
||||
return new Segment(this._id, p, q); |
||||
} |
||||
|
||||
equals(segment: Segment) { |
||||
return this._id === segment._id; |
||||
} |
||||
|
||||
// TODO: remove this and make original inheritence
|
||||
_segmentType: SegmentType |
||||
get segmentType(): SegmentType { return this._segmentType; } |
||||
set segmentType(value: SegmentType) { this._segmentType = value; } |
||||
|
||||
toObject() { |
||||
return { |
||||
id: this.id, |
||||
from: this.from, |
||||
to: this.to, |
||||
segment_type: this.segmentType |
||||
} |
||||
} |
||||
|
||||
static fromObject(obj: any) { |
||||
return new Segment( |
||||
obj.id, |
||||
obj.from, |
||||
obj.to, |
||||
obj.segment_type |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,101 @@
|
||||
import { SegmentsSet } from './segment_set'; |
||||
import { Segment, SegmentId } from './segment'; |
||||
|
||||
|
||||
export class SegmentArray<T extends Segment> implements SegmentsSet<T> { |
||||
private _segments: T[]; |
||||
private _keyToSegment: Map<SegmentId, T> = new Map<SegmentId, T>(); |
||||
|
||||
constructor(private segments?: T[]) { |
||||
this.setSegments(segments); |
||||
} |
||||
|
||||
getSegments(from?: number, to?: number): T[] { |
||||
if(from === undefined) { |
||||
from = -Infinity; |
||||
} |
||||
if(to === undefined) { |
||||
to = Infinity; |
||||
} |
||||
const result = []; |
||||
for(let i = 0; i < this._segments.length; i++) { |
||||
const s = this._segments[i]; |
||||
if(from <= s.from && s.to <= to) { |
||||
result.push(s); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
setSegments(segments: T[]) { |
||||
this._segments = []; |
||||
this._keyToSegment.clear(); |
||||
if(segments) { |
||||
segments.forEach(s => { |
||||
this.addSegment(s); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
addSegment(segment: T) { |
||||
if(this.has(segment.id)) { |
||||
throw new Error(`Segment with key ${segment.id} exists in set`); |
||||
} |
||||
this._keyToSegment.set(segment.id, segment); |
||||
this._segments.push(segment); |
||||
} |
||||
|
||||
findSegments(point: number, rangeDist: number): T[] { |
||||
return this._segments.filter(s => { |
||||
const expanded = s.expandDist(rangeDist, 0.01); |
||||
return (expanded.from <= point) && (point <= expanded.to); |
||||
}); |
||||
} |
||||
|
||||
removeInRange(from: number, to: number): T[] { |
||||
const deleted = []; |
||||
const newSegments = []; |
||||
for(let i = 0; i < this._segments.length; i++) { |
||||
const s = this._segments[i]; |
||||
if(from <= s.from && s.to <= to) { |
||||
this._keyToSegment.delete(s.id); |
||||
deleted.push(s); |
||||
} else { |
||||
newSegments.push(s); |
||||
} |
||||
} |
||||
this._segments = newSegments; |
||||
return deleted; |
||||
} |
||||
|
||||
get length() { |
||||
return this._segments.length; |
||||
} |
||||
|
||||
clear() { |
||||
this._segments = []; |
||||
this._keyToSegment.clear(); |
||||
} |
||||
|
||||
has(key: SegmentId): boolean { |
||||
return this._keyToSegment.has(key); |
||||
} |
||||
|
||||
remove(key: SegmentId): boolean { |
||||
if(!this.has(key)) { |
||||
return false; |
||||
} |
||||
const index = this._segments.findIndex(s => s.id === key); |
||||
this._segments.splice(index, 1); |
||||
this._keyToSegment.delete(key); |
||||
return true; |
||||
} |
||||
|
||||
updateId(fromKey: SegmentId, toKey: SegmentId) { |
||||
const segment = this._keyToSegment.get(fromKey); |
||||
this._keyToSegment.delete(fromKey); |
||||
segment.id = toKey; |
||||
this._keyToSegment.set(toKey, segment); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,14 @@
|
||||
import { Segment, SegmentId } from '@/types/segment' |
||||
|
||||
export interface SegmentsSet<T extends Segment> { |
||||
getSegments(from?: number, to?: number): T[]; |
||||
setSegments(segments: T[]): void; |
||||
addSegment(segment: T): void; |
||||
findSegments(point: number, rangeDist: number): T[]; |
||||
removeInRange(from: number, to: number): T[]; |
||||
remove(id: SegmentId): boolean; |
||||
has(id: SegmentId): boolean; |
||||
clear(): void; |
||||
updateId(fromId: SegmentId, toId: SegmentId): void; |
||||
length: number; |
||||
} |
@ -0,0 +1,5 @@
|
||||
export class User { |
||||
public username?: String |
||||
public email?: String |
||||
public password?: String |
||||
} |
@ -0,0 +1,15 @@
|
||||
export async function *getGenerator<T>( |
||||
duration: number, |
||||
func: (...args: any[]) => Promise<T>, |
||||
...args |
||||
): AsyncIterableIterator<T> { |
||||
|
||||
const timeout = async () => new Promise( |
||||
resolve => setTimeout(resolve, duration) |
||||
); |
||||
|
||||
while(true) { |
||||
yield await func(...args); |
||||
await timeout(); |
||||
} |
||||
} |
@ -0,0 +1,5 @@
|
||||
<template> |
||||
<div class="about"> |
||||
<h1>This is an about page</h1> |
||||
</div> |
||||
</template> |
@ -0,0 +1,163 @@
|
||||
<template> |
||||
<div class="home"> |
||||
<img alt="Vue logo" src="../assets/logo.png"> |
||||
<graph ref="graph" /> |
||||
|
||||
<analytic-status /> |
||||
|
||||
<template v-if="analyticStatus.available"> |
||||
<div> |
||||
Analytic unit type: |
||||
<select :value="analyticUnitType" @change="changeAnalyticUnitType"> |
||||
<option disabled value="">Please Select</option> |
||||
<option v-bind:key="option" v-for="option in analyticUnitTypes" :value="option">{{option}}</option> |
||||
</select> <br/><br/> |
||||
</div> |
||||
<div id="controls"> |
||||
<div v-if="analyticUnitType == analyticUnitTypes[0]"> |
||||
Threshold: |
||||
<input v-model="analyticUnitConfig.threshold" @change="thresholdChange" /> <br/><br/> |
||||
</div> |
||||
<div v-if="analyticUnitType == analyticUnitTypes[1]"> |
||||
Hold <pre>S</pre> to label patterns; |
||||
Hold <pre>A</pre> to label anti patterns <br/> |
||||
Hold <pre>D</pre> to delete patterns |
||||
<br/> |
||||
<hr/> |
||||
Correlation score: |
||||
<input v-model="analyticUnitConfig.correlation_score" @change="correlationScoreChange" /> <br/> |
||||
Anti correlation score: |
||||
<input v-model="analyticUnitConfig.anti_correlation_score" @change="antiCorrelationScoreChange" /> <br/> |
||||
Model score: |
||||
<input v-model="analyticUnitConfig.model_score" @change="modelScoreChange" /> <br/> |
||||
Threshold score: |
||||
<input v-model="analyticUnitConfig.threshold_score" @change="thresholdScoreChange" /> <br/><br/> |
||||
<button @click="clearAllLabeling"> clear all labeling </button> |
||||
</div> |
||||
<div v-if="analyticUnitType == analyticUnitTypes[2]"> |
||||
Hold <pre>Z</pre> to set seasonality timespan |
||||
<hr/> |
||||
<!-- Alpha: |
||||
<input :value="analyticUnitConfig.alpha" @change="alphaChange" /> <br/> --> |
||||
Confidence: |
||||
<input v-model="analyticUnitConfig.confidence" @change="confidenceChange" /> <br/> |
||||
Seasonality: |
||||
<input v-model="analyticUnitConfig.seasonality" @change="seasonalityChange" /> <br/> |
||||
Seasonality iterations: |
||||
<input v-model="analyticUnitConfig.seasonality_iterations" @change="seasonalityIterationsChange" /> <br/> |
||||
<br/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import Graph from '@/components/Graph.vue'; |
||||
import AnalyticStatus from '@/components/AnlyticsStatus.vue'; |
||||
import { AnalyticUnitType } from '@/types/analytic_units'; |
||||
|
||||
import { defineComponent } from 'vue'; |
||||
|
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
// TODO: move config editig to component |
||||
export default defineComponent({ |
||||
name: 'Home', |
||||
components: { |
||||
Graph, |
||||
AnalyticStatus |
||||
}, |
||||
methods: { |
||||
clearAllLabeling() { |
||||
this.$refs.graph.deleteAllSegments(); |
||||
}, |
||||
changeAnalyticUnitType(e) { |
||||
this.$store.dispatch('patchConfig', { [e.target.value]: null } ); |
||||
}, |
||||
|
||||
// Threshold |
||||
thresholdChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.threshold = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Threshold: cfg }); |
||||
}, |
||||
|
||||
// Pattern |
||||
correlationScoreChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.correlation_score = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Pattern: cfg }); |
||||
}, |
||||
antiCorrelationScoreChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.anti_correlation_score = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Pattern: cfg }); |
||||
}, |
||||
modelScoreChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.model_score = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Pattern: cfg }); |
||||
}, |
||||
thresholdScoreChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.threshold_score = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Pattern: cfg }); |
||||
}, |
||||
|
||||
// Anomaly |
||||
alphaChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.alpha = _.clamp(parseFloat(e.target.value), 0, 1); |
||||
this.$store.dispatch('patchConfig', { Anomaly: cfg }); |
||||
}, |
||||
confidenceChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.confidence = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Anomaly: cfg }); |
||||
}, |
||||
seasonalityChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.seasonality = parseFloat(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Anomaly: cfg }); |
||||
}, |
||||
seasonalityIterationsChange(e) { |
||||
let cfg = _.clone(this.analyticUnitConfig); |
||||
cfg.seasonality_iterations = Math.ceil(e.target.value); |
||||
this.$store.dispatch('patchConfig', { Anomaly: cfg }); |
||||
}, |
||||
}, |
||||
data: function () { |
||||
return { |
||||
analyticUnitTypes: [ |
||||
AnalyticUnitType.THRESHOLD, |
||||
AnalyticUnitType.PATTERN, |
||||
AnalyticUnitType.ANOMALY, |
||||
] |
||||
} |
||||
}, |
||||
computed: { |
||||
analyticUnitType() { |
||||
return this.$store.state.analyticUnitType; |
||||
}, |
||||
analyticUnitConfig() { |
||||
return this.$store.state.analyticUnitConfig; |
||||
}, |
||||
analyticStatus() { |
||||
return this.$store.state.analyticStatus; |
||||
} |
||||
} |
||||
}); |
||||
</script> |
||||
|
||||
<style scoped> |
||||
pre { |
||||
display: inline; |
||||
} |
||||
|
||||
#controls { |
||||
width: 50%; |
||||
margin: auto; |
||||
} |
||||
</style> |
@ -0,0 +1,34 @@
|
||||
<template> |
||||
<div class="hello"> |
||||
<div> |
||||
Login: <input type="text" v-model="this.username" /> <br/> <br/> |
||||
Password: <input type="password" v-model="this.password" /> <br/> <br/> |
||||
<input type="submit" @click="this.submit" value="login" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import AuthService from '@/services/auth.service'; |
||||
|
||||
import { Options, Vue } from 'vue-class-component'; |
||||
|
||||
@Options({ |
||||
|
||||
}) |
||||
|
||||
export default class Login extends Vue { |
||||
msg!: string |
||||
|
||||
username: string; |
||||
password: string; |
||||
|
||||
submit() { |
||||
AuthService.login({ username: this.username, password: this.password }) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
|
||||
</style> |
@ -0,0 +1,27 @@
|
||||
<template> |
||||
<div class="home"> |
||||
<img alt="Vue logo" src="../assets/logo.png"> |
||||
<scatter-plot /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
||||
import ScatterPlot from '@/components/ScatterPlot.vue'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'MOdel', |
||||
components: { |
||||
ScatterPlot |
||||
}, |
||||
methods: { |
||||
|
||||
} |
||||
}); |
||||
</script> |
||||
|
||||
<style scoped> |
||||
pre { |
||||
display: inline; |
||||
} |
||||
</style> |
@ -0,0 +1,12 @@
|
||||
import { shallowMount } from '@vue/test-utils' |
||||
import Graph from '@/components/Graph.vue' |
||||
|
||||
describe('HelloWorld.vue', () => { |
||||
it('renders props.msg when passed', () => { |
||||
const msg = 'new message' |
||||
const wrapper = shallowMount(Graph, { |
||||
props: { msg } |
||||
}) |
||||
expect(wrapper.text()).toMatch(msg) |
||||
}) |
||||
}) |
@ -0,0 +1,41 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"target": "es5", |
||||
"module": "esnext", |
||||
"strict": false, |
||||
"jsx": "preserve", |
||||
"importHelpers": true, |
||||
"moduleResolution": "node", |
||||
"experimentalDecorators": true, |
||||
"skipLibCheck": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"sourceMap": true, |
||||
"baseUrl": ".", |
||||
"types": [ |
||||
"webpack-env", |
||||
"jest" |
||||
], |
||||
"paths": { |
||||
"@/*": [ |
||||
"src/*" |
||||
] |
||||
}, |
||||
"lib": [ |
||||
"esnext", |
||||
"dom", |
||||
"dom.iterable", |
||||
"scripthost" |
||||
] |
||||
}, |
||||
"include": [ |
||||
"src/**/*.ts", |
||||
"src/**/*.tsx", |
||||
"src/**/*.vue", |
||||
"tests/**/*.ts", |
||||
"tests/**/*.tsx" |
||||
], |
||||
"exclude": [ |
||||
"node_modules" |
||||
] |
||||
} |
Loading…
Reference in new issue