瀏覽代碼

Merge branch 'data-download' of luccerny/senslog-dashboard into master

luccerny 4 年之前
父節點
當前提交
b9aea7bda4

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+################################################################################
+# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
+################################################################################
+
+/.vs
+node_modules
+/dist

文件差異過大導致無法顯示
+ 19589 - 1
package-lock.json


+ 2 - 1
package.json

@@ -4,7 +4,8 @@
   "scripts": {
     "ng": "ng",
     "start": "ng serve",
-    "build": "ng build",
+    "build": "ng build --configuration production",
+    "build-stage": "ng build --configuration production --base-href /senslog-staging",
     "test": "ng test",
     "lint": "ng lint",
     "e2e": "ng e2e"

+ 6 - 4
proxy-config.json

@@ -1,10 +1,12 @@
 {
   "/analytics": {
-    "target": "http://51.15.45.95:9090",
-    "secure": false
+    "target": "https://sensor.lesprojekt.cz",
+    "secure": true,
+    "changeOrigin": true
   },
   "/senslog15": {
-    "target": "http://51.15.45.95:8080",
-    "secure": false
+    "target": "https://sensor.lesprojekt.cz",
+    "secure": true,
+    "changeOrigin": true
   }
 }

+ 3 - 1
src/app/app.module.ts

@@ -1,5 +1,6 @@
 import {NgModule} from '@angular/core';
-import {BrowserModule} from '@angular/platform-browser';
+import { HashLocationStrategy, LocationStrategy } from '@angular/common';
+import { BrowserModule } from '@angular/platform-browser';
 
 import {AppRoutingModule} from './app-routing.module';
 import {AppComponent} from './app.component';
@@ -36,6 +37,7 @@ import {ToastModule} from 'primeng/toast';
   providers: [
     ConfirmationService,
     MessageService,
+    { provide: LocationStrategy, useClass: HashLocationStrategy }
   ],
   bootstrap: [AppComponent]
 })

+ 4 - 0
src/app/auth/interceptors/auth.interceptor.ts

@@ -38,6 +38,10 @@ export class AuthInterceptor implements HttpInterceptor {
     }
 
     console.log('Sending request!', request.url);
