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..2bc464c --- /dev/null +++ b/cf_config/headers.conf @@ -0,0 +1,9 @@ +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'"; +add_header X-Frame-Options DENY +add_header X-DNS-Prefetch-Control off +add_header Strict-Transport-Security max-age=15552000 +add_header X-Download-Options noopen +add_header X-Content-Type-Options nosniff +add_header X-Permitted-Cross-Domain-Policies none +add_header Referrer-Policy no-referrer +add_header X-XSS-Protection "1; mode=block" 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..5f6e05a 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", @@ -12324,6 +12388,11 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "str-compare": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/str-compare/-/str-compare-0.1.2.tgz", + "integrity": "sha1-eOaGGlccGiKhnq4Q5wmWt9z9r0Y=" + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -12914,6 +12983,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 +13801,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..6c9df32 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": { @@ -23,7 +24,8 @@ "@angular/platform-browser-dynamic": "~9.1.7", "@angular/router": "~9.1.7", "@hapi/joi": "^17.1.1", - "@inst-iot/bosch-angular-ui-components": "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz", + "@inst-iot/bosch-angular-ui-components": + "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz", "angular-2-local-storage": "^3.0.2", "chart.js": "^2.9.3", "chartjs-plugin-datalabels": "^0.7.0", @@ -32,6 +34,7 @@ "ng2-charts": "^2.3.2", "quick-score": "0.0.8", "rxjs": "~6.5.5", + "str-compare": "^0.1.2", "tslib": "^1.10.0", "zone.js": "~0.10.2" }, @@ -54,6 +57,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..8fa4f6c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,6 +5,9 @@ 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'; +import {SettingsComponent} from './settings/settings.component'; +import {UsersComponent} from './users/users.component'; const routes: Routes = [ @@ -13,6 +16,10 @@ 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, canActivate: [LoginService]}, + // {path: 'users', component: UsersComponent, canActivate: [LoginService]}, + {path: 'users', component: UsersComponent}, // TODO: change + {path: 'settings', component: SettingsComponent, 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..884286d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,6 +2,8 @@ @@ -10,12 +12,9 @@ {{loginService.username}} - +
-

- -

