diff --git a/angular.json b/angular.json index 066b70a..126fb2c 100644 --- a/angular.json +++ b/angular.json @@ -26,8 +26,7 @@ "assets": [ "src/favicon.ico", "src/assets", - { "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }, - "src/Staticfile" + { "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" } ], "styles": [ "src/styles.scss" diff --git a/src/Staticfile b/cf_config/Staticfile similarity index 55% rename from src/Staticfile rename to cf_config/Staticfile index 1b9f882..b6f086b 100644 --- a/src/Staticfile +++ b/cf_config/Staticfile @@ -1,4 +1,4 @@ pushstate: enabled force_https: true root: UI -location_include: custom-header.conf +location_include: ../../headers.conf diff --git a/cf_config/headers.conf b/cf_config/headers.conf new file mode 100644 index 0000000..a9ddb9f --- /dev/null +++ b/cf_config/headers.conf @@ -0,0 +1 @@ +add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src https://definma-api.apps.de1.bosch-iot-cloud.com; form-action 'none'; frame-ancestors 'none'; base-uri 'self'"; diff --git a/manifest.yml b/manifest.yml index 19a5de6..5aa697c 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,9 +1,9 @@ --- applications: - name: definma - path: dist/UI + path: dist buildpacks: - staticfile_buildpack - memory: 128M + memory: 64M instances: 1 stack: cflinuxfs3 diff --git a/package-lock.json b/package-lock.json index 7d7b150..cde5a07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2316,6 +2316,12 @@ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, "adm-zip": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", @@ -2892,6 +2898,18 @@ "callsite": "1.0.0" } }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3393,6 +3411,12 @@ "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz", "integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g==" }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, "chokidar": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", @@ -4782,6 +4806,12 @@ "is-obj": "^2.0.0" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -4810,6 +4840,12 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, "electron-to-chromium": { "version": "1.3.446", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", @@ -5483,6 +5519,12 @@ "minimatch": "^3.0.3" } }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5879,6 +5921,16 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -6063,6 +6115,12 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, "hosted-git-info": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz", @@ -9242,6 +9300,12 @@ "is-wsl": "^2.1.1" } }, + "opener": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "dev": true + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -12914,6 +12978,12 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, "ts-node": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", @@ -13726,6 +13796,35 @@ } } }, + "webpack-bundle-analyzer": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz", + "integrity": "sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "dependencies": { + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "dev": true + } + } + }, "webpack-dev-middleware": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", diff --git a/package.json b/package.json index 9941ad5..96c6204 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "ng": "ng", "start": "ng serve", "build": "ng build --prod --aot", - "build-push": "ng build --prod --aot && cf push", + "build-push": "ng build --prod --aot && copy /Y cf_config\\ dist && cf push", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "coverage": "ng test --no-watch --code-coverage", - "api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js" + "api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js", + "bundle-report": "ng build --prod --aot --stats-json && webpack-bundle-analyzer dist/UI/stats-es2015.json" }, "private": true, "dependencies": { @@ -54,6 +55,7 @@ "protractor": "~5.4.0", "ts-node": "~7.0.0", "tslint": "~5.15.0", - "typescript": "~3.8.3" + "typescript": "~3.8.3", + "webpack-bundle-analyzer": "^3.8.0" } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d3071c5..25fcf56 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,6 +5,7 @@ import {LoginService} from './services/login.service'; import {SampleComponent} from './sample/sample.component'; import {SamplesComponent} from './samples/samples.component'; import {DocumentationComponent} from './documentation/documentation.component'; +import {TemplatesComponent} from './templates/templates.component'; const routes: Routes = [ @@ -13,6 +14,8 @@ const routes: Routes = [ {path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, + {path: 'templates', component: TemplatesComponent}, // TODO: change after development + // {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]}, {path: 'documentation', component: DocumentationComponent}, // if not authenticated diff --git a/src/app/app.component.html b/src/app/app.component.html index eadcb78..919b47f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,6 +2,7 @@ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fc074c0..77cccbb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -8,6 +8,13 @@ import {Router} from '@angular/router'; // TODO: filter by not completely filled/no measurements // TODO: account // TODO: admin user handling, template pages, validation of samples +// TODO: activate filter on start typing + +// TODO: Build IconComponent free lib version because of CSP +// TODO: more helmet headers, UI presentatin plan +// TODO: sort material numbers, filter field measurements +// TODO: get rid of chart.js (+moment.js) and lodash +// TODO: look into CSS/XHR/Anfragen tab of console @Component({ selector: 'app-root', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4fd6a1c..c78cc33 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,7 +11,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {LocalStorageModule} from 'angular-2-local-storage'; import {HttpClientModule} from '@angular/common/http'; import { SamplesComponent } from './samples/samples.component'; -import {RbTableModule} from './rb-table/rb-table.module'; +import {RbCustomInputsModule} from './rb-custom-inputs/rb-custom-inputs.module'; import { SampleComponent } from './sample/sample.component'; import { ValidateDirective } from './validate.directive'; import {CommonModule} from '@angular/common'; @@ -21,6 +21,8 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { DocumentationComponent } from './documentation/documentation.component'; import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; import { ExistsPipe } from './exists.pipe'; +import { TemplatesComponent } from './templates/templates.component'; +import { ParametersPipe } from './parameters.pipe'; @NgModule({ declarations: [ @@ -34,7 +36,9 @@ import { ExistsPipe } from './exists.pipe'; ObjectPipe, DocumentationComponent, ImgMagnifierComponent, - ExistsPipe + ExistsPipe, + TemplatesComponent, + ParametersPipe ], imports: [ LocalStorageModule.forRoot({ @@ -47,7 +51,7 @@ import { ExistsPipe } from './exists.pipe'; RbUiComponentsModule, FormsModule, HttpClientModule, - RbTableModule, + RbCustomInputsModule, ReactiveFormsModule, FormFieldsModule, CommonModule, diff --git a/src/app/models/sample.model.ts b/src/app/models/sample.model.ts index 319af04..18cbfc1 100644 --- a/src/app/models/sample.model.ts +++ b/src/app/models/sample.model.ts @@ -17,6 +17,7 @@ export class SampleModel extends BaseModel { note_id: IdModel = null; user_id: IdModel = null; notes: {comment: string, sample_references: {sample_id: IdModel, relation: string}[], custom_fields: {[prop: string]: string}} = {comment: '', sample_references: [], custom_fields: {}}; + added: Date = null; deserialize(input: any): this { Object.assign(this, input); @@ -27,6 +28,9 @@ export class SampleModel extends BaseModel { if (input.hasOwnProperty('measurements')) { this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e)); } + if (input.hasOwnProperty('added')) { + this.added = new Date(input.added); + } return this; } diff --git a/src/app/models/template.model.ts b/src/app/models/template.model.ts index c6d7099..23cbbe6 100644 --- a/src/app/models/template.model.ts +++ b/src/app/models/template.model.ts @@ -4,6 +4,7 @@ import {BaseModel} from './base.model'; export class TemplateModel extends BaseModel { _id: IdModel = null; name = ''; - version = 1; - parameters: {name: string, range: {[prop: string]: any}}[] = []; + version = 0; + first_id: IdModel = null; + parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = []; } diff --git a/src/app/object.pipe.ts b/src/app/object.pipe.ts index 7dc3e50..27c101b 100644 --- a/src/app/object.pipe.ts +++ b/src/app/object.pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; +import _ from 'lodash'; @Pipe({ name: 'object', @@ -6,8 +7,9 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class ObjectPipe implements PipeTransform { - transform(value: object): string { - return value ? JSON.stringify(value) : ''; + transform(value: object, omit: string[] = []): string { + const res = _.omit(value, omit); + return res && Object.keys(res).length ? JSON.stringify(res) : ''; } } diff --git a/src/app/parameters.pipe.spec.ts b/src/app/parameters.pipe.spec.ts new file mode 100644 index 0000000..2dc7f21 --- /dev/null +++ b/src/app/parameters.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ParametersPipe } from './parameters.pipe'; + +describe('ParametersPipe', () => { + it('create an instance', () => { + const pipe = new ParametersPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/parameters.pipe.ts b/src/app/parameters.pipe.ts new file mode 100644 index 0000000..c3372e1 --- /dev/null +++ b/src/app/parameters.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'parameters' +}) +export class ParametersPipe implements PipeTransform { + + transform(value: {name: string, range: object}[]): string { + return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any').replace(/["{}]/g, '')}>`).join(', ')}}`; + } + +} diff --git a/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.spec.ts b/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.spec.ts new file mode 100644 index 0000000..9c60a48 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ArrayInputHelperService } from './array-input-helper.service'; + +describe('ArrayInputHelperService', () => { + let service: ArrayInputHelperService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ArrayInputHelperService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.ts b/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.ts new file mode 100644 index 0000000..5f71f55 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/array-input-helper.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import {Observable, Subject} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ArrayInputHelperService { + + com: Subject<{ id: string, index: number, value: any }> = new Subject(); + + constructor() { } + + values(id: string) { + return new Observable<{index: number, value: any}>(observer => { + this.com.subscribe(data => { + if (data.id === id) { + observer.next({index: data.index, value: data.value}); + } + }); + }); + } + + newValue(id: string, index: number, value: any) { + this.com.next({id, index, value}); + } +} diff --git a/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.html b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.html new file mode 100644 index 0000000..d68f8c8 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.scss b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.spec.ts b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.spec.ts new file mode 100644 index 0000000..1e0acad --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RbArrayInputComponent } from './rb-array-input.component'; + +describe('RbArrayInputComponent', () => { + let component: RbArrayInputComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RbArrayInputComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbArrayInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.ts b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.ts new file mode 100644 index 0000000..e258217 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.ts @@ -0,0 +1,107 @@ +import { + AfterViewInit, + Component, + ContentChild, + Directive, + forwardRef, + HostListener, + Input, + OnInit, + TemplateRef +} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import _ from 'lodash'; +import {ArrayInputHelperService} from './array-input-helper.service'; + +// TODO: implement everywhere + +@Directive({ // directive for template and input values + // tslint:disable-next-line:directive-selector + selector: '[rbArrayInputItem]' +}) +export class RbArrayInputItemDirective { + constructor(public templateRef: TemplateRef) { + } +} + +@Directive({ // directive for change detection + // tslint:disable-next-line:directive-selector + selector: '[rbArrayInputListener]' +}) +export class RbArrayInputListenerDirective { + + @Input() rbArrayInputListener: string; + @Input() index: number; + + constructor( + private helperService: ArrayInputHelperService + ) { } + + @HostListener('ngModelChange', ['$event']) + onChange(event) { + console.log(event); + this.helperService.newValue(this.rbArrayInputListener, this.index, event); + } +} + + +@Component({ + // tslint:disable-next-line:component-selector + selector: 'rb-array-input', + templateUrl: './rb-array-input.component.html', + styleUrls: ['./rb-array-input.component.scss'], + providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}] +}) +export class RbArrayInputComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + @Input() pushTemplate: any; + @Input() pushPath: string; + + @ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective; + @ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective; + + values = []; // main array to display + + onChange = (ignore?: any): void => {}; + onTouched = (ignore?: any): void => {}; + + + constructor( + private helperService: ArrayInputHelperService + ) { } + + ngOnInit(): void { + } + + ngAfterViewInit() { + setTimeout(() => { // needed to find reference + this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => { // action on value change + this.values[data.index][this.pushPath] = data.value; + console.log(this.values); + if (this.values[this.values.length - 1][this.pushPath] === '' && this.values[this.values.length - 2][this.pushPath] === '') { // remove last element if last two are empty + this.values.pop(); + } + else if (this.values[this.values.length - 1][this.pushPath] !== '') { // add element if last one is filled + this.values.push(_.cloneDeep(this.pushTemplate)); + } + this.onChange(this.values.filter(e => e !== '')); // trigger ngModel with filled elements + }); + }, 0); + } + + writeValue(obj: any) { // add empty value on init + this.values = obj ? obj : []; + if (this.values.length === 0 || this.values[0] !== '') { + console.log(this.values); + this.values.push(_.cloneDeep(this.pushTemplate)); + } + } + + registerOnChange(fn: any) { + this.onChange = fn; + } + + registerOnTouched(fn: any) { + this.onTouched = fn; + } +} diff --git a/src/app/rb-custom-inputs/rb-custom-inputs.module.ts b/src/app/rb-custom-inputs/rb-custom-inputs.module.ts new file mode 100644 index 0000000..890205f --- /dev/null +++ b/src/app/rb-custom-inputs/rb-custom-inputs.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RbTableComponent } from './rb-table/rb-table.component'; +import {RbArrayInputComponent, RbArrayInputListenerDirective, RbArrayInputItemDirective} from './rb-array-input/rb-array-input.component'; +import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; +import {FormsModule} from '@angular/forms'; +import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component'; + + + +@NgModule({ + declarations: [ + RbTableComponent, + RbArrayInputComponent, + RbArrayInputListenerDirective, + RbArrayInputItemDirective, + RbIconButtonComponent + ], + imports: [ + CommonModule, + FormsModule, + RbUiComponentsModule + ], + exports: [ + RbTableComponent, + RbArrayInputComponent, + RbArrayInputListenerDirective, + RbArrayInputItemDirective, + RbIconButtonComponent + ] +}) +export class RbCustomInputsModule { } diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html new file mode 100644 index 0000000..33826bc --- /dev/null +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.spec.ts b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.spec.ts new file mode 100644 index 0000000..dad6730 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RbIconButtonComponent } from './rb-icon-button.component'; + +describe('RbIconButtonComponent', () => { + let component: RbIconButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RbIconButtonComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbIconButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts new file mode 100644 index 0000000..f48ebf7 --- /dev/null +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts @@ -0,0 +1,22 @@ +import {Component, Input, OnInit} from '@angular/core'; + +// TODO: apply everywhere + +@Component({ + // tslint:disable-next-line:component-selector + selector: 'rb-icon-button', + templateUrl: './rb-icon-button.component.html', + styleUrls: ['./rb-icon-button.component.scss'] +}) +export class RbIconButtonComponent implements OnInit { + + @Input() icon: string; + @Input() mode: string; + @Input() disabled; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/rb-table/rb-table/rb-table.component.html b/src/app/rb-custom-inputs/rb-table/rb-table.component.html similarity index 100% rename from src/app/rb-table/rb-table/rb-table.component.html rename to src/app/rb-custom-inputs/rb-table/rb-table.component.html diff --git a/src/app/rb-table/rb-table/rb-table.component.scss b/src/app/rb-custom-inputs/rb-table/rb-table.component.scss similarity index 100% rename from src/app/rb-table/rb-table/rb-table.component.scss rename to src/app/rb-custom-inputs/rb-table/rb-table.component.scss diff --git a/src/app/rb-table/rb-table/rb-table.component.spec.ts b/src/app/rb-custom-inputs/rb-table/rb-table.component.spec.ts similarity index 100% rename from src/app/rb-table/rb-table/rb-table.component.spec.ts rename to src/app/rb-custom-inputs/rb-table/rb-table.component.spec.ts diff --git a/src/app/rb-table/rb-table/rb-table.component.ts b/src/app/rb-custom-inputs/rb-table/rb-table.component.ts similarity index 85% rename from src/app/rb-table/rb-table/rb-table.component.ts rename to src/app/rb-custom-inputs/rb-table/rb-table.component.ts index 6394052..67e4f68 100644 --- a/src/app/rb-table/rb-table/rb-table.component.ts +++ b/src/app/rb-custom-inputs/rb-table/rb-table.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ + // tslint:disable-next-line:component-selector selector: 'rb-table', templateUrl: './rb-table.component.html', styleUrls: ['./rb-table.component.scss'] diff --git a/src/app/rb-table/rb-table.module.ts b/src/app/rb-table/rb-table.module.ts deleted file mode 100644 index 37ff2ed..0000000 --- a/src/app/rb-table/rb-table.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RbTableComponent } from './rb-table/rb-table.component'; - - - -@NgModule({ - declarations: [ - RbTableComponent - ], - imports: [ - CommonModule - ], - exports: [ - RbTableComponent - ] -}) -export class RbTableModule { } diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 1e453bb..4056ef8 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -1,3 +1,4 @@ +

Samples

@@ -87,8 +88,10 @@
{{key.label}} - - + + + +
@@ -104,7 +107,7 @@ {{sample.type}} {{sample.color}} {{sample.batch}} - {{sample.notes | object}} + {{sample.notes | object: ['_id', 'sample_references']}} {{sample[key[1]] | exists: key[2]}} {{sample.added | date:'dd/MM/yy'}}
diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index 55d78fd..7c2cc64 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -178,5 +178,5 @@ textarea.linkmodal { .filter-inputs > * { display: inline-block; - max-width: 250px; + width: 220px; } diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index 0aa5c89..c4f6195 100644 --- a/src/app/samples/samples.component.ts +++ b/src/app/samples/samples.component.ts @@ -2,6 +2,7 @@ import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core import {ApiService} from '../services/api.service'; import {AutocompleteService} from '../services/autocomplete.service'; import _ from 'lodash'; +import {SampleModel} from '../models/sample.model'; interface LoadSamplesOptions { @@ -13,6 +14,7 @@ interface KeyInterface { id: string; label: string; active: boolean; + sortable: boolean; } @Component({ @@ -21,9 +23,8 @@ interface KeyInterface { styleUrls: ['./samples.component.scss'] }) -// TODO: manage branches, introduce versioning, only upload ui from master -// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html +// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html export class SamplesComponent implements OnInit { @@ -32,7 +33,7 @@ export class SamplesComponent implements OnInit { downloadCsv = false; materials = {}; - samples = []; + samples: SampleModel[] = []; totalSamples = 0; // total number of samples csvUrl = ''; // store url separate so it only has to be generated when clicking the download button filters = { @@ -61,16 +62,16 @@ export class SamplesComponent implements OnInit { loadSamplesQueue = []; // arguments of queued up loadSamples() calls apiKey = ''; keys: KeyInterface[] = [ - {id: 'number', label: 'Number', active: true}, - {id: 'material.numbers', label: 'Material numbers', active: true}, - {id: 'material.name', label: 'Material name', active: true}, - {id: 'material.supplier', label: 'Supplier', active: true}, - {id: 'material.group', label: 'Material', active: false}, - {id: 'type', label: 'Type', active: true}, - {id: 'color', label: 'Color', active: true}, - {id: 'batch', label: 'Batch', active: true}, - {id: 'notes', label: 'Notes', active: false}, - {id: 'added', label: 'Added', active: true} + {id: 'number', label: 'Number', active: true, sortable: true}, + {id: 'material.numbers', label: 'Material numbers', active: true, sortable: false}, + {id: 'material.name', label: 'Material name', active: true, sortable: true}, + {id: 'material.supplier', label: 'Supplier', active: true, sortable: true}, + {id: 'material.group', label: 'Material', active: false, sortable: true}, + {id: 'type', label: 'Type', active: true, sortable: true}, + {id: 'color', label: 'Color', active: true, sortable: true}, + {id: 'batch', label: 'Batch', active: true, sortable: true}, + {id: 'notes', label: 'Notes', active: false, sortable: false}, + {id: 'added', label: 'Added', active: true, sortable: true}, ]; isActiveKey: {[key: string]: boolean} = {}; activeKeys: KeyInterface[] = []; @@ -112,8 +113,11 @@ export class SamplesComponent implements OnInit { const templateKeys = []; data.forEach(item => { item.parameters.forEach(parameter => { - templateKeys.push({id: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false}); - this.filters.filters.push({field: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']}); + const parameterName = encodeURIComponent(parameter.name); + if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) { // exclude spectrum + templateKeys.push({id: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, sortable: true}); + this.filters.filters.push({field: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']}); + } }); }); this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys); @@ -220,6 +224,7 @@ export class SamplesComponent implements OnInit { updateFilterFields(field) { const filter = this.filters.filters.find(e => e.field === field); + filter.active = true; if (filter.mode === 'in' || filter.mode === 'nin') { if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') { filter.values.pop(); @@ -250,6 +255,8 @@ export class SamplesComponent implements OnInit { } calcFieldSelectKeys() { + console.log('CALC'); + console.log(this.keys); this.keys.forEach(key => { this.isActiveKey[key.id] = key.active; }); diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index 088792a..6de63eb 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -9,7 +9,10 @@ import {Observable} from 'rxjs'; }) export class LoginService implements CanActivate { + private maintainPaths = ['templates']; + private loggedIn; + private level; constructor( private api: ApiService, @@ -27,6 +30,7 @@ export class LoginService implements CanActivate { if (!error) { if (data.status === 'Authorization successful') { this.loggedIn = true; + this.level = data.level; resolve(true); } else { this.loggedIn = false; @@ -49,14 +53,21 @@ export class LoginService implements CanActivate { canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable { return new Observable(observer => { - if (this.loggedIn === undefined) { - this.login().then(res => { - observer.next(res as any); + const isMaintainPath = this.maintainPaths.indexOf(route.url[0].path) >= 0; + if (!isMaintainPath || (isMaintainPath && this.isMaintain)) { + if (this.loggedIn === undefined) { + this.login().then(res => { + observer.next(res as any); + observer.complete(); + }); + } + else { + observer.next(this.loggedIn); observer.complete(); - }); + } } else { - observer.next(this.loggedIn); + observer.next(false); observer.complete(); } }); @@ -66,6 +77,10 @@ export class LoginService implements CanActivate { return this.loggedIn; } + get isMaintain() { + return this.level === 'maintain' || this.level === 'admin'; + } + get username() { return atob(this.storage.get('basicAuth')).split(':')[0]; } diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts index 229c967..8dd273a 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -121,4 +121,53 @@ export class ValidationService { } return {ok: true, error: ''}; } + + parameterName(data) { + const {ignore, error} = Joi.string() + .max(128) + .invalid('condition_template', 'material_template') + .pattern(/^[^.]+$/) + .required() + .messages({'string.pattern.base': 'name must not contain a dot'}) + .validate(data); + if (error) { + return {ok: false, error: error.details[0].message}; + } + return {ok: true, error: ''}; + } + + parameterRange(data) { + if (data) { + try { + const {ignore, error} = Joi.object({ + values: Joi.array() + .min(1), + + min: Joi.number(), + + max: Joi.number(), + + type: Joi.string() + .valid('array') + }) + .oxor('values', 'min') + .oxor('values', 'max') + .oxor('type', 'values') + .oxor('type', 'min') + .oxor('type', 'max') + .required() + .validate(JSON.parse(data)); + if (error) { + return {ok: false, error: error.details[0].message}; + } + } + catch (e) { + return {ok: false, error: `no valid JSON`}; + } + return {ok: true, error: ''}; + } + else { + return {ok: false, error: `no valid value`}; + } + } } diff --git a/src/app/templates/templates.component.html b/src/app/templates/templates.component.html new file mode 100644 index 0000000..7169c51 --- /dev/null +++ b/src/app/templates/templates.component.html @@ -0,0 +1,66 @@ +

Templates

+ + + + + + + + +New template + +
+
+
Name
+
Version
+
+ + +
+
{{group.name}}
+
{{group.version}}
+
+
+
+ +
{{template.name}}
+
{{template.version}}
+
{{template.parameters | parameters}}
+
+
+
+
+ + {{supplierInput.errors.failure}} + + + + + {{parameterName.errors.failure}} + + + {{parameterRange.errors.failure}} + + + +
+ + Edit template + + + Save template + +
+
+
+
+
+
+ diff --git a/src/app/templates/templates.component.scss b/src/app/templates/templates.component.scss new file mode 100644 index 0000000..eb79c15 --- /dev/null +++ b/src/app/templates/templates.component.scss @@ -0,0 +1,44 @@ +@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; + +.list { + + .row { + display: grid; + grid-template-columns: 1fr 4fr; + border-bottom: 1px solid $color-gray-mercury; + overflow: hidden; + + & > div { + padding: 8px 5px; + + &.header { + font-weight: bold; + } + + &.details { + grid-column: span 2; + display: grid; + grid-template-columns: 1fr 1fr 3fr; + background: $color-gray-alabaster; + + .template-actions { + grid-column: span 3; + margin-top: 10px; + + .parameters { + display: grid; + grid-template-columns: 1fr 2fr; + } + + rb-icon-button[icon="save"] { + float: right; + } + } + } + } + } +} + +.clickable { + cursor: pointer; +} diff --git a/src/app/templates/templates.component.spec.ts b/src/app/templates/templates.component.spec.ts new file mode 100644 index 0000000..b5c63aa --- /dev/null +++ b/src/app/templates/templates.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TemplatesComponent } from './templates.component'; + +describe('TemplatesComponent', () => { + let component: TemplatesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TemplatesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TemplatesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/templates/templates.component.ts b/src/app/templates/templates.component.ts new file mode 100644 index 0000000..c86e07c --- /dev/null +++ b/src/app/templates/templates.component.ts @@ -0,0 +1,124 @@ +import { Component, OnInit } from '@angular/core'; +import {ApiService} from '../services/api.service'; +import {TemplateModel} from '../models/template.model'; +import {animate, style, transition, trigger} from '@angular/animations'; +import {ValidationService} from '../services/validation.service'; +import _ from 'lodash'; + +@Component({ + selector: 'app-templates', + templateUrl: './templates.component.html', + styleUrls: ['./templates.component.scss'], + animations: [ + trigger( + 'inOut', [ + transition(':enter', [ + style({height: 0, opacity: 0}), + animate('0.5s ease-out', style({height: '*', opacity: 1})) + ]), + transition(':leave', [ + style({height: '*', opacity: 1}), + animate('0.5s ease-in', style({height: 0, opacity: 0})) + ]) + ] + ) + ] +}) +export class TemplatesComponent implements OnInit { + + collection = 'measurement'; + templates: TemplateModel[] = []; + templateGroups: {[first_id: string]: TemplateModel[]} = {}; // templates grouped by first_id + templateEdit: {[first_id: string]: TemplateModel} = {}; // latest template of each first_id for editing + groupsView: {first_id: string, name: string, version: number, expanded: boolean, edit: boolean, entries: TemplateModel[]}[] = []; + arr = ['testA', 'testB', 'testC']; + + constructor( + private api: ApiService, + private validate: ValidationService + ) { } + + ngOnInit(): void { + this.loadTemplates(); + } + + loadTemplates() { + this.api.get(`/template/${this.collection}s`, data => { + this.templates = data; + this.templateFormat(); + }); + } + + templateFormat() { + this.templateGroups = {}; + this.templateEdit = {}; + this.templates.forEach(template => { + if (this.templateGroups[template.first_id]) { + this.templateGroups[template.first_id].push(template); + } + else { + this.templateGroups[template.first_id] = [template]; + } + }); + Object.keys(this.templateGroups).forEach(id => { + this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version); + this.templateEdit[id] = _.cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]); + this.templateEdit[id].parameters = this.templateEdit[id].parameters.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; }); + }); + this.groupsView = Object.values(this.templateGroups) + .map(e => ({ + first_id: e[e.length - 1].first_id, + name: e[e.length - 1].name, + version: e[e.length - 1].version, + expanded: false, + edit: false, + entries: e + })); + } + + saveTemplate(first_id) { + const template = _.cloneDeep(this.templateEdit[first_id]); + template.parameters = template.parameters.filter(e => e.name !== ''); + let valid = true; + valid = valid && this.validate.string(template.name).ok; + template.parameters.forEach(parameter => { + valid = valid && this.validate.parameterName(parameter.name).ok; + valid = valid && this.validate.parameterRange(parameter.rangeString).ok; + if (valid) { + parameter.range = JSON.parse(parameter.rangeString); + } + }); + if (valid) { + console.log('valid', template); + const sendData = {name: template.name, parameters: template.parameters.map(e => _.omit(e, ['rangeString']))}; + if (first_id === 'null') { + this.api.post(`/template/${this.collection}/new`, sendData, data => { + if (data.version > template.version) { // there were actual changes and a new version was created + this.templates.push(data); + } + this.templateFormat(); + }); + } + else { + this.api.put(`/template/${this.collection}/${template.first_id}`, sendData, data => { + if (data.version > template.version) { // there were actual changes and a new version was created + this.templates.push(data); + } + this.templateFormat(); + }); + } + } + else { + console.log('not valid'); + } + } + + newTemplate() { + if (!this.templateEdit.null) { + const template = new TemplateModel(); + template.name = 'new template'; + this.groupsView.push({first_id: 'null', name: 'new template', version: 0, expanded: true, edit: true, entries: [template]}); + this.templateEdit.null = new TemplateModel(); + } + } +} diff --git a/src/assets/imgs/supergraphic.svg b/src/assets/imgs/supergraphic.svg new file mode 100644 index 0000000..85e56b9 --- /dev/null +++ b/src/assets/imgs/supergraphic.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index d08cb34..ae773e1 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -15,3 +15,7 @@ a, a:active, a:focus { button::-moz-focus-inner { border: 0; } + +.supergraphic { + background-image: url("assets/imgs/supergraphic.svg"); +}