rozetko
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 |
# 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 |
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