Browse Source

Merge pull request 'copy client code from the new hastic' (#1) from copy-code-from-the-new-hastic into master

Reviewed-on: #1
master
rozetko 2 years ago
parent
commit
1ec8d1ef1f
  1. 3
      .browserslistrc
  2. 1
      .env.development
  3. 1
      .env.production
  4. 29
      .eslintrc.js
  5. 23
      .gitignore
  6. 36
      README.md
  7. 6
      jest.config.js
  8. 19565
      package-lock.json
  9. 45
      package.json
  10. BIN
      public/favicon.ico
  11. 17
      public/index.html
  12. 27
      src/App.vue
  13. BIN
      src/assets/logo.png
  14. 23
      src/components/AnlyticsStatus.vue
  15. 245
      src/components/Graph.vue
  16. 102
      src/components/ScatterPlot.vue
  17. 166
      src/components/pods/anomaly_pod.ts
  18. 109
      src/components/pods/hastic_pod.ts
  19. 5
      src/components/pods/index.ts
  20. 187
      src/components/pods/pattern_pod.ts
  21. 44
      src/components/pods/threshold_pod.ts
  22. 5
      src/config.ts
  23. 12
      src/main.ts
  24. 37
      src/router/index.ts
  25. 86
      src/services/analytics.service.ts
  26. 37
      src/services/auth.service.ts
  27. 24
      src/services/metrics.service.ts
  28. 42
      src/services/segments.service.ts
  29. 6
      src/shims-vue.d.ts
  30. 62
      src/store/auth.module.ts
  31. 94
      src/store/index.ts
  32. 210
      src/types/analytic_units/index.ts
  33. 105
      src/types/colors.ts
  34. 20
      src/types/detection.ts
  35. 7
      src/types/index.ts
  36. 67
      src/types/segment.ts
  37. 101
      src/types/segment_array.ts
  38. 14
      src/types/segment_set.ts
  39. 5
      src/types/user.ts
  40. 15
      src/utils.ts
  41. 5
      src/views/About.vue
  42. 163
      src/views/Home.vue
  43. 34
      src/views/Login.vue
  44. 27
      src/views/Model.vue
  45. 12
      tests/unit/example.spec.ts
  46. 41
      tsconfig.json
  47. 9964
      yarn.lock

3
.browserslistrc

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

1
.env.development

@ -0,0 +1 @@
VUE_APP_API_URL="http://localhost:4347/api/"

1
.env.production

@ -0,0 +1 @@
VUE_APP_API_URL="/api/"

29
.eslintrc.js

@ -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
}
}
]
}

23
.gitignore vendored

@ -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?

36
README.md

@ -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
based on new version of hastic client from https://code.corpglory.net/hastic/hastic
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/).

6
jest.config.js

@ -0,0 +1,6 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}

19565
package-lock.json generated

File diff suppressed because it is too large Load Diff

45
package.json

@ -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"
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

17
public/index.html

@ -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>

27
src/App.vue

@ -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>

BIN
src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

23
src/components/AnlyticsStatus.vue

@ -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>

245
src/components/Graph.vue

@ -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>

102
src/components/ScatterPlot.vue

@ -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>

166
src/components/pods/anomaly_pod.ts

@ -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)
}
}

109
src/components/pods/hastic_pod.ts

@ -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();
}

5
src/components/pods/index.ts

@ -0,0 +1,5 @@
import { HasticPod } from './hastic_pod';
import { PatternPod } from './pattern_pod';
import { AnalyticUnitType } from '@/types/analytic_units';

187
src/components/pods/pattern_pod.ts

@ -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();
}
}

44
src/components/pods/threshold_pod.ts

@ -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 */ })
}
}

5
src/config.ts

@ -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!");
}

12
src/main.ts

@ -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')

37
src/router/index.ts

@ -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

86
src/services/analytics.service.ts

@ -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;
}

37
src/services/auth.service.ts

@ -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();

24
src/services/metrics.service.ts

@ -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];
}

42
src/services/segments.service.ts

@ -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'];
}

6
src/shims-vue.d.ts vendored

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

62
src/store/auth.module.ts

@ -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;
}
}
};

94
src/store/index.ts

@ -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;

210
src/types/analytic_units/index.ts

@ -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;
}
}

105
src/types/colors.ts

@ -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;

20
src/types/detection.ts

@ -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']
]);

7
src/types/index.ts

@ -0,0 +1,7 @@
export type TimeRange = { from: number, to: number };
export type AnomalyHSR = {
seasonality: number,
timestamp: number,
ts: [number, number, [number, number]][]
};

67
src/types/segment.ts

@ -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
);
}
}

101
src/types/segment_array.ts

@ -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);
}
}

14
src/types/segment_set.ts

@ -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;
}

5
src/types/user.ts

@ -0,0 +1,5 @@
export class User {
public username?: String
public email?: String
public password?: String
}

15
src/utils.ts

@ -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();
}
}

5
src/views/About.vue

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

163
src/views/Home.vue

@ -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>

34
src/views/Login.vue

@ -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>

27
src/views/Model.vue

@ -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>

12
tests/unit/example.spec.ts

@ -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)
})
})

41
tsconfig.json

@ -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"
]
}

9964
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save