diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bdd4e17..5bcbf68 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -13,6 +13,8 @@ import {DocumentationDatabaseComponent} from './documentation/documentation-data import {PredictionComponent} from './prediction/prediction.component'; import {ModelTemplatesComponent} from './model-templates/model-templates.component'; import {DocumentationArchitectureComponent} from './documentation/documentation-architecture/documentation-architecture.component'; +import {MaterialsComponent} from './materials/materials.component'; +import {MaterialComponent} from './material/material.component'; const routes: Routes = [ @@ -23,6 +25,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: 'materials', component: MaterialsComponent, canActivate: [LoginService]}, + {path: 'materials/edit/:id', component: MaterialComponent, canActivate: [LoginService]}, {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]}, {path: 'changelog', component: ChangelogComponent, canActivate: [LoginService]}, {path: 'users', component: UsersComponent, canActivate: [LoginService]}, diff --git a/src/app/app.component.html b/src/app/app.component.html index fd3479b..75d8741 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,6 +4,7 @@ Prediction Models Samples + Materials Templates diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c4b92f8..a2441d2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,6 +32,8 @@ import { HelpComponent } from './help/help.component'; import { ModelTemplatesComponent } from './model-templates/model-templates.component'; import { SizePipe } from './size.pipe'; import { DocumentationArchitectureComponent } from './documentation/documentation-architecture/documentation-architecture.component'; +import { MaterialsComponent } from './materials/materials.component'; +import { MaterialComponent } from './material/material.component'; @NgModule({ declarations: [ @@ -56,7 +58,9 @@ import { DocumentationArchitectureComponent } from './documentation/documentatio HelpComponent, ModelTemplatesComponent, SizePipe, - DocumentationArchitectureComponent + DocumentationArchitectureComponent, + MaterialsComponent, + MaterialComponent ], imports: [ LocalStorageModule.forRoot({ diff --git a/src/app/material/material.component.html b/src/app/material/material.component.html new file mode 100644 index 0000000..9c195ff --- /dev/null +++ b/src/app/material/material.component.html @@ -0,0 +1,65 @@ +

Edit material

+ +
+ + {{materialnameInput.errors.failure}} + + + {{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 + + + + Save material + + + Delete sample + +
+ + + + + Do you really want to delete {{material.name}}? + + + diff --git a/src/app/material/material.component.scss b/src/app/material/material.component.scss new file mode 100644 index 0000000..314fe09 --- /dev/null +++ b/src/app/material/material.component.scss @@ -0,0 +1,3 @@ +.delete-material { + float: right; +} diff --git a/src/app/material/material.component.spec.ts b/src/app/material/material.component.spec.ts new file mode 100644 index 0000000..7293b78 --- /dev/null +++ b/src/app/material/material.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MaterialComponent } from './material.component'; + +describe('MaterialComponent', () => { + let component: MaterialComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MaterialComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MaterialComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/material/material.component.ts b/src/app/material/material.component.ts new file mode 100644 index 0000000..586ff28 --- /dev/null +++ b/src/app/material/material.component.ts @@ -0,0 +1,137 @@ +import {AfterContentChecked, Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {MaterialModel} from '../models/material.model'; +import {ApiService} from '../services/api.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {DataService} from '../services/data.service'; +import strCompare from 'str-compare'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; +import {AutocompleteService} from '../services/autocomplete.service'; +import {NgForm, Validators} from '@angular/forms'; +import {ValidationService} from '../services/validation.service'; +import {ErrorComponent} from '../error/error.component'; + +@Component({ + selector: 'app-material', + templateUrl: './material.component.html', + styleUrls: ['./material.component.scss'] +}) +export class MaterialComponent implements OnInit, AfterContentChecked { + + @ViewChild('materialForm') materialForm: NgForm; + + material: MaterialModel; + materialNames: string[] = []; + + modalText = {list: '', suggestion: ''}; + loading = 0; + checkFormAfterInit = true; + + constructor( + private api: ApiService, + private route: ActivatedRoute, + public d: DataService, + private modal: ModalService, + public autocomplete: AutocompleteService, + private router: Router, + private validation: ValidationService + ) { } + + ngOnInit(): void { + this.loading = 5; + this.api.get('/material/' + this.route.snapshot.paramMap.get('id'), data => { + this.material = new MaterialModel().deserialize(data); + this.loading--; + this.d.load('materials', () => { + this.materialNames = this.d.arr.materials.map(e => e.name).filter(e => e !== this.material.name); + this.loading--; + }); + }); + this.d.load('materialSuppliers', () => { + this.loading--; + }); + this.d.load('materialGroups', () => { + this.loading--; + }); + this.d.load('materialTemplates', () => { + this.loading--; + }); + } + + ngAfterContentChecked() { + if (this.materialForm && this.material.properties.material_template) { // material template is set + this.d.id.materialTemplates[this.material.properties.material_template].parameters.forEach((parameter, i) => { + this.attachValidator(this.materialForm, 'materialParameter' + i, parameter.range); + }); + } + + if (this.checkFormAfterInit && this.materialForm !== undefined && this.materialForm.form.get('propertiesSelect')) { + this.checkFormAfterInit = false; + Object.keys(this.materialForm.form.controls).forEach(field => { + this.materialForm.form.get(field).updateValueAndValidity(); + }); + } + } + + // attach validators specified in range to input with name + attachValidator(form, name: string, range: {[prop: string]: any}) { + if (form && form.form.get(name)) { + const validators = []; + if (range.hasOwnProperty('required')) { + validators.push(Validators.required); + } + if (range.hasOwnProperty('values')) { + validators.push(this.validation.generate('stringOf', [range.values])); + } + else if (range.hasOwnProperty('min') && range.hasOwnProperty('max')) { + validators.push(this.validation.generate('minMax', [range.min, range.max])); + } + else if (range.hasOwnProperty('min')) { + validators.push(this.validation.generate('min', [range.min])); + } + else if (range.hasOwnProperty('max')) { + validators.push(this.validation.generate('max', [range.max])); + } + form.form.get(name).setValidators(validators); + } + } + + materialSave() { + this.api.put('/material/' + this.material._id, this.material.sendFormat(), () => { + this.router.navigate(['/materials']); + }); + } + + deleteConfirm(modal) { + this.modal.open(modal).then(result => { + if (result) { + this.api.delete('/material/' + this.material._id, (ignore, error) => { + if (error) { + const modalRef = this.modal.openComponent(ErrorComponent); + modalRef.instance.message = 'Cannot delete material as it is still in use!'; + } + else { + this.router.navigate(['/materials']); + } + }); + } + }); + } + + checkTypo(event, list, mKey, modal: TemplateRef) { + // user did not click on suggestion and entry is not in list + if (!(event.relatedTarget && (event.relatedTarget.className.indexOf('rb-dropdown-item') >= 0 || + event.relatedTarget.className.indexOf('close-btn rb-btn rb-passive-link') >= 0)) && + this.d.arr[list].indexOf(this.material[mKey]) < 0) { + this.modalText.list = mKey; + this.modalText.suggestion = this.d.arr[list] // find possible entry from list + .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[mKey])})) + .sort((a, b) => b.s - a.s)[0].v; + this.modal.open(modal).then(result => { + if (result) { // use suggestion + this.material[mKey] = this.modalText.suggestion; + } + }); + } + } + +} diff --git a/src/app/materials/materials.component.html b/src/app/materials/materials.component.html new file mode 100644 index 0000000..d4ee65e --- /dev/null +++ b/src/app/materials/materials.component.html @@ -0,0 +1,86 @@ +
+

Materials

+ + + {{sampleSelect ? 'Validate' : 'Validation'}} + +
+ +
+ + + validated + + + new + + + deleted + +
+ + + + + + + all + + Name + Supplier + Group + {{key.label}} + Numbers + + + + + + + + {{material.name}} + {{material.supplier}} + {{material.group}} + {{material.properties[key.key] | exists}} + {{material.numbers}} + + + + + + + + + + + + +
+ + + + of {{pages}} + + +
+
+ + + + Do you really want to restore this sample? + + diff --git a/src/app/materials/materials.component.scss b/src/app/materials/materials.component.scss new file mode 100644 index 0000000..021af3b --- /dev/null +++ b/src/app/materials/materials.component.scss @@ -0,0 +1,53 @@ +.paging { + height: 50px; + float: left; + + rb-form-input { + max-width: 65px; + } + + > * { + float: left; + } + + > button { + margin-top: 18px; + } + + > span { + margin-top: 20px; + margin-left: 5px; + } +} + +.status-selection { + overflow: hidden; + margin-bottom: 10px; + float: left; + margin-right: 15px; + + label { + display: block; + font-weight: 700; + font-size: 10px; + } + + rb-form-checkbox { + float: left; + margin-right: 10px; + margin-top: -10px; + } +} + +.header-addnew { + margin-bottom: 40px; + + & > * { + display: inline; + margin-bottom: 10px; + } + + rb-icon-button { + float: right; + } +} diff --git a/src/app/materials/materials.component.spec.ts b/src/app/materials/materials.component.spec.ts new file mode 100644 index 0000000..d89957d --- /dev/null +++ b/src/app/materials/materials.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MaterialsComponent } from './materials.component'; + +describe('MaterialsComponent', () => { + let component: MaterialsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MaterialsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MaterialsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/materials/materials.component.ts b/src/app/materials/materials.component.ts new file mode 100644 index 0000000..c6e692b --- /dev/null +++ b/src/app/materials/materials.component.ts @@ -0,0 +1,92 @@ +import { Component, OnInit } from '@angular/core'; +import {DataService} from '../services/data.service'; +import {MaterialModel} from '../models/material.model'; +import {ApiService} from '../services/api.service'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; + + +@Component({ + selector: 'app-materials', + templateUrl: './materials.component.html', + styleUrls: ['./materials.component.scss'] +}) +export class MaterialsComponent implements OnInit { + + materials: MaterialModel[] = []; + templateKeys: {key: string, label: string}[] = []; + materialStatus = {validated: true, new: true, deleted: false}; + sampleSelect = false; + + page = 1; + pages = 0; + pageSize = 25; + + constructor( + private api: ApiService, + public d: DataService, + private modal: ModalService + ) { } + + ngOnInit(): void { + this.loadMaterials(); + this.d.load('materialTemplates', () => { + this.d.arr.materialTemplates.forEach(template => { + template.parameters.forEach(parameter => { + this.templateKeys.push({key: parameter.name, label: `${this.ucFirst(template.name)} ${parameter.name}`}); + }); + }); + this.templateKeys = this.templateKeys.filter((e, i, a) => !a.slice(0, i).find(el => el.key === e.key)); + console.log(this.templateKeys); + }); + } + + loadMaterials() { + this.api.get('/materials?' + + Object.entries(this.materialStatus).filter(e => e[1]).map(e => 'status[]=' + e[0]).join('&'), data => { + this.materials = data.map(e => new MaterialModel().deserialize(e)); + this.pages = Math.ceil(this.materials.length / this.pageSize); + this.page = 1; + }); + } + + validate() { + if (this.sampleSelect) { + this.materials.forEach(sample => { + if (sample.selected) { + this.api.put('/material/validate/' + sample._id); + } + }); + this.loadMaterials(); + this.sampleSelect = false; + } + else { + this.sampleSelect = true; + } + } + + selectAll(event) { + this.materials.forEach(material => { + if (material.status !== 'deleted') { + material.selected = event.target.checked; + } + else { + material.selected = false; + } + }); + } + + restoreMaterial(id, modal) { + this.modal.open(modal).then(res => { + if (res) { + this.api.put('/sample/restore/' + id, {}, ignore => { + this.materials.find(e => e._id === id).status = 'new'; + }); + } + }); + } + + ucFirst(string) { + return string[0].toUpperCase() + string.slice(1); + } + +} diff --git a/src/app/model-templates/model-templates.component.ts b/src/app/model-templates/model-templates.component.ts index 256fd72..17e2fdb 100644 --- a/src/app/model-templates/model-templates.component.ts +++ b/src/app/model-templates/model-templates.component.ts @@ -4,6 +4,7 @@ import {ModelItemModel} from '../models/model-item.model'; import {ApiService} from '../services/api.service'; import {AutocompleteService} from '../services/autocomplete.service'; import {ModalService} from '@inst-iot/bosch-angular-ui-components'; +import omit from 'lodash/omit'; @Component({ selector: 'app-model-templates', @@ -44,7 +45,7 @@ export class ModelTemplatesComponent implements OnInit { if (this.oldModelGroup !== '' && this.modelGroup !== this.oldModelGroup) { // group was changed, delete model in old group this.delete(null, this.oldModelGroup, this.oldModelName); } - this.api.post('/model/' + this.modelGroup, this.model, () => { + this.api.post('/model/' + this.modelGroup, omit(this.model, '_id'), () => { this.newModel = false; this.loadGroups(); this.modelGroup = ''; diff --git a/src/app/models/material.model.ts b/src/app/models/material.model.ts index d69cc7f..38c6479 100644 --- a/src/app/models/material.model.ts +++ b/src/app/models/material.model.ts @@ -9,6 +9,8 @@ export class MaterialModel extends BaseModel { group = ''; properties: {material_template: string, [prop: string]: string} = {material_template: null}; numbers: string[] = ['']; + selected = false; + status = ''; sendFormat() { return pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']); diff --git a/src/app/prediction/prediction.component.html b/src/app/prediction/prediction.component.html index 46a4359..b8bd37b 100644 --- a/src/app/prediction/prediction.component.html +++ b/src/app/prediction/prediction.component.html @@ -20,7 +20,7 @@

Average result: {{result.mean}}#

- Details + Details

{{spectrumNames[i]}}: {{prediction}}# diff --git a/src/app/prediction/prediction.component.ts b/src/app/prediction/prediction.component.ts index d6cb0c2..755b039 100644 --- a/src/app/prediction/prediction.component.ts +++ b/src/app/prediction/prediction.component.ts @@ -99,6 +99,8 @@ export class PredictionComponent implements OnInit { loadPrediction() { this.loading = true; + console.log(this.activeGroup); + console.log(this.activeModelIndex); this.api.post(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => { this.result = { predictions: Object.entries(omit(data, ['mean', 'std', 'label'])) @@ -114,7 +116,9 @@ export class PredictionComponent implements OnInit { } groupChange(index) { + console.log(index); this.activeGroup = this.d.arr.modelGroups[index]; + this.activeModelIndex = 0; this.result = undefined; } diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 52d0898..3dc102c 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -1,4 +1,3 @@ -

Samples

diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index e1425a0..68dd3f4 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -17,7 +17,7 @@ export class DataService { ) { } private collectionMap = { - materials: {path: '/materials?status=all', model: MaterialModel, type: 'idArray'}, + materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'}, materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'}, materialGroups: {path: '/material/groups', model: null, type: 'idArray'}, materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'}, diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index ace657b..2d5330b 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -11,6 +11,7 @@ import {DataService} from './data.service'; export class LoginService implements CanActivate { private pathPermissions = [ + {path: 'materials', permission: 'dev'}, {path: 'templates', permission: 'dev'}, {path: 'changelog', permission: 'dev'}, {path: 'users', permission: 'admin'} diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts index 8584154..bcefafa 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -71,6 +71,14 @@ export class ValidationService { return {ok: true, error: ''}; } + stringNin(data, list) { + const {ignore, error} = Joi.string().invalid(...list).validate(data); + if (error) { + return {ok: false, error: 'value not allowed'}; + } + return {ok: true, error: ''}; + } + stringLength(data, length) { const {ignore, error} = Joi.string().max(length).allow('').validate(data); if (error) {