+    request = request.clone({
+      withCredentials: true
+    });
+
     return next.handle(request)
       .pipe(
         catchError(err => {

+ 11 - 1
src/app/auth/states/user.state.ts

@@ -3,6 +3,8 @@ import {BehaviorSubject, Observable} from 'rxjs';
 import {User} from '../models/user';
 import {LoginService} from '../../shared/api/endpoints/services/login.service';
 import {ToastService} from '../../shared/services/toast.service';
+//import {AuthService} from '../services/auth.service';
+import {CookieService} from 'ngx-cookie-service';
 
 @Injectable({
   providedIn: 'root'
@@ -12,6 +14,8 @@ export class UserState {
 
   constructor(
     private loginService: LoginService,
+    //private authService: AuthService,
+    private cookieService: CookieService,
     private toastService: ToastService
   ) {}
 
@@ -35,7 +39,13 @@ export class UserState {
    if (this.userState$.getValue()){
       this.loginService.getUserInfo$Response().subscribe(res => {
         this.userState$.next({...this.userState$.getValue(), userInfo: res.body});
-      }, err => this.toastService.showError(err.error.message));
+      }, err => {
+        this.toastService.showError(err.error.message);
+        console.log('Authentication failed!');
+        this.setUser(null);
+        this.cookieService.deleteAll();
+      //this.authService.doLogout();
+      });
    }
     return this.userState$.asObservable();
   }

+ 1 - 1
src/app/dashboard/components/dashboard.component.ts

@@ -91,7 +91,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
   getUnits() {
     this.dataService.getData().subscribe(data => {
       this.units = data;
-      this.units.forEach(unit => unit.sensors.sort((a, b)  => a.sensorId - b.sensorId));
+      this.units.forEach(unit => unit.sensors.sort((a, b) => a.sensorId - b.sensorId));
     }, err => this.toastService.showError(err.error.message));
   }
 

+ 2 - 1
src/app/shared/api/endpoints/base-service.ts

@@ -3,6 +3,7 @@
 import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { ApiConfiguration } from './api-configuration';
+import { environment } from '../../../../environments/environment';
 
 /**
  * Base class for services
@@ -15,7 +16,7 @@ export class BaseService {
   ) {
   }
 
-  private _rootUrl: string = '';
+  private _rootUrl: string = environment.sensLogBaseUrl;
 
   /**
    * Returns the root url for all operations in this service. If not set directly in this

+ 2 - 1
src/app/shared/api/endpoints/request-builder.ts

@@ -346,7 +346,8 @@ export class RequestBuilder {
       params: httpParams,
       headers: httpHeaders,
       responseType: options.responseType,
-      reportProgress: options.reportProgress
+      reportProgress: options.reportProgress,
+      //withCredentials: true
     });
   }
 }

+ 6 - 2
src/app/shared/api/endpoints/services/login.service.ts

@@ -6,8 +6,8 @@ import { BaseService } from '../base-service';
 import { ApiConfiguration } from '../api-configuration';
 import { StrictHttpResponse } from '../strict-http-response';
 import { RequestBuilder } from '../request-builder';
-import { Observable } from 'rxjs';
-import { map, filter } from 'rxjs/operators';
+import { Observable, throwError } from 'rxjs';
+import { map, filter, catchError } from 'rxjs/operators';
 
 import { UserCookie } from '../models/user-cookie';
 import { UserInfo } from '../models/user-info';
@@ -126,6 +126,10 @@ export class LoginService extends BaseService {
       responseType: 'json',
       accept: 'application/json'
     })).pipe(
+      catchError((err) => {
+        console.error(err);
+        return throwError(err);
+      }),
       filter((r: any) => r instanceof HttpResponse),
       map((r: HttpResponse<any>) => {
         return r as StrictHttpResponse<UserInfo>;

+ 28 - 0
src/app/shared/api/endpoints/services/observation.service.ts

@@ -1,5 +1,6 @@
 /* tslint:disable */
 /* eslint-disable */
+import { formatDate } from '@angular/common';
 import { Injectable } from '@angular/core';
 import { HttpClient, HttpResponse } from '@angular/common/http';
 import { BaseService } from '../base-service';
@@ -30,6 +31,7 @@ export class ObservationService extends BaseService {
    * Path part for operation getObservation
    */
   static readonly GetObservationPath = '/senslog15/SensorService?Operation=GetObservations';
+  static readonly RestObservationsPath = '/senslog15/rest/observation/';
 
   /**
    * Get observation.
@@ -89,4 +91,30 @@ export class ObservationService extends BaseService {
     );
   }
 
+
+  exportObservations(params: {
+    //group_id: number,
+    sensor_id: number,
+    from?: Date;
+    to?: Date;
+  }): Observable<StrictHttpResponse<string>> {
+    const rb = new RequestBuilder(this.rootUrl, ObservationService.RestObservationsPath + 'export', 'get');
+    if (params) {
+      rb.query('sensor_id', params.sensor_id);
+      rb.query('from_time', formatDate(params.from, 'yyyy-MM-dd', 'en-US'));
+      rb.query('to_time', formatDate(params.to, 'yyyy-MM-dd', 'en-US'));
+      rb.query('style', 'crosstab');
+      rb.query('nullable', false);
+    }
+
+    return this.http.request(rb.build({
+      responseType: 'text',
+      accept: 'text/plain'
+    })).pipe(
+      filter((r: any) => r instanceof HttpResponse),
+      map((r: HttpResponse<any>) => {
+        return r as StrictHttpResponse<string>;
+      })
+    );
+  }
 }

+ 42 - 0
src/app/shared/nav-bar/components/data-download/data-download-popup.component.html

@@ -0,0 +1,42 @@
+<p-dialog [visible]="isVisible" [modal]="true" [closable]="false" [draggable]="false" header="Data download"
+          [baseZIndex]="10000" [className]="'popup-form'">
+
+  <form [formGroup]="downloadForm">
+    <div class="input-group form-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text"><i class="fas fa-file-signature"></i></span>
+        <select formControlName="sensor_id" id="sensor_id">
+          <option value="null" disabled>Select sensor</option>
+          <option *ngFor="let s of sensors; let i = index" [value]="sensors[i].sensorId">
+            {{s.sensorName + '   (' + s.sensorId + ')'}}
+          </option>
+        </select>
+      </div>
+    </div>
+    <div class="input-group form-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text"><i class="far fa-calendar-alt"></i>From</span>
+        <p-calendar [monthNavigator]="true" [yearNavigator]="true" yearRange="2000:2021" dateFormat="d. m. yy" inputId="navigators" formControlName="from"></p-calendar>
+      </div>
+    </div>
+    <div class="input-group form-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text"><i class="far fa-calendar-alt"></i>To</span>
+        <p-calendar [monthNavigator]="true" [yearNavigator]="true" yearRange="2000:2021" dateFormat="d. m. yy" inputId="navigators" formControlName="to"></p-calendar>
+      </div>
+    </div>
+    <p-listbox [options]="units" formControlName="selectedUnits" [metaKeySelection]="false" [checkbox]="true" [filter]="true" filterPlaceHolder="Search and select units"
+               optionLabel="description" optionValue="unitId"
+               emptyFilterMessage="No units for specified filter" [multiple]="true" [listStyle]="{'max-height':'250px'}" [style]="{'width':'100%'}">
+    </p-listbox>
+  </form>
+  <div *ngIf="inProgress" class="download-progress">Export in progress<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar></div>
+  <p-footer>
+    <div class="row">
+      <div class="popup-buttons">
+        <button pButton type="button" label="Close" class="p-button-primary dark" icon="pi pi-times" (click)="close()"></button>
+        <button pButton type="submit" label="Download" class="p-button-primary dark" icon="pi pi-download" (click)="processDownload()" [disabled]="inProgress"></button>
+      </div>
+    </div>
+  </p-footer>
+</p-dialog>

+ 22 - 0
src/app/shared/nav-bar/components/data-download/data-download-popup.component.scss

@@ -0,0 +1,22 @@
+
+::ng-deep .p-dialog-content {
+  min-height: 500px;
+  height: 600px;
+  max-height: 95%;
+}
+
+::ng-deep .p-datepicker-calendar tbody span {
+  background-color: transparent !important;
+}
+
+::ng-deep .input-group-text i {
+  margin-right: 0.5rem;
+}
+
+.download-progress {
+  position: absolute;
+  bottom: 100px;
+  width: 90%;
+  margin-left: 2.5%;
+  text-align: center;
+}

+ 124 - 0
src/app/shared/nav-bar/components/data-download/data-download-popup.component.ts

@@ -0,0 +1,124 @@
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import {HttpResponse} from '@angular/common/http';
+import {map} from 'rxjs/operators';
+import {ObservationService} from '../../../api/endpoints/services/observation.service';
+import { ToastService } from '../../../services/toast.service';
+import { SensorsService } from '../../../api/endpoints/services/sensors.service';
+import { DataService } from '../../../api/endpoints/services/data.service';
+import { Sensor } from '../../../api/endpoints/models/sensor';
+import { Unit } from '../../../api/endpoints/models/unit';
+import * as moment from 'moment-timezone';
+import { DashboardComponent } from '../../../../dashboard/components/dashboard.component';
+import { formatDate } from '@angular/common';
+
+@Component({
+  selector: 'app-data-download-popup',
+  templateUrl: './data-download-popup.component.html',
+  styleUrls: ['./data-download-popup.component.scss']
+})
+export class DataDownloadPopupComponent implements OnInit {
+
+  downloadForm: FormGroup;
+  items: FormArray;
+  dateFrom: Date = moment().hour(0).minutes(0).subtract(1, 'days').toDate();
+  dateTo: Date = moment().toDate();
+  selectedUnits: Unit[];
+
+  inProgress: Boolean = false;
+  @Input() isVisible;
+  @Output() isVisibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();
+  @Input() sensors: Sensor[];
+  @Input() units: Unit[];
+
+  constructor(
+    private formBuilder: FormBuilder,
+    private observationService: ObservationService,
+    private dataService: DataService,
+    private sensorsService: SensorsService,
+    private toastService: ToastService
+  ) {
+    this.initForm();
+  }
+
+  ngOnInit(): void {
+  }
+
+  close() {
+    this.isVisibleChange.emit(false);
+  }
+
+  initForm() {
+    this.downloadForm = this.formBuilder.group({
+      from: [this.dateFrom, Validators.required],
+      to: [this.dateTo, Validators.required],
+      sensor_id: [null, Validators.required],
+      selectedUnits: new FormControl([])
+    });
+
+    this.dataService.getData().subscribe(data => {
+      if (data && data.length > 0) {
+        this.units = Array.from(data, d =>  d.unit );
+        let firstUnitId: number = data[0].unit.unitId;
+
+        this.sensorsService.getUnitSensors({ unit_id: firstUnitId }).subscribe(sens => {
+          if (sens) {
+            this.sensors = sens;
+            this.downloadForm.patchValue({
+              selectedUnits: Array.from(this.units, u => u.unitId)
+            });
+          }
+        });
+      }
+    }, err => this.toastService.showError(err.error.message));
+  }
+
+  /**
+   * Insert unit with sensor and position if form valid
+   */
+  processDownload() {
+    if (this.downloadForm.valid) {
+
+      this.inProgress = true;
+      this.observationService.exportObservations(this.downloadForm.value).pipe(
+        map((response: HttpResponse<any>) => {
+          this.inProgress = false;
+          if (response.status === 200) {
+            console.log('Export successful');
+            this.saveAsFile(
+              response.body,
+              this.downloadForm.value.sensor_id + '_' +
+              formatDate(this.downloadForm.value.from, 'dd-MM-yyyy', 'en-US') + '_' +
+              formatDate(this.downloadForm.value.to, 'dd-MM-yyyy', 'en-US') +
+              '.csv');
+          } else {
+            this.toastService.showError('Data download caused error!');
+          }
+        })
+      ).toPromise().then().catch(err => {
+        this.inProgress = false;
+        this.toastService.showError(err.error.message)
+      });
+    }
+  }
+
+  saveAsFile(data, filename) {
+    let file = new Blob([data], { type: 'text/plain' });
+    let evt = document.createEvent('MouseEvents');
+    let link = document.createElement('a');
+
+    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
+      window.navigator.msSaveOrOpenBlob(file, filename);
+    }
+    else {
+      var e = document.createEvent('MouseEvents'),
+        a = document.createElement('a');
+
+      a.download = filename;
+      a.href = window.URL.createObjectURL(file);
+      a.dataset.downloadurl = ['text/plain', a.download, a.href].join(':');
+      e.initEvent('click', true, false);
+      a.dispatchEvent(e);
+    }
+  }
+}

+ 6 - 2
src/app/shared/nav-bar/components/nav-bar.component.html

@@ -12,15 +12,18 @@
       </a>
       <div class="collapse navbar-collapse" id="navbarNav">
         <ul class="navbar-nav left">
-          <li class="nav-item">
+          <!--<li class="nav-item">
             <a class="nav-link" [routerLink]="['/dashboard']"><h2>Dashboard</h2></a>
-          </li>
+          </li>-->
           <li *ngIf="loggedUser?.userInfo?.rightsId == 0" class="nav-item">
             <a class="nav-link" id="addUser" (click)="addUser()"><h2><i class="fas fa-user-plus"></i>&nbsp;Add user</h2></a>
           </li>
           <li *ngIf="loggedUser?.userInfo?.rightsId == 0 || loggedUser?.userInfo?.rightsId == 1" class="nav-item">
             <a class="nav-link" id="addUnit" (click)="insertUnitPopup()"><h2><i class="fas fa-folder-plus"></i>&nbsp;Add unit</h2></a>
           </li>
+          <li *ngIf="loggedUser?.userInfo?.rightsId == 0 || loggedUser?.userInfo?.rightsId == 1" class="nav-item">
+            <a class="nav-link" id="downloadData" (click)="downloadData()"><h2><i class="fas fa-download"></i>&nbsp;Data download</h2></a>
+          </li>
         </ul>
         <a class="navbar-brand desktop" href="/">
           <img src="/assets/images/senslog-logo.svg" alt="Logo SensLog">
@@ -40,3 +43,4 @@
 
 <app-user-insert-popup *ngIf="showAddUserPopup" [(isVisible)]="showAddUserPopup"></app-user-insert-popup>
 <app-unit-insert-popup [(isVisible)]="showInsertUnitPopup" [phenomenons]="phenomenons" (emitNewUnit)="addUnit($event)" [sensorTypes]="sensorTypes"></app-unit-insert-popup>
+<app-data-download-popup [(isVisible)]="showDataDownloadPopup"></app-data-download-popup>

+ 5 - 0
src/app/shared/nav-bar/components/nav-bar.component.scss

@@ -13,3 +13,8 @@
   border-left: 1px solid #F2F2F2;
   border-right: 1px solid #FFF;
 }
+
+::ng-deep .input-group-prepend .input-group-text {
+  width: 77px;
+  min-height: 38px;
+}

+ 12 - 1
src/app/shared/nav-bar/components/nav-bar.component.ts

@@ -6,7 +6,7 @@ import {Right} from '../../api/endpoints/models/right';
 import {Phenomenon} from '../../api/endpoints/models/phenomenon';
 import {SensorsService} from '../../api/endpoints/services/sensors.service';
 import {InsertUnit} from '../../api/endpoints/models/insert-unit';
-import {InsertSensor} from '../../api/endpoints/models/insert-sensor';
+import { InsertSensor } from '../../api/endpoints/models/insert-sensor';
 
 @Component({
   selector: 'app-nav-bar',
@@ -20,6 +20,7 @@ export class NavBarComponent implements OnInit, OnDestroy {
   showAddUserPopup = false;
   rights: Right[];
   showInsertUnitPopup = false;
+  showDataDownloadPopup = false;
   phenomenons: Phenomenon[];
   @Output() emitNewUnit: EventEmitter<{unit: InsertUnit, sensors: InsertSensor[]}> =
     new EventEmitter<{unit: InsertUnit, sensors: InsertSensor[]}>();
@@ -56,6 +57,16 @@ export class NavBarComponent implements OnInit, OnDestroy {
     this.showInsertUnitPopup = true;
   }
 
+  /**
+   * Show data download popup
+   */
+  downloadData() {
+    this.sensorService.getPhenomenons().subscribe(
+      response => this.phenomenons = response
+    );
+    this.showDataDownloadPopup = true;
+  }
+
   logOut(): void {
     this.authService.doLogout();
   }

+ 13 - 5
src/app/shared/nav-bar/nav-bar.module.ts

@@ -1,25 +1,33 @@
 import {NgModule} from '@angular/core';
-import {CommonModule} from '@angular/common';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
 import {NavBarComponent} from './components/nav-bar.component';
 import {RouterModule} from '@angular/router';
 import {ButtonModule} from 'primeng/button';
 import { UserInsertPopupComponent } from './components/user-insert-popup/user-insert-popup.component';
 import {DialogModule} from 'primeng/dialog';
 import {ReactiveFormsModule} from '@angular/forms';
-import {UnitInsertPopupComponent} from './components/unit-insert-popup/unit-insert-popup.component';
-
+import { UnitInsertPopupComponent } from './components/unit-insert-popup/unit-insert-popup.component';
+import { DataDownloadPopupComponent } from './components/data-download/data-download-popup.component';
+import { CalendarModule } from 'primeng/calendar';
+import { ProgressBarModule } from 'primeng/progressbar';
+import { ListboxModule } from 'primeng/listbox';
 
 @NgModule({
-  declarations: [NavBarComponent, UserInsertPopupComponent, UnitInsertPopupComponent],
+  declarations: [NavBarComponent, UserInsertPopupComponent, UnitInsertPopupComponent, DataDownloadPopupComponent],
   exports: [
     NavBarComponent
   ],
   imports: [
     CommonModule,
+    FormsModule,
     RouterModule,
     ButtonModule,
     DialogModule,
-    ReactiveFormsModule
+    ReactiveFormsModule,
+    CalendarModule,
+    ListboxModule,
+    ProgressBarModule
   ]
 })
 export class NavBarModule {

+ 2 - 1
src/environments/environment.prod.ts

@@ -1,5 +1,6 @@
 export const environment = {
   production: true,
   baseHref: '/',
-  useMock: false
+  useMock: false,
+  sensLogBaseUrl: 'https://sensor.lesprojekt.cz'
 };

+ 2 - 14
src/environments/environment.ts

@@ -1,16 +1,4 @@
-// This file can be replaced during build by using the `fileReplacements` array.
-// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
-// The list of file replacements can be found in `angular.json`.
-
 export const environment = {
-  production: false
+  production: false,
+  sensLogBaseUrl: ''
 };
-
-/*
- * For easier debugging in development mode, you can import the following file
- * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
- *
- * This import should be commented out in production mode because it will have a negative impact
- * on performance if an error is thrown.
- */
-// import 'zone.js/dist/zone-error';  // Included with Angular CLI.

部分文件因文件數量過多而無法顯示