diff --git a/package-lock.json b/package-lock.json index 991b426..2764d23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2009,6 +2009,14 @@ } } }, + "@types/chart.js": { + "version": "2.9.20", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.20.tgz", + "integrity": "sha512-Xv4dd+DYqtTdUWbDIwaDEFtUIFwoQN44wiDDrWXdJtfGtOFlFIxXrsu8D+XJCS9o7mZbW29X8vPptwVrduz4JA==", + "requires": { + "moment": "^2.10.2" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -3442,6 +3450,32 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz", + "integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, "chokidar": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", @@ -3717,7 +3751,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -3725,8 +3758,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.3", @@ -6219,9 +6251,9 @@ "integrity": "sha512-QBSEWNbEx1H0numXP1qgxKVCZHonRaky5ft4pGzQBcO4cy7mEja6TuJ8rc7BqX2pmkvetVQWKDH+DK/8y7GTag==" }, "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", "dev": true }, "events": { @@ -7529,9 +7561,9 @@ "dev": true }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { "eventemitter3": "^4.0.0", @@ -9091,6 +9123,11 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -9860,6 +9897,11 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "moment": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", + "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -9945,6 +9987,16 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "ng2-charts": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-2.3.2.tgz", + "integrity": "sha512-T0rPivwZITKtEtFRVodRCO+kIczWIP6V4YLZvf6Kg1jqc8jYGZ37H5ywT0Q7N0Rt5dJGhC5z1/38nWFBVFx5iw==", + "requires": { + "@types/chart.js": "^2.7.48", + "lodash-es": "^4.17.11", + "tslib": "^1.9.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index 24a2c69..e4f64fb 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,10 @@ "@nebular/eva-icons": "^5.0.0", "@nebular/theme": "^5.0.0", "bootstrap": "^4.5.0", + "chart.js": "^2.9.3", "eva-icons": "^1.1.3", "nebular-icons": "^1.1.0", + "ng2-charts": "^2.3.2", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/@dataflow/extra/name-validation.ts b/src/app/@dataflow/extra/name-validation.ts index 89e0c60..8367383 100644 --- a/src/app/@dataflow/extra/name-validation.ts +++ b/src/app/@dataflow/extra/name-validation.ts @@ -1,8 +1,8 @@ import { SupersetFlow, FlowInNode, CombErr } from '../core'; import { Observable, of } from 'rxjs'; -import { IUser, UsersFlowNode } from './users-flow'; +import { IUser, UsersFlowOutNode } from './users-flow'; -export interface NameValidationPreNode extends UsersFlowNode { +export interface NameValidationPreNode extends UsersFlowOutNode { currentName: string; } diff --git a/src/app/@dataflow/extra/users-flow.ts b/src/app/@dataflow/extra/users-flow.ts index 6ee0470..4ece2e2 100644 --- a/src/app/@dataflow/extra/users-flow.ts +++ b/src/app/@dataflow/extra/users-flow.ts @@ -11,17 +11,17 @@ export interface IUser extends IRcloneServer { name: string; } -export interface UsersFlowNode extends FlowInNode { +export interface UsersFlowOutNode extends FlowInNode { users: IUser[]; loginUser: IUser; } -export abstract class UsersFlow extends BareFlow { +export abstract class UsersFlow extends BareFlow { public static readonly defaultUser: IUser[] = [ { name: 'localhost', url: 'http://localhost:5572' }, ]; private static trigger$ = new Subject(); - protected request(pre: CombErr): Observable> { + protected request(pre: CombErr): Observable> { return of([{ users: UsersFlow.getAll(), loginUser: UsersFlow.getLogin() }, []]); } public static getAll(): IUser[] { diff --git a/src/app/@dataflow/rclone/core-stats-flow.ts b/src/app/@dataflow/rclone/core-stats-flow.ts new file mode 100644 index 0000000..94a99ce --- /dev/null +++ b/src/app/@dataflow/rclone/core-stats-flow.ts @@ -0,0 +1,33 @@ +import { PostFlow } from './post-flow'; +import { NoopAuthFlowSupNode } from './noop-auth-flow'; +import { FlowOutNode, CombErr } from '../core'; +import { AjaxFlowInteralNode } from '../core/ajax-flow'; +import { IRcloneServer } from '../extra'; + +export interface CoreStatsFlowOutNode extends FlowOutNode { + 'core-stats': { + bytes: number; + checks: number; + deletes: number; + elapsedTime: number; + errors: number; + fatalError: boolean; + retryError: boolean; + speed: number; + transfers: number; + }; +} + +export interface CoreStatsFlowSupNode extends CoreStatsFlowOutNode, NoopAuthFlowSupNode {} + +export abstract class CoreStatsFlow extends PostFlow { + // public prerequest$: Observable>; + protected cmd: string = 'core/stats'; + protected params: object = {}; + protected cacheSupport: boolean = false; + protected reconstructAjaxResult(x: AjaxFlowInteralNode): CombErr { + if (x[1].length !== 0) return [{}, x[1]] as any; + const rsp = x[0].ajaxRsp.response; + return [{ 'core-stats': rsp }, []]; + } +} diff --git a/src/app/@dataflow/rclone/index.ts b/src/app/@dataflow/rclone/index.ts index ea34f7d..06a1d98 100644 --- a/src/app/@dataflow/rclone/index.ts +++ b/src/app/@dataflow/rclone/index.ts @@ -1,2 +1,3 @@ export * from './post-flow'; export * from './noop-auth-flow'; +export * from './core-stats-flow'; diff --git a/src/app/pages/dashboard/dashboard.component.ts b/src/app/pages/dashboard/dashboard.component.ts index 61f288b..9c1fbbe 100644 --- a/src/app/pages/dashboard/dashboard.component.ts +++ b/src/app/pages/dashboard/dashboard.component.ts @@ -11,7 +11,7 @@ import { Component, OnInit } from '@angular/core';
- speed +
core/stats diff --git a/src/app/pages/dashboard/dashboard.module.ts b/src/app/pages/dashboard/dashboard.module.ts index cfed1bc..5f07530 100644 --- a/src/app/pages/dashboard/dashboard.module.ts +++ b/src/app/pages/dashboard/dashboard.module.ts @@ -3,10 +3,13 @@ import { CommonModule } from '@angular/common'; import { DashboardRoutingModule } from './dashboard-routing.module'; import { DashboardComponent } from './dashboard.component'; -import { NbCardModule } from '@nebular/theme'; +import { NbCardModule, NbButtonModule, NbIconModule } from '@nebular/theme'; +import { SpeedCardComponent } from './speed-card/speed-card.component'; +import { SpeedChartsComponent } from './speed-card/speed-charts.component'; +import { ChartsModule } from 'ng2-charts'; @NgModule({ - declarations: [DashboardComponent], - imports: [CommonModule, DashboardRoutingModule, NbCardModule], + declarations: [DashboardComponent, SpeedCardComponent, SpeedChartsComponent], + imports: [CommonModule, DashboardRoutingModule, NbCardModule, NbButtonModule, NbIconModule, ChartsModule], }) export class DashboardModule {} diff --git a/src/app/pages/dashboard/speed-card/speed-card.component.ts b/src/app/pages/dashboard/speed-card/speed-card.component.ts new file mode 100644 index 0000000..cbe7eb9 --- /dev/null +++ b/src/app/pages/dashboard/speed-card/speed-card.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'dashboard-speed-card', + template: ` + + + + + Speed Chart + + + + + + + + + + + Speed Limitation + + + + 123 + + + + + `, + styles: [], +}) +export class SpeedCardComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/src/app/pages/dashboard/speed-card/speed-charts.component.ts b/src/app/pages/dashboard/speed-card/speed-charts.component.ts new file mode 100644 index 0000000..e066dae --- /dev/null +++ b/src/app/pages/dashboard/speed-card/speed-charts.component.ts @@ -0,0 +1,155 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ChartDataSets, ChartOptions, ChartPoint } from 'chart.js'; +import { Color, BaseChartDirective, Label } from 'ng2-charts'; +import * as moment from 'moment'; +import { CoreStatsFlow, NoopAuthFlowSupNode } from 'src/app/@dataflow/rclone'; +import { Observable, interval } from 'rxjs'; +import { CombErr } from 'src/app/@dataflow/core'; +import { UsersService } from '../../users.service'; +import { map, withLatestFrom } from 'rxjs/operators'; +import { IRcloneServer } from 'src/app/@dataflow/extra'; + +@Component({ + selector: 'dashboard-speed-charts', + template: ` +
+ +
+ `, + styles: [], +}) +export class SpeedChartsComponent implements OnInit { + public lineChartData: ChartDataSets[] = [ + { + data: [], + // label: 'Series A', + }, + // { + // data: [ + // { x: moment().add(11, 'second'), y: 380 }, + // { x: moment().add(22, 'second'), y: 110 }, + // { x: moment().add(33, 'second'), y: 400 }, + // { x: moment().add(44, 'second'), y: 300 }, + // { x: moment().add(55, 'second'), y: 800 }, + // { x: moment().add(57, 'second'), y: 350 }, + // { x: moment().add(1, 'minute'), y: 320 }, + // ], + // label: 'Series B', + // }, + ]; + public lineChartOptions: ChartOptions = { + responsive: true, + animation: { + duration: 0, + }, + legend: { + display: false, + }, + scales: { + // We use this empty structure as a placeholder for dynamic theming. + xAxes: [ + { + type: 'time', + distribution: 'linear', + time: { + minUnit: 'second', + displayFormats: { + second: 'ss', + }, + }, + gridLines: { + display: false, + }, + }, + ], + yAxes: [ + { + position: 'right', + gridLines: { + color: 'rgb(0,0,0,0.3)', + }, + ticks: { + fontColor: 'back', + min: 0, + }, + }, + ], + unit: 'minute', + }, + }; + public lineChartColors: Color[] = [ + { + // red + backgroundColor: 'rgba(255,0,0,0.3)', + borderColor: 'red', + pointBackgroundColor: 'rgba(148,159,177,1)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgba(148,159,177,0.8)', + }, + { + // grey + backgroundColor: 'rgba(148,159,177,0.2)', + borderColor: 'rgba(148,159,177,1)', + pointBackgroundColor: 'rgba(148,159,177,1)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgba(148,159,177,0.8)', + }, + ]; + public lineChartLegend = true; + public lineChartType = 'line'; + public lineChartPlugins = []; + + @ViewChild(BaseChartDirective, { static: true }) chart: BaseChartDirective; + + constructor(private userService: UsersService) { + this.lineChartData[0].data = [...Array(Math.round(this.threadhold / this.period)).keys()].map( + (i) => { + return { x: moment().subtract(this.threadhold - i * this.period, 'seconds'), y: 0 }; + } + ); + } + + coreStatsFlow$: CoreStatsFlow; + threadhold = 60; + period = 3; + + ngOnInit() { + const outer = this; + this.coreStatsFlow$ = new (class extends CoreStatsFlow { + public prerequest$ = interval(outer.period * 1000).pipe( + withLatestFrom(outer.userService.usersFlow$.getOutput()), + map( + ([_, x]): CombErr => { + if (x[1].length !== 0) return [{}, x[1]] as any; + return [x[0].loginUser, []]; + } + ) + ); + })(); + this.coreStatsFlow$.deploy(); + this.coreStatsFlow$.getOutput().subscribe((node) => { + if (node[1].length !== 0) return; + const speed = node[0]['core-stats'].speed; + const data = this.lineChartData[0].data as ChartPoint[]; + const speedNode = { x: moment(), y: speed }; + const threadhold = speedNode.x.clone().subtract(this.threadhold, 'seconds'); + while (data.length !== 0 && data[0].x < threadhold) { + data.shift(); + } + data.push(speedNode); + this.chart.update(); + }); + } +} diff --git a/src/app/pages/user/config/config.component.ts b/src/app/pages/user/config/config.component.ts index 2d27028..6677d97 100644 --- a/src/app/pages/user/config/config.component.ts +++ b/src/app/pages/user/config/config.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { Observable, Subject, of } from 'rxjs'; import { - UsersFlowNode, + UsersFlowOutNode, NameValidation, NameValidationPreNode, IRcloneServer, @@ -139,7 +139,7 @@ export class ConfigComponent implements OnInit { connectTrigger$ = new Subject(); - users$: Observable>; + users$: Observable>; @Input() editUser: Observable> = of([{ prevName: '' }, []]); @Output() diff --git a/src/app/pages/user/confirm/confirm.component.ts b/src/app/pages/user/confirm/confirm.component.ts index bd5b673..250ebe7 100644 --- a/src/app/pages/user/confirm/confirm.component.ts +++ b/src/app/pages/user/confirm/confirm.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Observable } from 'rxjs'; import { CombErr } from 'src/app/@dataflow/core'; -import { UsersFlowNode, IUser } from 'src/app/@dataflow/extra'; +import { UsersFlowOutNode, IUser } from 'src/app/@dataflow/extra'; import { map, filter, withLatestFrom, startWith, tap } from 'rxjs/operators'; import { UsersService } from '../../users.service'; @@ -37,7 +37,7 @@ export class ConfirmComponent implements OnInit { @Output() onDelete: EventEmitter = new EventEmitter(); selectedUser$: Observable; - users$: Observable>; + users$: Observable>; constructor(private usersService: UsersService) {} diff --git a/src/app/pages/user/select/select.component.ts b/src/app/pages/user/select/select.component.ts index 0c3f1ac..a5e6028 100644 --- a/src/app/pages/user/select/select.component.ts +++ b/src/app/pages/user/select/select.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Observable } from 'rxjs'; -import { UsersFlowNode } from 'src/app/@dataflow/extra'; +import { UsersFlowOutNode } from 'src/app/@dataflow/extra'; import { map, startWith } from 'rxjs/operators'; import { CombErr } from 'src/app/@dataflow/core'; import { FormControl } from '@angular/forms'; @@ -42,7 +42,7 @@ export class SelectComponent implements OnInit { constructor(private usersService: UsersService) {} - users$: Observable>; + users$: Observable>; @Output() onConfirm: EventEmitter = new EventEmitter();