- +   Settings
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 31ad797..dd5d5f3 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -3,3 +3,9 @@ font-size: 32px; margin-right: 40px; } + +.spacing { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} 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..18590d1 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,10 @@ 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'; +import { SettingsComponent } from './settings/settings.component'; +import { UsersComponent } from './users/users.component'; @NgModule({ declarations: [ @@ -34,7 +38,11 @@ import { ExistsPipe } from './exists.pipe'; ObjectPipe, DocumentationComponent, ImgMagnifierComponent, - ExistsPipe + ExistsPipe, + TemplatesComponent, + ParametersPipe, + SettingsComponent, + UsersComponent ], imports: [ LocalStorageModule.forRoot({ @@ -47,7 +55,7 @@ import { ExistsPipe } from './exists.pipe'; RbUiComponentsModule, FormsModule, HttpClientModule, - RbTableModule, + RbCustomInputsModule, ReactiveFormsModule, FormFieldsModule, CommonModule, diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index e765fc9..6de06e5 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -6,10 +6,14 @@ {{usernameInput.errors.failure}} - + {{passwordInput.errors.failure}} - - {{message}} + + {{emailInput.errors.failure}} + + Forgot password + +
{{message}}
diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index 52566ab..9f3ea33 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -4,9 +4,14 @@ .message { font-size: 13px; - white-space: pre-line; + margin-top: 10px; } .login-button { margin-right: 10px; } + +.forgot-pass { + display: block; + margin-bottom: 1rem; +} diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 6c28dcc..c1bc69f 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -2,6 +2,7 @@ import {Component, OnInit, ViewChild} from '@angular/core'; import {ValidationService} from '../services/validation.service'; import {LoginService} from '../services/login.service'; import {Router} from '@angular/router'; +import {ApiService} from '../services/api.service'; @Component({ @@ -13,13 +14,17 @@ export class LoginComponent implements OnInit { username = ''; // credentials password = ''; + email = ''; message = ''; // message below login fields + passreset = false; + @ViewChild('loginForm') loginForm; constructor( private validate: ValidationService, private loginService: LoginService, + private api: ApiService, private router: Router ) { } @@ -27,14 +32,26 @@ export class LoginComponent implements OnInit { } login() { - this.loginService.login(this.username, this.password).then(ok => { - if (ok) { - this.message = 'Login successful'; - this.router.navigate(['/samples']); - } - else { - this.message = 'Wrong credentials!'; - } - }); + if (this.passreset) { + this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => { + if (err) { + this.message = 'Could not find a valid user'; + } + else { + this.message = 'Password reset, check your inbox'; + } + }); + } + else { + this.loginService.login(this.username, this.password).then(ok => { + if (ok) { + this.message = 'Login successful'; + this.router.navigate(['/samples']); + } + else { + this.message = 'Wrong credentials!'; + } + }); + } } } 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/models/user.model.spec.ts b/src/app/models/user.model.spec.ts new file mode 100644 index 0000000..3ba7e0b --- /dev/null +++ b/src/app/models/user.model.spec.ts @@ -0,0 +1,7 @@ +import { UserModel } from './user.model'; + +describe('User.Model', () => { + it('should create an instance', () => { + expect(new UserModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts new file mode 100644 index 0000000..66f62fe --- /dev/null +++ b/src/app/models/user.model.ts @@ -0,0 +1,28 @@ +import _ from 'lodash'; +import {BaseModel} from './base.model'; +import {IdModel} from './id.model'; + +export class UserModel extends BaseModel{ + _id: IdModel = null; + name = ''; + origName = ''; + email = ''; + level = ''; + location = ''; + device_name = ''; + edit = false; + + deserialize(input: any): this { + Object.assign(this, input); + this.origName = this.name; + return this; + } + + sendFormat(mode = 'user') { + const keys = ['name', 'email', 'location', 'device_name']; + if (mode === 'admin') { + keys.push('level'); + } + return _.pick(this, keys); + } +} 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..82501eb --- /dev/null +++ b/src/app/rb-custom-inputs/rb-array-input/rb-array-input.component.ts @@ -0,0 +1,149 @@ +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'; + + +@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; + + constructor( + private helperService: ArrayInputHelperService + ) { } + + @HostListener('ngModelChange', ['$event']) + onChange(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 { + + pushTemplate: any = ''; + @Input('pushTemplate') set _pushTemplate(value) { + this.pushTemplate = value; + if (this.values.length) { + this.updateArray(); + } + } + @Input() pushPath: string = null; + + @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 + // assign value + if (this.pushPath) { + this.values[data.index][this.pushPath] = data.value; + } + else { + this.values[data.index] = data.value; + } + console.log(111, this.values); + this.updateArray(); + }); + }, 0); + } + + updateArray() { + let res; + // adjust fields if pushTemplate is specified + if (this.pushTemplate !== null) { + if (this.pushPath) { + // remove last element if last two are empty + if (this.values[this.values.length - 1][this.pushPath] === '' && this.values[this.values.length - 2][this.pushPath] === '') { + this.values.pop(); + } + // add element if last all are filled + else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) { + this.values.push(_.cloneDeep(this.pushTemplate)); + } + res = this.values.filter(e => e[this.pushPath] !== ''); + } + else { + // remove last element if last two are empty + if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') { + this.values.pop(); + } + else if (this.values.filter(e => e !== '').length === this.values.length) { // add element if all are is filled + this.values.push(_.cloneDeep(this.pushTemplate)); + } + res = this.values.filter(e => e !== ''); + } + } + else { + this.values = [this.values[0]]; + res = this.values; + } + if (!res.length) { + res = ['']; + } + this.onChange(res); // trigger ngModel with filled elements + } + + writeValue(obj: any) { // add empty value on init + this.values = obj ? obj : []; + if (this.values.length === 0 || this.values[0] !== '') { + // add empty last field if pushTemplate is specified + if (this.pushTemplate !== null) { + 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..6e4f70d --- /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..8896e99 --- /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'; + + +@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; + @Input() type = 'button'; + + 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/sample/sample.component.html b/src/app/sample/sample.component.html index 7401416..5780178 100644 --- a/src/app/sample/sample.component.html +++ b/src/app/sample/sample.component.html @@ -1,4 +1,4 @@ -

{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}

+

{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}

@@ -6,28 +6,48 @@
- + Cannot be empty Unknown material, add properties for new material - + New material

Material properties

- + {{supplierInput.errors.failure}} - + {{groupInput.errors.failure}} -
- -
- + + + The specified {{modalText.list}} could not be found in the list.
+ Did you mean {{modalText.suggestion}}? +
+
+ + + + - + {{parameterInput.errors.failure}} Cannot be empty @@ -36,11 +56,13 @@  
- + {{typeInput.errors.failure}} Cannot be empty - + {{colorInput.errors.failure}} Cannot be empty @@ -51,31 +73,43 @@
- + {{commentInput.errors.failure}}
Sample references
- + Unknown sample number
- + Cannot be empty
Additional properties
-
-
- - {{keyInput.errors.failure}} + + +
+ + {{keyInput.errors.failure}} + +
+ + Cannot be empty -
- - Cannot be empty - -
+ +
  @@ -83,14 +117,18 @@

Condition - +

- + - + {{parameterInput.errors.failure}} Cannot be empty @@ -102,16 +140,23 @@

Measurements

- + -
- +
+ {{parameterInput.errors.failure}} Cannot be empty - + Cannot be empty
- - + + Delete measurement +
 
- + + New measurement +
- +
diff --git a/src/app/sample/sample.component.spec.ts b/src/app/sample/sample.component.spec.ts index c32c4c1..85ea267 100644 --- a/src/app/sample/sample.component.spec.ts +++ b/src/app/sample/sample.component.spec.ts @@ -2,7 +2,7 @@ // // import { SampleComponent } from './sample.component'; // -// // TODO +// // TODO: tests // // describe('SampleComponent', () => { // let component: SampleComponent; diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index 5f68668..c03ab0f 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; +import strCompare from 'str-compare'; import { AfterContentChecked, Component, - OnInit, + OnInit, TemplateRef, ViewChild } from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; @@ -17,18 +18,14 @@ import {MeasurementModel} from '../models/measurement.model'; import { ChartOptions } from 'chart.js'; import {animate, style, transition, trigger} from '@angular/animations'; import {Observable} from 'rxjs'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; +import {UserModel} from '../models/user.model'; + -// TODO: tests -// TODO: confirmation for new group/supplier // TODO: work on better recognition for file input // TODO: only show condition (if not set) and measurements in edit sample dialog at first -// TODO: multiple spectra // TODO: multiple samples for base data, extend multiple measurements, conditions -// TODO: material properties, color (in material and sample (not required)) - -// TODO: API $in Regex - @Component({ selector: 'app-sample', templateUrl: './sample.component.html', @@ -55,16 +52,18 @@ export class SampleComponent implements OnInit, AfterContentChecked { new; // true if new sample should be created newMaterial = false; // true if new material should be created materials: MaterialModel[] = []; // all materials - suppliers: string[] = []; // all suppliers - groups: string[] = []; // all groups + ac: {[group: string]: string[]} = { // autocomplete data + supplier: [], + group: [], + materialName: [] + }; conditionTemplates: TemplateModel[]; // all conditions condition: TemplateModel | null = null; // selected condition materialTemplates: TemplateModel[]; // all material templates materialTemplate: TemplateModel | null = null; // selected material template - materialNames = []; // names of all materials material = new MaterialModel(); // object of current selected material sample = new SampleModel(); - customFields: [string, string][] = [['', '']]; + customFields: [string, string][]; sampleReferences: [string, string, string][] = [['', '', '']]; sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db currentSRIndex = 0; // index of last entered sample reference @@ -74,7 +73,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { measurementTemplates: TemplateModel[]; loading = 0; // number of currently loading instances checkFormAfterInit = false; - charts = []; // chart data for spectrums + modalText = {list: '', suggestion: ''}; + charts = []; // chart data for spectra + defaultDevice = ''; readonly chartInit = [{ data: [], label: 'Spectrum', @@ -101,7 +102,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { private route: ActivatedRoute, private api: ApiService, private validation: ValidationService, - public autocomplete: AutocompleteService + public autocomplete: AutocompleteService, + private modal: ModalService ) { } ngOnInit(): void { @@ -109,15 +111,15 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.loading = 7; this.api.get('/materials?status=all', (data: any) => { this.materials = data.map(e => new MaterialModel().deserialize(e)); - this.materialNames = data.map(e => e.name); + this.ac.materialName = data.map(e => e.name); this.loading--; }); this.api.get('/material/suppliers', (data: any) => { - this.suppliers = data; + this.ac.supplier = data; this.loading--; }); this.api.get('/material/groups', (data: any) => { - this.groups = data; + this.ac.mgroup = data; this.loading--; }); this.api.get('/template/conditions', data => { @@ -129,6 +131,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.selectMaterialTemplate(this.materialTemplates[0]._id); this.loading--; }); + this.api.get('/user', data => { + this.defaultDevice = data.device_name; + }); this.api.get('/template/measurements', data => { this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e)); if (!this.new) { @@ -149,7 +154,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { } }); this.material = sData.material; - this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; + this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? + Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; if (this.sample.notes.sample_references.length) { this.sampleReferences = []; this.sampleReferenceAutocomplete = []; @@ -185,7 +191,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { ngAfterContentChecked() { // attach validators to dynamic condition fields when all values are available and template was fully created - if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 && this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) { + if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 && + this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) { for (const i in this.condition.parameters) { if (this.condition.parameters[i]) { this.attachValidator('conditionParameter' + i, this.condition.parameters[i].range, true); @@ -194,7 +201,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { } // attach validators to dynamic material fields when all values are available and template was fully created - if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) { + if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && + this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) { for (const i in this.materialTemplate.parameters) { if (this.materialTemplate.parameters[i]) { this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true); @@ -250,7 +258,6 @@ export class SampleComponent implements OnInit, AfterContentChecked { saveSample() { new Promise(resolve => { if (this.newMaterial) { // save material first if new one exists - this.material.numbers = this.material.numbers.filter(e => e !== ''); this.api.post('/material/new', this.material.sendFormat(), data => { this.materials.push(data); // add material to data this.material = data; @@ -268,7 +275,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.sample.notes.custom_fields[element[0]] = element[1]; } }); - this.sample.notes.sample_references = this.sampleReferences.filter(e => e[0] && e[1] && e[2]).map(e => ({sample_id: e[2], relation: e[1]})); + this.sample.notes.sample_references = this.sampleReferences + .filter(e => e[0] && e[1] && e[2]) + .map(e => ({sample_id: e[2], relation: e[1]})); new Promise(resolve => { if (this.new) { this.api.post('/sample/new', this.sample.sendFormat(), resolve); @@ -289,7 +298,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.api.post('/measurement/new', measurement.sendFormat()); } else { // update measurement - this.api.put('/measurement/' + measurement._id, measurement.sendFormat(['sample_id', 'measurement_template'])); + this.api.put('/measurement/' + measurement._id, + measurement.sendFormat(['sample_id', 'measurement_template'])); } } else if (measurement._id !== null) { // existing measurement was left empty to delete @@ -321,7 +331,6 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } - // TODO: rework later setNewMaterial(value = null) { if (value === null) { this.newMaterial = !this.sample.material_id; @@ -333,24 +342,12 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.sampleForm.form.get('materialname').setValidators([Validators.required]); } else { - this.sampleForm.form.get('materialname').setValidators([Validators.required, this.validation.generate('stringOf', [this.materialNames])]); + this.sampleForm.form.get('materialname') + .setValidators([Validators.required, this.validation.generate('stringOf', [this.ac.materialName])]); } this.sampleForm.form.get('materialname').updateValueAndValidity(); } - handleMaterialNumbers() { - const fieldNo = this.material.numbers.length; - const filledFields = this.material.numbers.filter(e => e !== '').length; - // append new field - if (filledFields === fieldNo) { - this.material.numbers.push(''); - } - // remove if two end fields are empty - if (fieldNo > 1 && this.material.numbers[fieldNo - 1] === '' && this.material.numbers[fieldNo - 2] === '') { - this.material.numbers.pop(); - } - } - selectCondition(id) { this.condition = this.conditionTemplates.find(e => e._id === id); console.log(this.condition); @@ -372,7 +369,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { } addMeasurement() { - this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id)); + this.sample.measurements.push(new MeasurementModel(this.measurementTemplates.filter(e => e.name === 'spectrum').reverse()[0]._id)); + this.sample.measurements[this.sample.measurements.length - 1].values.device = this.defaultDevice; this.charts.push(_.cloneDeep(this.chartInit)); } @@ -389,12 +387,22 @@ export class SampleComponent implements OnInit, AfterContentChecked { } fileToArray(files, mIndex, parameter) { - const fileReader = new FileReader(); - fileReader.onload = () => { - this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(',')); - this.generateChart(this.sample.measurements[mIndex].values[parameter], mIndex); - }; - fileReader.readAsText(files[0]); + for (const i in files) { + if (files.hasOwnProperty(i)) { + const fileReader = new FileReader(); + fileReader.onload = () => { + let index: number = mIndex; + if (Number(i) > 0) { // append further spectra + this.addMeasurement(); + index = this.sample.measurements.length - 1; + } + this.sample.measurements[index].values[parameter] = + fileReader.result.toString().split('\r\n').map(e => e.split(',')); + this.generateChart(this.sample.measurements[index].values[parameter], index); + }; + fileReader.readAsText(files[i]); + } + } } generateChart(spectrum, index) { @@ -411,22 +419,17 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } - adjustCustomFields(value, index) { - this.customFields[index][0] = value; - const fieldNo = this.customFields.length; - let filledFields = 0; - this.customFields.forEach(field => { - if (field[0] !== '') { - filledFields ++; - } - }); - // append new field - if (filledFields === fieldNo) { - this.customFields.push(['', '']); - } - // remove if two end fields are empty - if (fieldNo > 1 && this.customFields[fieldNo - 1][0] === '' && this.customFields[fieldNo - 2][0] === '') { - this.customFields.pop(); + checkTypo(list, modal: TemplateRef) { + if (this.ac[list].indexOf(this.material[list]) < 0) { // entry is not in lise + this.modalText.list = list; + this.modalText.suggestion = this.ac[list] // find possible entry from list + .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[list])})) + .sort((a, b) => b.s - a.s)[0].v; + this.modal.open(modal).then(result => { + if (result) { // use suggestion + this.material[list] = this.modalText.suggestion; + } + }); } } @@ -457,7 +460,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { sampleReferenceList(value) { return new Observable(observer => { - this.api.get<{_id: string, number: string}[]>('/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => { + this.api.get<{_id: string, number: string}[]>( + '/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&' + + 'filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => { console.log(data); this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number); this.sampleReferenceFinds = data; @@ -483,12 +488,14 @@ export class SampleComponent implements OnInit, AfterContentChecked { } uniqueCfValues(index) { // returns all names until index for unique check - return this.customFields.slice(0, index).map(e => e[0]); + return this.customFields ? this.customFields.slice(0, index).map(e => e[0]) : []; } } -// 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante +// 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der +// Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante // 2. Der state "dirty" soll eigentlich anzeigen, wenn ein Form-Field vom User geändert wurde; damit missbrauchst du es hier etwas -// 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^ +// 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich +// abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^ diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 1e453bb..24f665b 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -1,7 +1,8 @@ + @@ -11,14 +12,17 @@
- + validated - + new
- + @@ -28,15 +32,18 @@ - + {{item.label}}
- - + + @@ -48,13 +55,26 @@
- - - - - - - + + + + + + +
@@ -67,18 +87,20 @@
- - - URL for JSON download: - + JSON download link + + + add spectra - + Copy to clipboard - + + Download result as CSV +
@@ -87,8 +109,14 @@
{{key.label}} - - + + + + + + + +
@@ -100,11 +128,13 @@ {{materials[sample.material_id].name}} {{materials[sample.material_id].supplier}} {{materials[sample.material_id].group}} - {{materials[sample.material_id].properties[key[2]] | exists}} + + {{materials[sample.material_id].properties[key[2]] | exists}} + {{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'}} @@ -118,7 +148,8 @@ - + + of {{pages}} ({{totalSamples}} samples) 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.spec.ts b/src/app/samples/samples.component.spec.ts index 1964449..d3ec677 100644 --- a/src/app/samples/samples.component.spec.ts +++ b/src/app/samples/samples.component.spec.ts @@ -2,7 +2,7 @@ // // import { SamplesComponent } from './samples.component'; // -// // TODO +// // TODO: tests // // describe('SamplesComponent', () => { // let component: SamplesComponent; diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index 0aa5c89..3523e14 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 = { @@ -53,7 +54,7 @@ export class SamplesComponent implements OnInit { {field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']}, - {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]} + {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']} ] }; page = 1; @@ -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[] = []; @@ -91,7 +92,8 @@ export class SamplesComponent implements OnInit { this.materials[material._id] = material; }); this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name); - this.filters.filters.find(e => e.field === 'color').autocomplete = [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))]; + this.filters.filters.find(e => e.field === 'color').autocomplete = + [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))]; this.loadSamples(); }); this.api.get('/user/key', (data: {key: string}) => { @@ -112,8 +114,24 @@ 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); + // exclude spectrum + if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) { + 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); @@ -152,10 +170,22 @@ export class SamplesComponent implements OnInit { }); } - sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples + sampleUrl(options: { + paging?: boolean, + pagingOptions?: { + firstPage?: boolean, + toPage?: number, + event?: Event + }, + csv?: boolean, + export?: boolean, + host?: boolean + }) { // return url to fetch samples const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false const query: string[] = []; - query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))); + query.push( + 'status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')) + ); if (options.paging) { if (this.samples[0]) { // do not include from-id when page size was changed if (!options.pagingOptions.firstPage) { @@ -178,11 +208,11 @@ export class SamplesComponent implements OnInit { query.push('key=' + this.apiKey); } this.keys.forEach(key => { - if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { // do not load material properties for table + // do not load material properties for table + if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { query.push('fields[]=' + key.id); } }); - console.log(this.filters.filters); query.push(..._.cloneDeep(this.filters.filters) .map(e => { @@ -195,7 +225,6 @@ export class SamplesComponent implements OnInit { .filter(e => e.active && e.values.length > 0) .map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values'])))) ); - console.log(this.filters); if (!options.export) { additionalTableKeys.forEach(key => { if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added @@ -206,8 +235,9 @@ export class SamplesComponent implements OnInit { else if (this.downloadCsv) { query.push('fields[]=measurements.spectrum.dpt'); } - console.log('/samples?' + query.join('&')); - return (options.host && isDevMode() ? window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&'); + return (options.host && isDevMode() ? window.location.host : '') + + (options.export ? this.api.hostName : '') + + '/samples?' + query.join('&'); } loadPage(delta) { @@ -220,17 +250,7 @@ export class SamplesComponent implements OnInit { updateFilterFields(field) { const filter = this.filters.filters.find(e => e.field === field); - if (filter.mode === 'in' || filter.mode === 'nin') { - if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') { - filter.values.pop(); - } - else if (filter.values[filter.values.length - 1] !== '') { - filter.values.push((filter.field === 'added' ? new Date() : '') as string & Date); - } - } - else { - filter.values = [filter.values[0] as string & Date]; - } + filter.active = true; if (filter.active) { this.loadSamples({firstPage: true}); } @@ -243,10 +263,13 @@ export class SamplesComponent implements OnInit { updateActiveKeys() { // array with all activeKeys this.activeKeys = this.keys.filter(e => e.active); - this.activeTemplateKeys.material = this.keys.filter(e => e.id.indexOf('material.properties.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); - this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); - console.log(this.activeTemplateKeys); - console.log(this.keys); // TODO: glass fiber filter not working + this.activeTemplateKeys.material = this.keys + .filter(e => e.id.indexOf('material.properties.') >= 0 && e.active) + .map(e => e.id.split('.') + .map(el => decodeURIComponent(el))); + this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active) + .map(e => e.id.split('.') + .map(el => decodeURIComponent(el))); // TODO: glass fiber filter not working } calcFieldSelectKeys() { diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index 088792a..f4816bc 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -9,7 +9,20 @@ import {Observable} from 'rxjs'; }) export class LoginService implements CanActivate { + private pathPermissions = [ + {path: 'templates', permission: 'maintain'}, + {path: 'users', permission: 'admin'} + ]; + readonly levels = [ + 'read', + 'write', + 'maintain', + 'dev', + 'admin' + ]; + private loggedIn; + private level; constructor( private api: ApiService, @@ -20,13 +33,30 @@ export class LoginService implements CanActivate { login(username = '', password = '') { return new Promise(resolve => { - if (username !== '') { - this.storage.set('basicAuth', btoa(username + ':' + password)); + if (username !== '' || password !== '') { // some credentials given + let credentials: string[]; + const credentialString: string = this.storage.get('basicAuth'); + if (credentialString) { // found stored credentials + credentials = atob(credentialString).split(':'); + } + else { + credentials = ['', '']; + } + if (username !== '' && password !== '') { // all credentials given + this.storage.set('basicAuth', btoa(username + ':' + password)); + } + else if (username !== '') { // username given + this.storage.set('basicAuth', btoa(username + ':' + credentials[1])); + } + else if (password !== '') { // password given + this.storage.set('basicAuth', btoa(credentials[0] + ':' + password)); + } } this.api.get('/authorized', (data: any, error) => { if (!error) { if (data.status === 'Authorization successful') { this.loggedIn = true; + this.level = data.level; resolve(true); } else { this.loggedIn = false; @@ -49,14 +79,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 pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); + if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path + 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 +103,10 @@ export class LoginService implements CanActivate { return this.loggedIn; } + is(level) { + return this.levels.indexOf(this.level) >= this.levels.indexOf(level); + } + 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..a58ec72 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -66,10 +66,16 @@ export class ValidationService { return {ok: true, error: ''}; } - string(data) { - const {ignore, error} = Joi.string().max(128).allow('').validate(data); + string(data, option = null) { + let validator = Joi.string().max(128).allow(''); + let errorMsg = 'must contain max 128 characters'; + if (option === 'alphanum') { + validator = validator.alphanum(); + errorMsg = 'must contain max 128 alphanumerical characters'; + } + const {ignore, error} = validator.validate(data); if (error) { - return {ok: false, error: 'must contain max 128 characters'}; + return {ok: false, error: errorMsg}; } return {ok: true, error: ''}; } @@ -121,4 +127,60 @@ export class ValidationService { } return {ok: true, error: ''}; } + + equal(data, compare) { + if (data !== compare) { + return {ok: false, error: `must be equal`}; + } + 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/settings/settings.component.html b/src/app/settings/settings.component.html new file mode 100644 index 0000000..8e73ba1 --- /dev/null +++ b/src/app/settings/settings.component.html @@ -0,0 +1,45 @@ +

Settings

+ + + + {{nameInput.errors.failure}} + Cannot be empty + + + Invalid email + Cannot be empty + + + {{locationInput.errors.failure}} + Cannot be empty + + + {{deviceInput.errors.failure}} + + + Save change + + {{messageUser}} + + + +

Change password

+ +
+ + {{passAInput.errors.failure}} + + + {{passBInput.errors.failure}} + + + {{messagePass}} +
+ diff --git a/src/app/settings/settings.component.scss b/src/app/settings/settings.component.scss new file mode 100644 index 0000000..3651625 --- /dev/null +++ b/src/app/settings/settings.component.scss @@ -0,0 +1,7 @@ +.pass-heading { + margin-top: 40px; +} + +.message { + margin-left: 20px; +} diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/settings.component.spec.ts new file mode 100644 index 0000000..91588f3 --- /dev/null +++ b/src/app/settings/settings.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts new file mode 100644 index 0000000..313d727 --- /dev/null +++ b/src/app/settings/settings.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import {ApiService} from '../services/api.service'; +import {UserModel} from '../models/user.model'; +import {Router} from '@angular/router'; +import {LoginService} from '../services/login.service'; + + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'] +}) +export class SettingsComponent implements OnInit { + + user: UserModel = new UserModel(); + password = ''; + messageUser = ''; + messagePass = ''; + + constructor( + private api: ApiService, + private login: LoginService, + private router: Router + ) { } + + ngOnInit(): void { + this.api.get('/user', data => { + this.user.deserialize(data); + }); + } + + saveUser() { + this.api.put('/user', this.user.sendFormat(), (data, err) => { + if (err) { + this.messageUser = err.error.status; + } + else { + this.login.login(data.name).then(res => { + if (res) { + this.router.navigate(['/samples']); + } + else { + this.messageUser = 'request not successful, try again'; + } + }); + } + }); + } + + savePass() { + this.api.put('/user', {pass: this.password}, (ignore, err) => { + if (err) { + this.messagePass = err.error.status; + } + else { + this.login.login('', this.password).then(res => { + if (res) { + this.router.navigate(['/samples']); + } + else { + this.messagePass = 'request not successful, try again'; + } + }); + } + }); + } + +} diff --git a/src/app/templates/templates.component.html b/src/app/templates/templates.component.html new file mode 100644 index 0000000..6d71ebc --- /dev/null +++ b/src/app/templates/templates.component.html @@ -0,0 +1,67 @@ +

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..ede71e8 --- /dev/null +++ b/src/app/templates/templates.component.scss @@ -0,0 +1,40 @@ +@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; + } + } + } + } + } +} 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/app/users/users.component.html b/src/app/users/users.component.html new file mode 100644 index 0000000..b176428 --- /dev/null +++ b/src/app/users/users.component.html @@ -0,0 +1,94 @@ +

