Merge pull request #33 in ~VLE2FE/definma-ui from development to master

* commit '250b04e096b8a476af8608c571f43a2636897a6a':
  material and activeModelIndex fix
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-08-31 16:15:22 +02:00
commit b814335130
19 changed files with 515 additions and 5 deletions

View File

@ -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]},

View File

@ -4,6 +4,7 @@
<a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Prediction</a>
<a routerLink="/models" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Models</a>
<a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.read">Samples</a>
<a routerLink="/materials" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Materials</a>
<a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">
Templates
</a>

View File

@ -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({

View File

@ -0,0 +1,65 @@
<h2>Edit material</h2>
<form #materialForm="ngForm" *ngIf="!loading">
<rb-form-input name="materialname" label="material name" appValidate="stringNin" [appValidateArgs]="[materialNames]"
required [(ngModel)]="material.name" #materialnameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{materialnameInput.errors.failure}}</ng-template>
</rb-form-input>
<rb-form-input name="supplier" label="supplier"
[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.supplier" #supplierInput="ngModel"
(focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
title="material supplier, eg. BASF">
<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
</rb-form-input>
<rb-form-input name="group" label="group"
[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.group" #groupInput="ngModel"
(focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
title="chemical material type, eg. PA66">
<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
</rb-form-input>
<ng-template #modalWarning>
<rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
The specified {{modalText.list}} could not be found in the list. <br>
Did you mean {{modalText.suggestion}}?
</rb-alert>
</ng-template>
<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
[ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
</rb-array-input>
<rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
[(ngModel)]="material.properties.material_template">
<option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
</rb-form-select>
<rb-form-input *ngFor="let parameter of
d.id.materialTemplates[material.properties.material_template].parameters;
index as i" [name]="'materialParameter' + i"
[label]="parameter.name" appValidate="string" required
[(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
<rb-icon-button icon="save" mode="primary" type="submit" (click)="materialSave()"
[disabled]="materialForm.form.invalid">
Save material
</rb-icon-button>
<rb-icon-button class="delete-material" icon="delete" mode="danger" (click)="deleteConfirm(modalDeleteConfirm)">
Delete sample
</rb-icon-button>
</form>
<ng-template #modalDeleteConfirm>
<rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete material'"
cancelBtnLabel="Cancel">
Do you really want to delete {{material.name}}?
</rb-alert>
</ng-template>

View File

@ -0,0 +1,3 @@
.delete-material {
float: right;
}

View File

@ -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<MaterialComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MaterialComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MaterialComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<MaterialModel>('/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<any>) {
// 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;
}
});
}
}
}

View File

@ -0,0 +1,86 @@
<div class="header-addnew">
<h2>Materials</h2>
<rb-icon-button *ngIf="sampleSelect" mode="secondary" icon="close" (click)="sampleSelect = false"
class="validation-close" iconOnly></rb-icon-button>
<rb-icon-button [icon]="sampleSelect ? 'checkmark' : 'clear-all'"
mode="secondary" (click)="validate()">
{{sampleSelect ? 'Validate' : 'Validation'}}
</rb-icon-button>
</div>
<div class="status-selection">
<label class="label">Status</label>
<rb-form-checkbox name="status-validated" [(ngModel)]="materialStatus.validated"
[disabled]="!materialStatus.new && !materialStatus.deleted"
(ngModelChange)="loadMaterials()">
validated
</rb-form-checkbox>
<rb-form-checkbox name="status-new" [(ngModel)]="materialStatus.new"
[disabled]="!materialStatus.validated && !materialStatus.deleted"
(ngModelChange)="loadMaterials()">
new
</rb-form-checkbox>
<rb-form-checkbox name="status-deleted" [(ngModel)]="materialStatus.deleted"
[disabled]="!materialStatus.validated && !materialStatus.new"
(ngModelChange)="loadMaterials()">
deleted
</rb-form-checkbox>
</div>
<ng-container *ngTemplateOutlet="paging"></ng-container>
<rb-table ellipsis scrollTop>
<tr>
<th *ngIf="sampleSelect">
<rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
</th>
<th>Name</th>
<th>Supplier</th>
<th>Group</th>
<th *ngFor="let key of templateKeys">{{key.label}}</th>
<th>Numbers</th>
<th></th>
</tr>
<tr *ngFor="let material of (materials || []).slice((page - 1) * pageSize, page * pageSize); index as i">
<td *ngIf="sampleSelect">
<rb-form-checkbox *ngIf="material.status !== 'deleted'" [name]="'validate-' + i"
[(ngModel)]="material.selected">
</rb-form-checkbox>
</td>
<td>{{material.name}}</td>
<td>{{material.supplier}}</td>
<td>{{material.group}}</td>
<td *ngFor="let key of templateKeys">{{material.properties[key.key] | exists}}</td>
<td>{{material.numbers}}</td>
<td>
<a [routerLink]="'/materials/edit/' + material._id" *ngIf="material.status !== 'deleted'">
<span class="rb-ic rb-ic-edit clickable"></span>
</a>
<span class="rb-ic rb-ic-undo clickable" *ngIf="material.status === 'deleted'"
(click)="restoreMaterial(material._id, restoreConfirm)"></span>
</td>
</tr>
</rb-table>
<ng-container *ngTemplateOutlet="paging"></ng-container>
<ng-template #paging>
<div class="paging">
<button class="rb-btn rb-link" type="button" (click)="page = page - 1" [disabled]="page === 1">
<span class="rb-ic rb-ic-back-left"></span>
</button>
<rb-form-input label="page" [(ngModel)]="page"></rb-form-input>
<span>
of {{pages}}
</span>
<button class="rb-btn rb-link" type="button" (click)="page = page + 1" [disabled]="page >= pages">
<span class="rb-ic rb-ic-forward-right"></span>
</button>
</div>
</ng-template>
<ng-template #restoreConfirm>
<rb-dialog dialogTitle="Restore sample">
Do you really want to restore this sample?
</rb-dialog>
</ng-template>

View File

@ -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;
}
}

View File

@ -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<MaterialsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MaterialsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MaterialsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<MaterialModel[]>('/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);
}
}

View File

@ -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 = '';

View File

@ -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']);

View File

@ -20,7 +20,7 @@
<h4>
Average result: {{result.mean}}<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
</h4>
<a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
<a class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
<div *ngIf="triggerDetails.open" class="space-below">
<p *ngFor="let prediction of result.predictions; index as i">
{{spectrumNames[i]}}: {{prediction}}<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>

View File

@ -99,6 +99,8 @@ export class PredictionComponent implements OnInit {
loadPrediction() {
this.loading = true;
console.log(this.activeGroup);
console.log(this.activeModelIndex);
this.api.post<any>(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;
}

View File

@ -1,4 +1,3 @@
<script src="samples.component.ts"></script>
<div class="header-addnew">
<h2>Samples</h2>
<a routerLink="/samples/new" *ngIf="login.isLevel.write">

View File

@ -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'},

View File

@ -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'}

View File

@ -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) {