Users

+ +New user + +
+ + {{nameInput.errors.failure}} + Cannot be empty + + + Invalid email + Cannot be empty + + + + Cannot be empty + + + {{locationInput.errors.failure}} + Cannot be empty + + + {{deviceInput.errors.failure}} + + + {{passAInput.errors.failure}} + + + {{passBInput.errors.failure}} + + + Save user + +
+ + + + Name + Email + Level + Location + Device + + + + + + {{user.name}} + {{user.email}} + {{user.level}} + {{user.location}} + {{user.device_name}} + + + + + + {{nameInput.errors.failure}} + Cannot be empty + + + + + Invalid email + Cannot be empty + + + + + + + + + + {{locationInput.errors.failure}} + Cannot be empty + + + + + {{deviceInput.errors.failure}} + + + Save + + + diff --git a/src/app/users/users.component.scss b/src/app/users/users.component.scss new file mode 100644 index 0000000..9ba4fcf --- /dev/null +++ b/src/app/users/users.component.scss @@ -0,0 +1,3 @@ +::ng-deep td .error-messages { + position: absolute; +} diff --git a/src/app/users/users.component.spec.ts b/src/app/users/users.component.spec.ts new file mode 100644 index 0000000..909b5ba --- /dev/null +++ b/src/app/users/users.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsersComponent } from './users.component'; + +describe('UsersComponent', () => { + let component: UsersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UsersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/users.component.ts b/src/app/users/users.component.ts new file mode 100644 index 0000000..d80817b --- /dev/null +++ b/src/app/users/users.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import {ApiService} from '../services/api.service'; +import {UserModel} from '../models/user.model'; +import {LoginService} from '../services/login.service'; + + +@Component({ + selector: 'app-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'] +}) +export class UsersComponent implements OnInit { + + users: UserModel[] = []; + newUser: UserModel | null = null; + newUserPass = ''; + + constructor( + private api: ApiService, + public login: LoginService + ) { } + + ngOnInit(): void { + this.api.get('/users', data => { + this.users = data.map(e => new UserModel().deserialize(e)); + }); + } + + saveUser(user: UserModel) { + this.api.put('/user/' + user.origName, user.sendFormat('admin'), data => { + user.deserialize(data); + user.edit = false; + }); + } + + saveNewUser() { + this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => { + this.newUser = null; + this.users.push(new UserModel().deserialize(data)); + }); + } + + addNewUser() { + this.newUser = this.newUser ? null : new UserModel(); + } + +} 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..a0f6fd5 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -15,3 +15,11 @@ a, a:active, a:focus { button::-moz-focus-inner { border: 0; } + +.supergraphic { + background-image: url("assets/imgs/supergraphic.svg"); +} + +.clickable { + cursor: pointer; +}