Merge pull request #50 in DEFINMA/definma-ui from proper-indentation to master

* commit '2f6231a6b5c5f0639341c4aac8fefd9bbbc77adf':
  Properly indent all source files
This commit is contained in:
Kai S. K. Engelbart 2021-01-25 12:56:14 +01:00
commit 29b8d3b922
106 changed files with 5170 additions and 5171 deletions

View File

@ -13,43 +13,43 @@ import {DocumentationDatabaseComponent} from './documentation/documentation-data
import {PredictionComponent} from './prediction/prediction.component'; import {PredictionComponent} from './prediction/prediction.component';
import {ModelTemplatesComponent} from './model-templates/model-templates.component'; import {ModelTemplatesComponent} from './model-templates/model-templates.component';
import {DocumentationArchitectureComponent} from import {DocumentationArchitectureComponent} from
'./documentation/documentation-architecture/documentation-architecture.component'; './documentation/documentation-architecture/documentation-architecture.component';
import {MaterialsComponent} from './materials/materials.component'; import {MaterialsComponent} from './materials/materials.component';
import {MaterialComponent} from './material/material.component'; import {MaterialComponent} from './material/material.component';
import {DocumentationModelsComponent} from './documentation/documentation-models/documentation-models.component'; import {DocumentationModelsComponent} from './documentation/documentation-models/documentation-models.component';
const routes: Routes = [ const routes: Routes = [
{path: '', component: HomeComponent}, {path: '', component: HomeComponent},
{path: 'home', component: HomeComponent}, {path: 'home', component: HomeComponent},
{path: 'prediction', component: PredictionComponent}, {path: 'prediction', component: PredictionComponent},
{path: 'models', component: ModelTemplatesComponent}, {path: 'models', component: ModelTemplatesComponent},
{path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, {path: 'samples', component: SamplesComponent, canActivate: [LoginService]},
{path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]},
{path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]},
{path: 'materials', component: MaterialsComponent, canActivate: [LoginService]}, {path: 'materials', component: MaterialsComponent, canActivate: [LoginService]},
{path: 'materials/edit/:id', component: MaterialComponent, canActivate: [LoginService]}, {path: 'materials/edit/:id', component: MaterialComponent, canActivate: [LoginService]},
{path: 'templates', component: TemplatesComponent, canActivate: [LoginService]}, {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]},
{path: 'changelog', component: ChangelogComponent, canActivate: [LoginService]}, {path: 'changelog', component: ChangelogComponent, canActivate: [LoginService]},
{path: 'users', component: UsersComponent, canActivate: [LoginService]}, {path: 'users', component: UsersComponent, canActivate: [LoginService]},
{path: 'settings', component: SettingsComponent, canActivate: [LoginService]}, {path: 'settings', component: SettingsComponent, canActivate: [LoginService]},
{path: 'documentation', component: DocumentationComponent}, {path: 'documentation', component: DocumentationComponent},
{path: 'documentation/architecture', component: DocumentationArchitectureComponent}, {path: 'documentation/architecture', component: DocumentationArchitectureComponent},
{path: 'documentation/database', component: DocumentationDatabaseComponent}, {path: 'documentation/database', component: DocumentationDatabaseComponent},
{path: 'documentation/models', component: DocumentationModelsComponent}, {path: 'documentation/models', component: DocumentationModelsComponent},
// If not authenticated // If not authenticated
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }
]; ];
const routerOptions: ExtraOptions = { const routerOptions: ExtraOptions = {
scrollPositionRestoration: 'enabled', scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled', anchorScrolling: 'enabled',
scrollOffset: [0, 64], scrollOffset: [0, 64],
}; };
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, routerOptions)], imports: [RouterModule.forRoot(routes, routerOptions)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule { } export class AppRoutingModule { }

View File

@ -1,73 +1,73 @@
<rb-full-header id="top"> <rb-full-header id="top">
<nav *rbMainNavItems> <nav *rbMainNavItems>
<a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a> <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
<a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.hasPrediction">Prediction</a> <a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.hasPrediction">Prediction</a>
<a routerLink="/models" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Models</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="/samples" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.read">Samples</a>
<a routerLink="/materials" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Materials</a> <a routerLink="/materials" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Materials</a>
<a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev"> <a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">
Templates Templates
</a> </a>
<a routerLink="/changelog" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Changelog</a> <a routerLink="/changelog" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Changelog</a>
<a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.admin">Users</a> <a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.admin">Users</a>
<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a> <a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
</nav> </nav>
<ng-container *ngIf="isDocumentation"> <ng-container *ngIf="isDocumentation">
<nav *rbSubNavItems> <nav *rbSubNavItems>
<a routerLink="/documentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">General</a> <a routerLink="/documentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">General</a>
<a routerLink="/documentation/architecture" routerLinkActive="active">Architecture</a> <a routerLink="/documentation/architecture" routerLinkActive="active">Architecture</a>
<a routerLink="/documentation/database" routerLinkActive="active">Database</a> <a routerLink="/documentation/database" routerLinkActive="active">Database</a>
<a routerLink="/documentation/models" routerLinkActive="active">Models</a> <a routerLink="/documentation/models" routerLinkActive="active">Models</a>
</nav> </nav>
</ng-container> </ng-container>
<nav *rbMetaNavItems> <nav *rbMetaNavItems>
<span class="rb-ic rb-ic-question-frame clickable space-above" (click)="help()"></span> <span class="rb-ic rb-ic-question-frame clickable space-above" (click)="help()"></span>
</nav> </nav>
<ng-container *ngIf="login.isLoggedIn"> <ng-container *ngIf="login.isLoggedIn">
<nav *rbActionNavItems> <nav *rbActionNavItems>
<a href="javascript:" [rbPopover]="userPopover" [anchor]="popoverAnchor"> <a href="javascript:" [rbPopover]="userPopover" [anchor]="popoverAnchor">
{{login.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a> {{login.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a>
</nav> </nav>
<ng-template #userPopover let-close="close"> <ng-template #userPopover let-close="close">
<div class="spacing"> <div class="spacing">
<a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span>&nbsp;&nbsp;Settings</a> <a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span>&nbsp;&nbsp;Settings</a>
<button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button> <button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button>
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
<div *rbSubBrandHeader> <div *rbSubBrandHeader>
<rb-icon-button icon="bug" mode="secondary" class="space-right" [rbModal]="bugModal"> <rb-icon-button icon="bug" mode="secondary" class="space-right" [rbModal]="bugModal">
Bug Bug
</rb-icon-button> </rb-icon-button>
<ng-template let-close="close" #bugModal> <ng-template let-close="close" #bugModal>
<h3>Report a bug</h3> <h3>Report a bug</h3>
<rb-form-textarea class="bug-textarea" label="What did you do?" [(ngModel)]="bugReport.do"></rb-form-textarea> <rb-form-textarea class="bug-textarea" label="What did you do?" [(ngModel)]="bugReport.do"></rb-form-textarea>
<rb-form-textarea class="bug-textarea" label="What did not work?" [(ngModel)]="bugReport.work"></rb-form-textarea> <rb-form-textarea class="bug-textarea" label="What did not work?" [(ngModel)]="bugReport.work"></rb-form-textarea>
<a [href]="bugReportContent()"> <a [href]="bugReportContent()">
<rb-icon-button icon="mail" mode="primary" (mouseup)="closeBugReport(close)">Send report</rb-icon-button> <rb-icon-button icon="mail" mode="primary" (mouseup)="closeBugReport(close)">Send report</rb-icon-button>
</a> </a>
</ng-template> </ng-template>
<span class="dev-label" *ngIf="devMode">DEVELOPMENT</span> <span class="dev-label" *ngIf="devMode">DEVELOPMENT</span>
DeFinMa DeFinMa
</div> </div>
</rb-full-header> </rb-full-header>
<div class="container"> <div class="container">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
<rb-icon-button icon="up" mode="primary" iconOnly (click)="toTheTop()" *ngIf="window.scrollY > 300" class="to-the-top"> <rb-icon-button icon="up" mode="primary" iconOnly (click)="toTheTop()" *ngIf="window.scrollY > 300" class="to-the-top">
</rb-icon-button> </rb-icon-button>
<rb-footer-nav> <rb-footer-nav>
<span class="copyright"> <span class="copyright">
CR/APS1 and CR/ANA1 2020 CR/APS1 and CR/ANA1 2020
</span> </span>
<nav> <nav>
<a [href]="'mailto:' + d.contact.mail">Contact</a> <a [href]="'mailto:' + d.contact.mail">Contact</a>
</nav> </nav>
</rb-footer-nav> </rb-footer-nav>

View File

@ -1,26 +1,26 @@
.dev-label { .dev-label {
color: #F00; color: #F00;
font-size: 32px; font-size: 32px;
margin-right: 40px; margin-right: 40px;
} }
.spacing { .spacing {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
.bug-textarea { .bug-textarea {
width: 800px; width: 800px;
} }
.container { .container {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.to-the-top { .to-the-top {
position: fixed; position: fixed;
right: 1rem; right: 1rem;
bottom: 20px; bottom: 20px;
} }

View File

@ -8,76 +8,76 @@ import {DataService} from './services/data.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent implements OnInit{ export class AppComponent implements OnInit{
bugReport = {do: '', work: ''}; // Data from bug report inputs bugReport = {do: '', work: ''}; // Data from bug report inputs
isDocumentation = false; // True if user is on documentation pages isDocumentation = false; // True if user is on documentation pages
devMode = false; devMode = false;
constructor( constructor(
public login: LoginService, public login: LoginService,
public router: Router, public router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
public window: Window, public window: Window,
private modal: ModalService, private modal: ModalService,
public d: DataService public d: DataService
) { ) {
this.devMode = isDevMode(); this.devMode = isDevMode();
this.router.events.subscribe(event => { this.router.events.subscribe(event => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
this.isDocumentation = /\/documentation/.test(event.url); this.isDocumentation = /\/documentation/.test(event.url);
} }
}); });
} }
ngOnInit() { ngOnInit() {
// Try to log in user // Try to log in user
this.login.login().then(res => { this.login.login().then(res => {
// Return to home page if log failed, except when on documentation pages // Return to home page if log failed, except when on documentation pages
if (!res && !(/\/documentation/.test(this.router.url))) { if (!res && !(/\/documentation/.test(this.router.url))) {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
}); });
} }
logout() { logout() {
this.login.logout(); this.login.logout();
this.router.navigate(['/']); this.router.navigate(['/']);
} }
help() { help() {
this.modal.openComponent(HelpComponent); this.modal.openComponent(HelpComponent);
} }
bugReportContent() { bugReportContent() {
return `mailto:${this.d.contact.mail}?subject=Bug report&body=Thanks for sending the report! Your bug will be return `mailto:${this.d.contact.mail}?subject=Bug report&body=Thanks for sending the report! Your bug will be
(hopefully) fixed soon. (hopefully) fixed soon.
%0D%0A%0D%0A--- REPORT DATA --- %0D%0A%0D%0A--- REPORT DATA ---
%0D%0A%0D%0ATime: ${new Date().toString()}%0D%0A %0D%0A%0D%0ATime: ${new Date().toString()}%0D%0A
URL: ${this.window.location}%0D%0A%0D%0AWhat did you do?%0D%0A${encodeURIComponent(this.bugReport.do)} URL: ${this.window.location}%0D%0A%0D%0AWhat did you do?%0D%0A${encodeURIComponent(this.bugReport.do)}
%0D%0A%0D%0AWhat did not work?%0D%0A${encodeURIComponent(this.bugReport.work)}%0D%0A%0D%0ABrowser:%0D%0A %0D%0A%0D%0AWhat did not work?%0D%0A${encodeURIComponent(this.bugReport.work)}%0D%0A%0D%0ABrowser:%0D%0A
%0D%0AappCodeName: ${navigator.appCodeName} %0D%0AappCodeName: ${navigator.appCodeName}
%0D%0AappVersion: ${navigator.appVersion} %0D%0AappVersion: ${navigator.appVersion}
%0D%0Alanguage: ${navigator.language} %0D%0Alanguage: ${navigator.language}
%0D%0AonLine: ${navigator.onLine} %0D%0AonLine: ${navigator.onLine}
%0D%0Aoscpu: ${navigator.oscpu} %0D%0Aoscpu: ${navigator.oscpu}
%0D%0Aplatform: ${navigator.platform} %0D%0Aplatform: ${navigator.platform}
%0D%0AuserAgent: ${navigator.userAgent} %0D%0AuserAgent: ${navigator.userAgent}
%0D%0AinnerWidth: ${this.window.innerWidth} %0D%0AinnerWidth: ${this.window.innerWidth}
%0D%0AinnerHeight: ${this.window.innerHeight}`; %0D%0AinnerHeight: ${this.window.innerHeight}`;
} }
closeBugReport(close) { closeBugReport(close) {
setTimeout(() => close(), 1); setTimeout(() => close(), 1);
} }
toTheTop() { toTheTop() {
this.window.scroll(0, 0); this.window.scroll(0, 0);
} }
} }

View File

@ -27,66 +27,66 @@ import { SettingsComponent } from './settings/settings.component';
import { UsersComponent } from './users/users.component'; import { UsersComponent } from './users/users.component';
import { ChangelogComponent } from './changelog/changelog.component'; import { ChangelogComponent } from './changelog/changelog.component';
import { DocumentationDatabaseComponent } from import { DocumentationDatabaseComponent } from
'./documentation/documentation-database/documentation-database.component'; './documentation/documentation-database/documentation-database.component';
import { PredictionComponent } from './prediction/prediction.component'; import { PredictionComponent } from './prediction/prediction.component';
import { HelpComponent } from './help/help.component'; import { HelpComponent } from './help/help.component';
import { ModelTemplatesComponent } from './model-templates/model-templates.component'; import { ModelTemplatesComponent } from './model-templates/model-templates.component';
import { SizePipe } from './size.pipe'; import { SizePipe } from './size.pipe';
import { DocumentationArchitectureComponent } from import { DocumentationArchitectureComponent } from
'./documentation/documentation-architecture/documentation-architecture.component'; './documentation/documentation-architecture/documentation-architecture.component';
import { MaterialsComponent } from './materials/materials.component'; import { MaterialsComponent } from './materials/materials.component';
import { MaterialComponent } from './material/material.component'; import { MaterialComponent } from './material/material.component';
import { DocumentationModelsComponent } from './documentation/documentation-models/documentation-models.component'; import { DocumentationModelsComponent } from './documentation/documentation-models/documentation-models.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
LoginComponent, LoginComponent,
HomeComponent, HomeComponent,
SamplesComponent, SamplesComponent,
SampleComponent, SampleComponent,
ValidateDirective, ValidateDirective,
ErrorComponent, ErrorComponent,
ObjectPipe, ObjectPipe,
DocumentationComponent, DocumentationComponent,
ImgMagnifierComponent, ImgMagnifierComponent,
ExistsPipe, ExistsPipe,
TemplatesComponent, TemplatesComponent,
ParametersPipe, ParametersPipe,
SettingsComponent, SettingsComponent,
UsersComponent, UsersComponent,
ChangelogComponent, ChangelogComponent,
DocumentationDatabaseComponent, DocumentationDatabaseComponent,
PredictionComponent, PredictionComponent,
HelpComponent, HelpComponent,
ModelTemplatesComponent, ModelTemplatesComponent,
SizePipe, SizePipe,
DocumentationArchitectureComponent, DocumentationArchitectureComponent,
MaterialsComponent, MaterialsComponent,
MaterialComponent, MaterialComponent,
DocumentationModelsComponent DocumentationModelsComponent
], ],
imports: [ imports: [
LocalStorageModule.forRoot({ LocalStorageModule.forRoot({
prefix: 'definma', prefix: 'definma',
storageType: 'localStorage' storageType: 'localStorage'
}), }),
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
AppRoutingModule, AppRoutingModule,
RbUiComponentsModule, RbUiComponentsModule,
FormsModule, FormsModule,
HttpClientModule, HttpClientModule,
RbCustomInputsModule, RbCustomInputsModule,
ReactiveFormsModule, ReactiveFormsModule,
FormFieldsModule, FormFieldsModule,
CommonModule, CommonModule,
ChartsModule ChartsModule
], ],
providers: [ providers: [
ModalService, ModalService,
{ provide: Window, useValue: window } { provide: Window, useValue: window }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }

View File

@ -1,46 +1,46 @@
<div class="header"> <div class="header">
<rb-form-date-input name="dateInput" label="older than" [options]="{enableTime: true}" <rb-form-date-input name="dateInput" label="older than" [options]="{enableTime: true}"
[(ngModel)]="timestamp" (ngModelChange)="loadChangelog()"> [(ngModel)]="timestamp" (ngModelChange)="loadChangelog()">
</rb-form-date-input> </rb-form-date-input>
<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="pageSize" (ngModelChange)="loadChangelog()"> <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="pageSize" (ngModelChange)="loadChangelog()">
<option value="3">3</option> <option value="3">3</option>
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
<option value="250">250</option> <option value="250">250</option>
<option value="500">500</option> <option value="500">500</option>
</rb-form-select> </rb-form-select>
<button class="rb-btn rb-link" type="button" (click)="loadChangelog(1)"> <button class="rb-btn rb-link" type="button" (click)="loadChangelog(1)">
next page next page
<span class="rb-ic rb-ic-forward-right"></span> <span class="rb-ic rb-ic-forward-right"></span>
</button> </button>
</div> </div>
<rb-table ellipsis> <rb-table ellipsis>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Action</th> <th>Action</th>
<th>Data</th> <th>Data</th>
</tr> </tr>
<tr *ngFor="let entry of changelog; index as i" class="clickable" (click)="showDetails(i, sampleModal)"> <tr *ngFor="let entry of changelog; index as i" class="clickable" (click)="showDetails(i, sampleModal)">
<td>{{entry.date}}</td> <td>{{entry.date}}</td>
<td>{{entry.action}}</td> <td>{{entry.action}}</td>
<td>{{entry.data | json}}</td> <td>{{entry.data | json}}</td>
</tr> </tr>
</rb-table> </rb-table>
<ng-template #sampleModal> <ng-template #sampleModal>
<h4>Date</h4> <h4>Date</h4>
<p class="space-below">{{changelog[modalDetail].date}}</p> <p class="space-below">{{changelog[modalDetail].date}}</p>
<h4>Action</h4> <h4>Action</h4>
<p class="space-below">{{changelog[modalDetail].action}}</p> <p class="space-below">{{changelog[modalDetail].action}}</p>
<h4>Collection</h4> <h4>Collection</h4>
<p class="space-below">{{changelog[modalDetail].collection}}</p> <p class="space-below">{{changelog[modalDetail].collection}}</p>
<h4>Conditions</h4> <h4>Conditions</h4>
<p class="space-below">{{changelog[modalDetail].conditions | json}}</p> <p class="space-below">{{changelog[modalDetail].conditions | json}}</p>
<h4>Data</h4> <h4>Data</h4>
<p class="space-below">{{changelog[modalDetail].data | json}}</p> <p class="space-below">{{changelog[modalDetail].data | json}}</p>
</ng-template> </ng-template>

View File

@ -2,20 +2,20 @@
.header { .header {
& > * { & > * {
float: left; float: left;
} }
button { button {
float: right; float: right;
} }
} }
tr.clickable { tr.clickable {
background: none; background: none;
transition: background-color 0.5s; transition: background-color 0.5s;
&:hover { &:hover {
background: $color-gray-mercury; background: $color-gray-mercury;
} }
} }

View File

@ -4,44 +4,44 @@ import {ApiService} from '../services/api.service';
import {ModalService} from '@inst-iot/bosch-angular-ui-components'; import {ModalService} from '@inst-iot/bosch-angular-ui-components';
@Component({ @Component({
selector: 'app-changelog', selector: 'app-changelog',
templateUrl: './changelog.component.html', templateUrl: './changelog.component.html',
styleUrls: ['./changelog.component.scss'] styleUrls: ['./changelog.component.scss']
}) })
export class ChangelogComponent implements OnInit { export class ChangelogComponent implements OnInit {
timestamp = new Date(); // Time from date input timestamp = new Date(); // Time from date input
pageSize = 25; pageSize = 25;
changelog: ChangelogModel[] = []; changelog: ChangelogModel[] = [];
modalDetail = 0; // Index of changelog element to show details of modalDetail = 0; // Index of changelog element to show details of
constructor( constructor(
private api: ApiService, private api: ApiService,
private modal: ModalService private modal: ModalService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadChangelog(); this.loadChangelog();
} }
loadChangelog(page = 0) { // Load changelog with page no relative to current page loadChangelog(page = 0) { // Load changelog with page no relative to current page
this.api.get<ChangelogModel[]>(`/changelog/${ this.api.get<ChangelogModel[]>(`/changelog/${
page > 0 ? this.changelog[0]._id : // Use id if no new date was given page > 0 ? this.changelog[0]._id : // Use id if no new date was given
Math.floor(new Date( Math.floor(new Date(
new Date(this.timestamp).getTime() - new Date(this.timestamp).getTimezoneOffset() * 60000 // Adjust timezone new Date(this.timestamp).getTime() - new Date(this.timestamp).getTimezoneOffset() * 60000 // Adjust timezone
).getTime() / 1000).toString(16) + '0000000000000000' // Id from time ).getTime() / 1000).toString(16) + '0000000000000000' // Id from time
}/${page}/${this.pageSize}`, data => { }/${page}/${this.pageSize}`, data => {
this.changelog = data.map(e => new ChangelogModel().deserialize(e)); this.changelog = data.map(e => new ChangelogModel().deserialize(e));
if (page) { // Adjust date picker to new first element when user clicked on next page if (page) { // Adjust date picker to new first element when user clicked on next page
this.timestamp = new Date(this.changelog[0].date); this.timestamp = new Date(this.changelog[0].date);
} }
}); });
} }
// Show details of a changelog element with reference to needed modal // Show details of a changelog element with reference to needed modal
showDetails(i: number, modal: TemplateRef<any>) { showDetails(i: number, modal: TemplateRef<any>) {
this.modalDetail = i; this.modalDetail = i;
this.modal.open(modal).then(() => {}); this.modal.open(modal).then(() => {});
} }
} }

View File

@ -1,50 +1,50 @@
<p> <p>
Bosch IoT Cloud space where all applications are hosted: Bosch IoT Cloud space where all applications are hosted:
<a href="https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2"> <a href="https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2">
https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2 https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2
</a><br> </a><br>
Find the API documentation here: Find the API documentation here:
<a href="https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/"> <a href="https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/">
https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/ https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/
</a><br> </a><br>
Admin database management page: Admin database management page:
<a href="https://definma-db.apps.de1.bosch-iot-cloud.com/"> <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/">
https://definma-db.apps.de1.bosch-iot-cloud.com/ https://definma-db.apps.de1.bosch-iot-cloud.com/
</a><br> </a><br>
Code repository UI Code repository UI
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui"> <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui">
https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui
</a><br> </a><br>
Code repository API Code repository API
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api"> <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api">
https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api
</a><br> </a><br>
Code repository Models Code repository Models
<a href="https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models"> <a href="https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models">
https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models
</a><br> </a><br>
</p> </p>
<p> <p>
All applications are hosted in the bosch IoT Cloud. The API is hosted in a Node.js container, with a bound MongoDB All applications are hosted in the bosch IoT Cloud. The API is hosted in a Node.js container, with a bound MongoDB
instance. <br> instance. <br>
The Angular UI is hosted in a staticfile container, serving the built Angular bundle. <br> The Angular UI is hosted in a staticfile container, serving the built Angular bundle. <br>
Finally any machine learning models are hosted in Python containers. Finally any machine learning models are hosted in Python containers.
</p> </p>
<h4>Development setup</h4> <h4>Development setup</h4>
<p> <p>
In any case, it is good to have PxProxy working, following the In any case, it is good to have PxProxy working, following the
<a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=608652557"> <a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=608652557">
Bosch installation guide Bosch installation guide
</a>. <br> </a>. <br>
For pushing to the BIC, you need the CloudFoundry CLI as described in the For pushing to the BIC, you need the CloudFoundry CLI as described in the
<a href="https://inside-docupedia.bosch.com/confluence/display/BICI/Cloud+Foundry+CLI">BIC documentation</a>. <a href="https://inside-docupedia.bosch.com/confluence/display/BICI/Cloud+Foundry+CLI">BIC documentation</a>.
If the the downloaded version of the CLI should not work, there is an old installer with a definitely working version If the the downloaded version of the CLI should not work, there is an old installer with a definitely working version
on the file share in the bin folder. on the file share in the bin folder.
Please consider that you need to have a login to the BIC and be marked as space developer for the DeFinMa Org. <br> Please consider that you need to have a login to the BIC and be marked as space developer for the DeFinMa Org. <br>
For front and back end development, you need a For front and back end development, you need a
<a href="https://www.mongodb.com/try/download/community">local MongoDB server</a> as well as <a href="https://www.mongodb.com/try/download/community">local MongoDB server</a> as well as
<a href="https://nodejs.org/en/">Node.js</a>. <a href="https://nodejs.org/en/">Node.js</a>.
</p> </p>

View File

@ -1,15 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-documentation-architecture', selector: 'app-documentation-architecture',
templateUrl: './documentation-architecture.component.html', templateUrl: './documentation-architecture.component.html',
styleUrls: ['./documentation-architecture.component.scss'] styleUrls: ['./documentation-architecture.component.scss']
}) })
export class DocumentationArchitectureComponent implements OnInit { export class DocumentationArchitectureComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -1,38 +1,38 @@
<p> <p>
The used database instance is a MongoDB instance running on the BIC, storing all application data. The used database instance is a MongoDB instance running on the BIC, storing all application data.
The admin database management page can be accessed <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/api-doc/">here</a>. The admin database management page can be accessed <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/api-doc/">here</a>.
However, it is recommended to use a dedicated MongoDB application for anything apart from a quick look. However, it is recommended to use a dedicated MongoDB application for anything apart from a quick look.
The two tested applications are <a href="https://www.mongodb.com/products/compass">MongoDB Compass</a> and The two tested applications are <a href="https://www.mongodb.com/products/compass">MongoDB Compass</a> and
<a href="https://robomongo.org/s">Robo 3T</a> (recommended for trying queries). <a href="https://robomongo.org/s">Robo 3T</a> (recommended for trying queries).
</p> </p>
<p> <p>
To connect to the BIC instance from your local application follow the To connect to the BIC instance from your local application follow the
<a href="https://inside-docupedia.bosch.com/confluence/display/BICI/MongoDB+Client+Tool+via+SSH+Tunnel">BIC guide</a>. <a href="https://inside-docupedia.bosch.com/confluence/display/BICI/MongoDB+Client+Tool+via+SSH+Tunnel">BIC guide</a>.
<br> <br>
TL;DR: TL;DR:
</p> </p>
<p>For the first time:</p> <p>For the first time:</p>
<ul> <ul>
<li>cf login</li> <li>cf login</li>
<li>cf enable-ssh definma-api</li> <li>cf enable-ssh definma-api</li>
<li>cf create-service-key definmadb &lt;key name, can be whatever&gt;</li> <li>cf create-service-key definmadb &lt;key name, can be whatever&gt;</li>
</ul> </ul>
<p>Every time:</p> <p>Every time:</p>
<ul> <ul>
<li>cf ssh -L 1120:st0cvm200118.internal-mongodb.de1.bosch-iot-cloud.com:30000 definma-api</li> <li>cf ssh -L 1120:st0cvm200118.internal-mongodb.de1.bosch-iot-cloud.com:30000 definma-api</li>
<li> <li>
Login into your application with localhost:1120 and use credentials and authentication database from the BIC Login into your application with localhost:1120 and use credentials and authentication database from the BIC
(Data can be found at Home &gt; DeFinMa &gt; production &gt; definma-api &gt; Services &gt; dots on the right of definmadb &gt; View Credentials) (Data can be found at Home &gt; DeFinMa &gt; production &gt; definma-api &gt; Services &gt; dots on the right of definmadb &gt; View Credentials)
</li> </li>
</ul> </ul>
<h4>Backup Instructions</h4> <h4>Backup Instructions</h4>
<p> <p>
For creating a database backup, you must follow the same steps from above (except the last one). For creating a database backup, you must follow the same steps from above (except the last one).
Additionally you need the <a href="https://www.mongodb.com/try/download/database-tools">MongoDB Tools</a> installed. Additionally you need the <a href="https://www.mongodb.com/try/download/database-tools">MongoDB Tools</a> installed.
Extract the ZIP file to a location on your hard drive and launch a Git Bash in that directory. Extract the ZIP file to a location on your hard drive and launch a Git Bash in that directory.
</p> </p>
<p>To back up the database from the BIC:</p> <p>To back up the database from the BIC:</p>
@ -51,377 +51,377 @@
</pre> </pre>
<p> <p>
More information can be found inside the More information can be found inside the
<a href="https://docs.mongodb.com/database-tools/">documentation</a>. <br> <a href="https://docs.mongodb.com/database-tools/">documentation</a>. <br>
The BIC service also creates an internal backup, which can be requested to restore, see The BIC service also creates an internal backup, which can be requested to restore, see
<a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=565402281">MongoDB FAQ</a> <a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=565402281">MongoDB FAQ</a>
</p> </p>
<h4>Database Model</h4> <h4>Database Model</h4>
<app-img-magnifier src="assets/imgs/db_structure_latest.svg" zoom="2" [magnifierSize]="{width: 400, height: 300}" <app-img-magnifier src="assets/imgs/db_structure_latest.svg" zoom="2" [magnifierSize]="{width: 400, height: 300}"
id="db-structure"></app-img-magnifier> id="db-structure"></app-img-magnifier>
<h4>Field Reference</h4> <h4>Field Reference</h4>
<rb-table class="field-reference"> <rb-table class="field-reference">
<tr><th>samples</th><th></th><th>Example</th></tr> <tr><th>samples</th><th></th><th>Example</th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63c98d1c020f8cda6e06'</td> <td>'5f2e63c98d1c020f8cda6e06'</td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td> <td>
The material status of the sample, can be either <span class="name">as-delivered/raw</span> or The material status of the sample, can be either <span class="name">as-delivered/raw</span> or
<span class="name">processed</span>. <span class="name">processed</span>.
</td> </td>
<td>'processed'</td> <td>'processed'</td>
</tr> </tr>
<tr> <tr>
<td>number</td> <td>number</td>
<td>The sample number, generated by the server for new samples</td> <td>The sample number, generated by the server for new samples</td>
<td>'An31'</td> <td>'An31'</td>
</tr> </tr>
<tr> <tr>
<td>color</td> <td>color</td>
<td>Sample color</td> <td>Sample color</td>
<td>'black'</td> <td>'black'</td>
</tr> </tr>
<tr> <tr>
<td>batch</td> <td>batch</td>
<td>Batch number the sample was from</td> <td>Batch number the sample was from</td>
<td>'2264486614'</td> <td>'2264486614'</td>
</tr> </tr>
<tr> <tr>
<td>status</td> <td>status</td>
<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
<span class="name">deleted</span>.</td> <span class="name">deleted</span>.</td>
<td>'new'</td> <td>'new'</td>
</tr> </tr>
<tr> <tr>
<td>condition</td> <td>condition</td>
<td>sample condition with <span class="name">condition_template</span> reference and all fields defined by the <td>sample condition with <span class="name">condition_template</span> reference and all fields defined by the
template</td> template</td>
<td>{{'{'}}condition_template: '5f3151b5b8a886007d2de9ed', time in minutes: 30{{'}'}}</td> <td>{{'{'}}condition_template: '5f3151b5b8a886007d2de9ed', time in minutes: 30{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>material_id</td> <td>material_id</td>
<td>Reference to the sample material</td> <td>Reference to the sample material</td>
<td>'5f2e63118d1c020f8cda6a0a'</td> <td>'5f2e63118d1c020f8cda6a0a'</td>
</tr> </tr>
<tr> <tr>
<td>note_id</td> <td>note_id</td>
<td>Reference to the sample note</td> <td>Reference to the sample note</td>
<td>'5f2e63c98d1c020f8cda6e08'</td> <td>'5f2e63c98d1c020f8cda6e08'</td>
</tr> </tr>
<tr> <tr>
<td>user_id</td> <td>user_id</td>
<td>Reference to the user who created the sample</td> <td>Reference to the user who created the sample</td>
<td>'5f294dd4aa9ea5085c7d7315'</td> <td>'5f294dd4aa9ea5085c7d7315'</td>
</tr> </tr>
<tr><th>notes</th><th></th><th></th></tr> <tr><th>notes</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63e98d1c020f8cda70cc'</td> <td>'5f2e63e98d1c020f8cda70cc'</td>
</tr> </tr>
<tr> <tr>
<td>comment</td> <td>comment</td>
<td>General remarks</td> <td>General remarks</td>
<td>'stabilized'</td> <td>'stabilized'</td>
</tr> </tr>
<tr> <tr>
<td>sample_references</td> <td>sample_references</td>
<td>Array of references to other samples, each reference containing the referenced <span class="name">sample_id <td>Array of references to other samples, each reference containing the referenced <span class="name">sample_id
</span> as well as a <span class="name">relation</span> field describing the relationship</td> </span> as well as a <span class="name">relation</span> field describing the relationship</td>
<td>{{'{'}}sample_id: '5f2e63d68d1c020f8cda701c', relation: 'belongs to'{{'}'}}</td> <td>{{'{'}}sample_id: '5f2e63d68d1c020f8cda701c', relation: 'belongs to'{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>custom_fields</td> <td>custom_fields</td>
<td>Additional information as key value pairs for the sample, making it easier to process this information</td> <td>Additional information as key value pairs for the sample, making it easier to process this information</td>
<td>{{'{'}}vwz: '0 min'{{'}'}}</td> <td>{{'{'}}vwz: '0 min'{{'}'}}</td>
</tr> </tr>
<tr><th>note_fields</th><th></th><th></th></tr> <tr><th>note_fields</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63e98d1c020f8cda70ce'</td> <td>'5f2e63e98d1c020f8cda70ce'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>name of the <span class="name">custom_fields</span> key</td> <td>name of the <span class="name">custom_fields</span> key</td>
<td>'test series'</td> <td>'test series'</td>
</tr> </tr>
<tr> <tr>
<td>qty</td> <td>qty</td>
<td>number of notes with this key</td> <td>number of notes with this key</td>
<td>24</td> <td>24</td>
</tr> </tr>
<tr><th>materials</th><th></th><th></th></tr> <tr><th>materials</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63e98d1c020f8cda70d0'</td> <td>'5f2e63e98d1c020f8cda70d0'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>Trade name of the material</td> <td>Trade name of the material</td>
<td>'Ultradur B4300 G6'</td> <td>'Ultradur B4300 G6'</td>
</tr> </tr>
<tr> <tr>
<td>numbers</td> <td>numbers</td>
<td>Bosch material part numbers</td> <td>Bosch material part numbers</td>
<td>['5515753021, '5515753404']</td> <td>['5515753021, '5515753404']</td>
</tr> </tr>
<tr> <tr>
<td>properties</td> <td>properties</td>
<td>material class specific properties with <span class="name">material_template</span> reference and all fields <td>material class specific properties with <span class="name">material_template</span> reference and all fields
defined by the template</td> defined by the template</td>
<td>{{'{'}}material_template: '5f2e89874ac96c007fb06e83', mineral: 0, glass_fiber: 30, carbon_fiber: 0{{'}'}}</td> <td>{{'{'}}material_template: '5f2e89874ac96c007fb06e83', mineral: 0, glass_fiber: 30, carbon_fiber: 0{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>group_id</td> <td>group_id</td>
<td>Reference to the material group</td> <td>Reference to the material group</td>
<td>'5f2e631191c5d68f8a0708c4'</td> <td>'5f2e631191c5d68f8a0708c4'</td>
</tr> </tr>
<tr> <tr>
<td>supplier_id</td> <td>supplier_id</td>
<td>Reference to the material supplier</td> <td>Reference to the material supplier</td>
<td>'5f2e631191c5d68f8a0708c7'</td> <td>'5f2e631191c5d68f8a0708c7'</td>
</tr> </tr>
<tr> <tr>
<td>status</td> <td>status</td>
<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
<span class="name">deleted</span>.</td> <span class="name">deleted</span>.</td>
<td>'new'</td> <td>'new'</td>
</tr> </tr>
<tr><th>material_groups</th><th></th><th></th></tr> <tr><th>material_groups</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e631291c5d68f8a0708f9'</td> <td>'5f2e631291c5d68f8a0708f9'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>The chemical material type</td> <td>The chemical material type</td>
<td>'PA66'</td> <td>'PA66'</td>
</tr> </tr>
<tr><th>material_supplier</th><th></th><th></th></tr> <tr><th>material_supplier</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e631991c5d68f8a0709c3'</td> <td>'5f2e631991c5d68f8a0709c3'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>The material supplier</td> <td>The material supplier</td>
<td>'BASF'</td> <td>'BASF'</td>
</tr> </tr>
<tr><th>measurements</th><th></th><th></th></tr> <tr><th>measurements</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f294d25aa9ea5085c7d7305'</td> <td>'5f294d25aa9ea5085c7d7305'</td>
</tr> </tr>
<tr> <tr>
<td>sample_id</td> <td>sample_id</td>
<td>Reference to the sample this measurement belongs to</td> <td>Reference to the sample this measurement belongs to</td>
<td>'5f2e63c98d1c020f8cda6e06'</td> <td>'5f2e63c98d1c020f8cda6e06'</td>
</tr> </tr>
<tr> <tr>
<td>measurement_template</td> <td>measurement_template</td>
<td>Reference to the Template defining the structure of the measurement values</td> <td>Reference to the Template defining the structure of the measurement values</td>
<td>'5f294d25aa9ea5085c7d7305'</td> <td>'5f294d25aa9ea5085c7d7305'</td>
</tr> </tr>
<tr> <tr>
<td>values</td> <td>values</td>
<td>Measurement values in defined format</td> <td>Measurement values in defined format</td>
<td>{{'{'}}vn: 100.4{{'}'}}</td> <td>{{'{'}}vn: 100.4{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>status</td> <td>status</td>
<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
<span class="name">deleted</span>.</td> <span class="name">deleted</span>.</td>
<td>'new'</td> <td>'new'</td>
</tr> </tr>
<tr><th>&lt;collection&gt;_templates</th><th></th><th></th></tr> <tr><th>&lt;collection&gt;_templates</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63ee8d1c020f8cda7128'</td> <td>'5f2e63ee8d1c020f8cda7128'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>Display name of the template</td> <td>Display name of the template</td>
<td>'spectrum'</td> <td>'spectrum'</td>
</tr> </tr>
<tr> <tr>
<td>version</td> <td>version</td>
<td>Version number of the template</td> <td>Version number of the template</td>
<td>2</td> <td>2</td>
</tr> </tr>
<tr> <tr>
<td>first_id</td> <td>first_id</td>
<td>Reference to the first instance of this template with <span class="name">version</span> number 1</td> <td>Reference to the first instance of this template with <span class="name">version</span> number 1</td>
<td>'5f2e89bb4ac96c007fb06e86'</td> <td>'5f2e89bb4ac96c007fb06e86'</td>
</tr> </tr>
<tr> <tr>
<td>parameters</td> <td>parameters</td>
<td>Specified parameters of this template. The <span class="name">name</span> property is used as the key in the <td>Specified parameters of this template. The <span class="name">name</span> property is used as the key in the
document using this template, the range can have the following properties: <span class="name">min</span> specifies document using this template, the range can have the following properties: <span class="name">min</span> specifies
the minimum numeric value, <span class="name">max</span> specifies the maximum numeric value, <span class="name"> the minimum numeric value, <span class="name">max</span> specifies the maximum numeric value, <span class="name">
values</span> specifies an array of allowed values of this parameter and <span class="name">type: 'array'</span> values</span> specifies an array of allowed values of this parameter and <span class="name">type: 'array'</span>
specifies that this parameter must be an array</td> specifies that this parameter must be an array</td>
<td></td> <td></td>
</tr> </tr>
<tr><th>users</th><th></th><th></th></tr> <tr><th>users</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63cc8d1c020f8cda6e6a'</td> <td>'5f2e63cc8d1c020f8cda6e6a'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>The username</td> <td>The username</td>
<td>'admin'</td> <td>'admin'</td>
</tr> </tr>
<tr> <tr>
<td>email</td> <td>email</td>
<td>The user's email address used for password reset</td> <td>The user's email address used for password reset</td>
<td>'test@bosch.com'</td> <td>'test@bosch.com'</td>
</tr> </tr>
<tr> <tr>
<td>location</td> <td>location</td>
<td>The abbreviation of the Bosch site of the user</td> <td>The abbreviation of the Bosch site of the user</td>
<td>'Rng'</td> <td>'Rng'</td>
</tr> </tr>
<tr> <tr>
<td>level</td> <td>level</td>
<td>The permission level, can be either <span class="name">read</span>, <span class="name">write</span>, <td>The permission level, can be either <span class="name">read</span>, <span class="name">write</span>,
<span class="name">dev</span> or <span class="name">admin</span>. The exact level permissions can be found at the <span class="name">dev</span> or <span class="name">admin</span>. The exact level permissions can be found at the
<a routerLink="/documentation">general documentation</a></td> <a routerLink="/documentation">general documentation</a></td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>devices</td> <td>devices</td>
<td>Array of all spectrum measurement devices the user has access to</td> <td>Array of all spectrum measurement devices the user has access to</td>
<td>['Rng01', 'Rng02']</td> <td>['Rng01', 'Rng02']</td>
</tr> </tr>
<tr> <tr>
<td>pass</td> <td>pass</td>
<td>The user's password in hashed form using bcrypt</td> <td>The user's password in hashed form using bcrypt</td>
<td>'$2a$10$m8DqvZR3plZEv8EPwPo7Luvyrm/ZQDiPzfBh6bpU/1XFWOGONkJyG'</td> <td>'$2a$10$m8DqvZR3plZEv8EPwPo7Luvyrm/ZQDiPzfBh6bpU/1XFWOGONkJyG'</td>
</tr> </tr>
<tr> <tr>
<td>key</td> <td>key</td>
<td>The API key, generated when the user is created</td> <td>The API key, generated when the user is created</td>
<td>'5f294dd4aa9ea5085c7d7314'</td> <td>'5f294dd4aa9ea5085c7d7314'</td>
</tr> </tr>
<tr> <tr>
<td>models</td> <td>models</td>
<td> <td>
Reference to the prediction models the user should have access to. Ignored for <span class="name">dev</span> and Reference to the prediction models the user should have access to. Ignored for <span class="name">dev</span> and
<span class="name">admin</span> as they automatically have access to all models. <span class="name">admin</span> as they automatically have access to all models.
</td> </td>
<td>['5f466fb1e566810dd8b3e919', '5f294d8aaa9ea5085c7d730b']</td> <td>['5f466fb1e566810dd8b3e919', '5f294d8aaa9ea5085c7d730b']</td>
</tr> </tr>
<tr> <tr>
<td>status</td> <td>status</td>
<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
<span class="name">deleted</span>.</td> <span class="name">deleted</span>.</td>
<td>'new'</td> <td>'new'</td>
</tr> </tr>
<tr><th>models</th><th></th><th></th></tr> <tr><th>models</th><th></th><th></th></tr>
<tr> <tr>
<td>group</td> <td>group</td>
<td>group the model belongs to</td> <td>group the model belongs to</td>
<td>'VN'</td> <td>'VN'</td>
</tr> </tr>
<tr> <tr>
<td>models</td> <td>models</td>
<td>models of the group with _id, name and url to the Python endpoint</td> <td>models of the group with _id, name and url to the Python endpoint</td>
<td>{{'{'}}_id: '5f466fb1e566810dd8b3e919', name: POM, url: 'http://localhost:9099/test'{{'}'}}</td> <td>{{'{'}}_id: '5f466fb1e566810dd8b3e919', name: POM, url: 'http://localhost:9099/test'{{'}'}}</td>
</tr> </tr>
<tr><th>model_files</th><th></th><th></th></tr> <tr><th>model_files</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f294d47aa9ea5085c7d7308'</td> <td>'5f294d47aa9ea5085c7d7308'</td>
</tr> </tr>
<tr> <tr>
<td>name</td> <td>name</td>
<td>The name of the model</td> <td>The name of the model</td>
<td>'humidity-1'</td> <td>'humidity-1'</td>
</tr> </tr>
<tr> <tr>
<td>data</td> <td>data</td>
<td>The Python model data in binary format</td> <td>The Python model data in binary format</td>
<td>&lt;binary data&gt;</td> <td>&lt;binary data&gt;</td>
</tr> </tr>
<tr><th>changelogs</th><th></th><th></th></tr> <tr><th>changelogs</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63cc8d1c020f8cda6e6e'</td> <td>'5f2e63cc8d1c020f8cda6e6e'</td>
</tr> </tr>
<tr> <tr>
<td>action</td> <td>action</td>
<td>The URL which invoked the database write access</td> <td>The URL which invoked the database write access</td>
<td>'POST /material/new'</td> <td>'POST /material/new'</td>
</tr> </tr>
<tr> <tr>
<td>collection_name</td> <td>collection_name</td>
<td>Collection that was written to</td> <td>Collection that was written to</td>
<td>'material_groups'</td> <td>'material_groups'</td>
</tr> </tr>
<tr> <tr>
<td>conditions</td> <td>conditions</td>
<td>Condition arguments used when accessing the database</td> <td>Condition arguments used when accessing the database</td>
<td>{{'{'}}id: '5f2e63118d1c020f8cda6a0a'{{'}'}}</td> <td>{{'{'}}id: '5f2e63118d1c020f8cda6a0a'{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>data</td> <td>data</td>
<td>data which was written to the database</td> <td>data which was written to the database</td>
<td>{{'{'}}name: 'PBT'{{'}'}}</td> <td>{{'{'}}name: 'PBT'{{'}'}}</td>
</tr> </tr>
<tr> <tr>
<td>user_id</td> <td>user_id</td>
<td>The user that executed this command</td> <td>The user that executed this command</td>
<td>'5f2e63118d1c020f8cda6a09'</td> <td>'5f2e63118d1c020f8cda6a09'</td>
</tr> </tr>
<tr><th>help</th><th></th><th></th></tr> <tr><th>help</th><th></th><th></th></tr>
<tr> <tr>
<td>_id</td> <td>_id</td>
<td>Automatically generated unique id</td> <td>Automatically generated unique id</td>
<td>'5f2e63d28d1c020f8cda6f86'</td> <td>'5f2e63d28d1c020f8cda6f86'</td>
</tr> </tr>
<tr> <tr>
<td>key</td> <td>key</td>
<td>The key used to find the required help text</td> <td>The key used to find the required help text</td>
<td>'/documentation/database'</td> <td>'/documentation/database'</td>
</tr> </tr>
<tr> <tr>
<td>level</td> <td>level</td>
<td>The minimum level required to read this help</td> <td>The minimum level required to read this help</td>
<td>'write'</td> <td>'write'</td>
</tr> </tr>
<tr> <tr>
<td>text</td> <td>text</td>
<td>The actual help text</td> <td>The actual help text</td>
<td>'This page documents the database.'</td> <td>'This page documents the database.'</td>
</tr> </tr>
</rb-table> </rb-table>

View File

@ -1,14 +1,14 @@
.field-reference td:nth-child(3) { .field-reference td:nth-child(3) {
font-family: boschmono, monospace; font-family: boschmono, monospace;
} }
span.name { span.name {
font-style: italic; font-style: italic;
} }
pre { pre {
padding: 1rem; padding: 1rem;
color: #212529; color: #212529;
background: #e6e6e6; background: #e6e6e6;
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@ -1,15 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-documentation-database', selector: 'app-documentation-database',
templateUrl: './documentation-database.component.html', templateUrl: './documentation-database.component.html',
styleUrls: ['./documentation-database.component.scss'] styleUrls: ['./documentation-database.component.scss']
}) })
export class DocumentationDatabaseComponent implements OnInit { export class DocumentationDatabaseComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -1,38 +1,38 @@
<h4>Model file upload</h4> <h4>Model file upload</h4>
<p> <p>
Model files have to be saved as a .pkl file in 80_Modelle_BIC. <br> Model files have to be saved as a .pkl file in 80_Modelle_BIC. <br>
For upload the upload script has to be opened (can be in every environment). The name of the model file has to be For upload the upload script has to be opened (can be in every environment). The name of the model file has to be
entered wih the .pkl ending in the first command and without .pkl in the second. entered wih the .pkl ending in the first command and without .pkl in the second.
</p> </p>
<h4 class="space-above">Adding new model scripts</h4> <h4 class="space-above">Adding new model scripts</h4>
<p> <p>
Open Spyder in the base environment and open the model.py within. Open Spyder in the base environment and open the model.py within.
</p> </p>
<ul> <ul>
<li>Enter model file name without .pkl in fetchData</li> <li>Enter model file name without .pkl in fetchData</li>
<li>Add new prediction function below others</li> <li>Add new prediction function below others</li>
<li>duplicate another @app.route and rename according to desired route name and prediction function name</li> <li>duplicate another @app.route and rename according to desired route name and prediction function name</li>
</ul> </ul>
<h4 class="space-above">Testing locally</h4> <h4 class="space-above">Testing locally</h4>
<p> <p>
To test the model Python script locally, you need to execute it in the base environment, in which all necessary To test the model Python script locally, you need to execute it in the base environment, in which all necessary
packages have to be installed. Note that Spyder also has to be opened in the base environment. packages have to be installed. Note that Spyder also has to be opened in the base environment.
</p> </p>
<ul> <ul>
<li>Open a separate Anaconda Prompt and cd into the definma-UI folder</li> <li>Open a separate Anaconda Prompt and cd into the definma-UI folder</li>
<li>Adapt the model-mock.json at definma-UI/assets/ and enter the new model URL and name</li> <li>Adapt the model-mock.json at definma-UI/assets/ and enter the new model URL and name</li>
<li>Execute &quot;python -m http.server&quot; in the Anaconda Prompt</li> <li>Execute &quot;python -m http.server&quot; in the Anaconda Prompt</li>
<li>Execute the model.py script in Spyder</li> <li>Execute the model.py script in Spyder</li>
<li>Navigate to <a href="http://localhost:8000">http://localhost:8000</a> in the browser</li> <li>Navigate to <a href="http://localhost:8000">http://localhost:8000</a> in the browser</li>
<li>If there are problems with cached data, open the Browser developer console (Ctrl+Shift+I) and tick <li>If there are problems with cached data, open the Browser developer console (Ctrl+Shift+I) and tick
&quot;Disable cache&quot; in the Network tab. The console then has to stay open.</li> &quot;Disable cache&quot; in the Network tab. The console then has to stay open.</li>
</ul> </ul>
<h4 class="space-above">Model script upload</h4> <h4 class="space-above">Model script upload</h4>
@ -40,17 +40,17 @@
In the Windows Powershell: In the Windows Powershell:
<ul> <ul>
<li>cf login (only at the first time)</li> <li>cf login (only at the first time)</li>
<li>API endpoint: https://api.sys.de1.bosch-iot-cloud.com</li> <li>API endpoint: https://api.sys.de1.bosch-iot-cloud.com</li>
<li>Enter email with .de.bosch.com</li> <li>Enter email with .de.bosch.com</li>
<li>Enter BIC password</li> <li>Enter BIC password</li>
<li>Select space to upload to (currently 3 - development)</li> <li>Select space to upload to (currently 3 - development)</li>
<li>cd into the folder containing the manifest.yaml (here is also model.py)</li> <li>cd into the folder containing the manifest.yaml (here is also model.py)</li>
<li>cf push</li> <li>cf push</li>
</ul> </ul>
<p> <p>
After upload the new model details have to be entered in the UI. After upload the new model details have to be entered in the UI.
</p> </p>

View File

@ -1,15 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-documentation-models', selector: 'app-documentation-models',
templateUrl: './documentation-models.component.html', templateUrl: './documentation-models.component.html',
styleUrls: ['./documentation-models.component.scss'] styleUrls: ['./documentation-models.component.scss']
}) })
export class DocumentationModelsComponent implements OnInit { export class DocumentationModelsComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -1,78 +1,78 @@
<p> <p>
<a [href]="api.hostName + '/static/intro-presentation/index.html'"> <a [href]="api.hostName + '/static/intro-presentation/index.html'">
View the presentation explaining the main functions View the presentation explaining the main functions
</a><br> </a><br>
View the <a href="/assets/docs/Veit-Lukas_T3000.pdf">T3000 report</a> and View the <a href="/assets/docs/Veit-Lukas_T3000.pdf">T3000 report</a> and
<a href="/assets/docs/Veit-Lukas_Bachelor-Thesis.pdf">Bachelor Thesis</a> for an in-depth application description. <a href="/assets/docs/Veit-Lukas_Bachelor-Thesis.pdf">Bachelor Thesis</a> for an in-depth application description.
</p> </p>
<h4>User levels</h4> <h4>User levels</h4>
<rb-table> <rb-table>
<tr> <tr>
<th></th> <th></th>
<th>prediction</th> <th>prediction</th>
<th>read</th> <th>read</th>
<th>write</th> <th>write</th>
<th>dev</th> <th>dev</th>
<th>admin</th> <th>admin</th>
</tr> </tr>
<tr> <tr>
<th>use prediction models</th> <th>use prediction models</th>
<td>specified ones</td> <td>specified ones</td>
<td>specified ones</td> <td>specified ones</td>
<td>specified ones</td> <td>specified ones</td>
<td>all</td> <td>all</td>
<td>all</td> <td>all</td>
</tr> </tr>
<tr> <tr>
<th>read sample data</th> <th>read sample data</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
<tr> <tr>
<th>add samples/edit own</th> <th>add samples/edit own</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
<tr> <tr>
<th>read spectral data</th> <th>read spectral data</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
<tr> <tr>
<th>edit other's data</th> <th>edit other's data</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
<tr> <tr>
<th>maintain templates</th> <th>maintain templates</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
<tr> <tr>
<th>edit users</th> <th>edit users</th>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-abort-frame"></span></td> <td><span class="rb-ic rb-ic-abort-frame"></span></td>
<td><span class="rb-ic rb-ic-checkmark-frame"></span></td> <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
</tr> </tr>
</rb-table> </rb-table>

View File

@ -1,17 +1,17 @@
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; @import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
p { p {
margin-bottom: 20px; margin-bottom: 20px;
} }
img#db-structure { img#db-structure {
width: 100%; width: 100%;
} }
.rb-ic-checkmark-frame { .rb-ic-checkmark-frame {
color: $brand-success; color: $brand-success;
} }
.rb-ic-abort-frame { .rb-ic-abort-frame {
color: $brand-danger; color: $brand-danger;
} }

View File

@ -3,17 +3,17 @@ import {ApiService} from '../services/api.service';
@Component({ @Component({
selector: 'app-documentation', selector: 'app-documentation',
templateUrl: './documentation.component.html', templateUrl: './documentation.component.html',
styleUrls: ['./documentation.component.scss'] styleUrls: ['./documentation.component.scss']
}) })
export class DocumentationComponent implements OnInit { export class DocumentationComponent implements OnInit {
constructor( constructor(
public api: ApiService public api: ApiService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -1,11 +1,11 @@
<rb-alert alertTitle="Error" type="error" okBtnLabel="Discard"> <rb-alert alertTitle="Error" type="error" okBtnLabel="Discard">
<div class="space-below"> <div class="space-below">
{{message}} {{message}}
</div> </div>
<div *ngIf="details.length"> <div *ngIf="details.length">
<a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a> <a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
<div *ngIf="triggerDetails.open"> <div *ngIf="triggerDetails.open">
<p *ngFor="let detail of details">{{detail}}</p> <p *ngFor="let detail of details">{{detail}}</p>
</div> </div>
</div> </div>
</rb-alert> </rb-alert>

View File

@ -1,18 +1,18 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-error', selector: 'app-error',
templateUrl: './error.component.html', templateUrl: './error.component.html',
styleUrls: ['./error.component.scss'] styleUrls: ['./error.component.scss']
}) })
export class ErrorComponent implements OnInit { export class ErrorComponent implements OnInit {
message = ''; // Main error message message = ''; // Main error message
details: string[] = []; // Array of error detail paragraphs details: string[] = []; // Array of error detail paragraphs
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -2,8 +2,8 @@ import { ExistsPipe } from './exists.pipe';
describe('ExistsPipe', () => { describe('ExistsPipe', () => {
it('create an instance', () => { it('create an instance', () => {
const pipe = new ExistsPipe(); const pipe = new ExistsPipe();
expect(pipe).toBeTruthy(); expect(pipe).toBeTruthy();
}); });
}); });

View File

@ -1,13 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'exists', name: 'exists',
pure: true pure: true
}) })
export class ExistsPipe implements PipeTransform { export class ExistsPipe implements PipeTransform {
transform(value: unknown, key?): unknown { transform(value: unknown, key?): unknown {
return value || value === 0 ? (key ? value[key] : value) : ''; return value || value === 0 ? (key ? value[key] : value) : '';
} }
} }

View File

@ -1,25 +1,25 @@
<h3>Help <h3>Help
<span *ngIf="login.isLevel.dev" class="rb-ic rb-ic-edit clickable space-left" (click)="edit = true"></span> <span *ngIf="login.isLevel.dev" class="rb-ic rb-ic-edit clickable space-left" (click)="edit = true"></span>
</h3> </h3>
<div *ngIf="edit; else normalView"> <div *ngIf="edit; else normalView">
<rb-form-select label="level" [(ngModel)]="content.level"> <rb-form-select label="level" [(ngModel)]="content.level">
<option value="none">none</option> <option value="none">none</option>
<option *ngFor="let level of login.levels" [value]="level">{{level}}</option> <option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
</rb-form-select> </rb-form-select>
<rb-form-textarea label="text" [(ngModel)]="content.text"></rb-form-textarea> <rb-form-textarea label="text" [(ngModel)]="content.text"></rb-form-textarea>
<rb-icon-button icon="save" mode="primary" (click)="saveHelp()">Save</rb-icon-button> <rb-icon-button icon="save" mode="primary" (click)="saveHelp()">Save</rb-icon-button>
<rb-icon-button icon="delete" mode="danger" (click)="deleteHelp()" class="delete-btn">Delete</rb-icon-button> <rb-icon-button icon="delete" mode="danger" (click)="deleteHelp()" class="delete-btn">Delete</rb-icon-button>
</div> </div>
<ng-template #normalView> <ng-template #normalView>
<p *ngIf="content.text; else defaultContent" class="content-text"> <p *ngIf="content.text; else defaultContent" class="content-text">
{{content.text}} {{content.text}}
</p> </p>
<ng-template #defaultContent> <ng-template #defaultContent>
<ng-container *ngIf="content.text === ''"> <ng-container *ngIf="content.text === ''">
Sadly, currently there is no help available for this page. Please contact Sadly, currently there is no help available for this page. Please contact
<a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a> for further questions. <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a> for further questions.
</ng-container> </ng-container>
</ng-template> </ng-template>
</ng-template> </ng-template>

View File

@ -1,7 +1,7 @@
.delete-btn { .delete-btn {
float: right; float: right;
} }
.content-text { .content-text {
white-space: pre-line; white-space: pre-line;
} }

View File

@ -6,48 +6,48 @@ import {HelpModel} from '../models/help.model';
import {LoginService} from '../services/login.service'; import {LoginService} from '../services/login.service';
@Component({ @Component({
selector: 'app-help', selector: 'app-help',
templateUrl: './help.component.html', templateUrl: './help.component.html',
styleUrls: ['./help.component.scss'] styleUrls: ['./help.component.scss']
}) })
export class HelpComponent implements OnInit { export class HelpComponent implements OnInit {
content: HelpModel = new HelpModel().deserialize({text: null, level: 'none'}); // Help content content: HelpModel = new HelpModel().deserialize({text: null, level: 'none'}); // Help content
edit = false; // Set true to change to edit mode edit = false; // Set true to change to edit mode
private route = ''; // URIComponent encoded route which serves as a key to fetch the help document private route = ''; // URIComponent encoded route which serves as a key to fetch the help document
constructor( constructor(
private router: Router, private router: Router,
public d: DataService, public d: DataService,
private api: ApiService, private api: ApiService,
public login: LoginService public login: LoginService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
// Remove ids from path // Remove ids from path
this.route = encodeURIComponent(this.router.url.replace(/\/[0-9a-f]{24}/, '')); this.route = encodeURIComponent(this.router.url.replace(/\/[0-9a-f]{24}/, ''));
this.api.get<HelpModel>('/help/' + this.route, (data, err) => { this.api.get<HelpModel>('/help/' + this.route, (data, err) => {
if (!err) { // Content was found if (!err) { // Content was found
this.content = new HelpModel().deserialize(data); this.content = new HelpModel().deserialize(data);
} }
else { else {
this.content.text = ''; this.content.text = '';
} }
}); });
} }
saveHelp() { saveHelp() {
this.api.post('/help/' + this.route, this.content.sendFormat(), () => { this.api.post('/help/' + this.route, this.content.sendFormat(), () => {
this.edit = false; this.edit = false;
}); });
} }
deleteHelp() { deleteHelp() {
this.api.delete('/help/' + this.route, (ignore, err) => { this.api.delete('/help/' + this.route, (ignore, err) => {
if (!err) { if (!err) {
this.content = new HelpModel().deserialize({text: null, level: 'none'}); this.content = new HelpModel().deserialize({text: null, level: 'none'});
this.edit = false; this.edit = false;
} }
}); });
} }
} }

View File

@ -1,20 +1,20 @@
<div *ngIf="!login.isLoggedIn"> <div *ngIf="!login.isLoggedIn">
<app-login></app-login> <app-login></app-login>
<img src="/assets/imgs/key-visual.png" alt="" class="key-visual"> <img src="/assets/imgs/key-visual.png" alt="" class="key-visual">
</div> </div>
<div *ngIf="login.isLoggedIn"> <div *ngIf="login.isLoggedIn">
<rb-form-multi-select name="groupSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Groups" class="selection"> <rb-form-multi-select name="groupSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Groups" class="selection">
<span *rbFormMultiSelectOption="let key">{{key.id}}</span> <span *rbFormMultiSelectOption="let key">{{key.id}}</span>
</rb-form-multi-select> </rb-form-multi-select>
<rb-icon-button icon="forward-right" mode="primary" (click)="updateGroups(isActiveKey)"> <rb-icon-button icon="forward-right" mode="primary" (click)="updateGroups(isActiveKey)">
Apply groups Apply groups
</rb-icon-button> </rb-icon-button>
<div id="divChart"> <div id="divChart">
<canvas id="myChart"> <canvas id="myChart">
</canvas> </canvas>
</div> </div>
</div> </div>

View File

@ -1,17 +1,17 @@
app-login { app-login {
float: left; float: left;
} }
.key-visual { .key-visual {
width: 70%; width: 70%;
float: right; float: right;
} }
.selection{ .selection{
float: left; float: left;
width: 20%; width: 20%;
} }
rb-form-multi-select { rb-form-multi-select {
width: 20%; width: 20%;
} }

View File

@ -5,138 +5,138 @@ import { Chart } from 'chart.js';
interface KeyInterface { interface KeyInterface {
id: string; id: string;
count: number; count: number;
active: boolean; active: boolean;
} }
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'] styleUrls: ['./home.component.scss']
}) })
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit {
keys: KeyInterface[] = []; keys: KeyInterface[] = [];
isActiveKey: { [key: string]: boolean } = {}; // Object to check if key is currently active isActiveKey: { [key: string]: boolean } = {}; // Object to check if key is currently active
myChart: Chart; myChart: Chart;
constructor( constructor(
public login: LoginService, public login: LoginService,
public api: ApiService public api: ApiService
) { } ) { }
ngOnInit() { ngOnInit() {
// Fetch all available groups // Fetch all available groups
this.fetchData('/material/groups', data => this.createGroup(data)); this.fetchData('/material/groups', data => this.createGroup(data));
} }
// Api access with callback // Api access with callback
async fetchData(URL: string, processor: any) { async fetchData(URL: string, processor: any) {
this.api.get(URL, (sData, err, headers) => { this.api.get(URL, (sData, err, headers) => {
processor(sData); processor(sData);
}); });
} }
// Fill interface with data // Fill interface with data
createGroup(data: any) { createGroup(data: any) {
let temp: KeyInterface[] = []; let temp: KeyInterface[] = [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
temp.push({ id: data[i], count: 0, active: false }); temp.push({ id: data[i], count: 0, active: false });
} }
this.keys = temp; // Invoke update in rb-multiselect this.keys = temp; // Invoke update in rb-multiselect
this.initChart(); this.initChart();
// Only neccesary if keys get preselected // Only neccesary if keys get preselected
//this.calcFieldSelectKeys(); //this.calcFieldSelectKeys();
// Fetch all samples populated with according group // Fetch all samples populated with according group
this.getSamples(); this.getSamples();
} }
// Iterate through active keys to assemble an api request for the required data // Iterate through active keys to assemble an api request for the required data
getSamples() { getSamples() {
let query = '/samples?status%5B%5D=validated&status=new&filters%5B%5D=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.group%22%2C%22values%22%3A%5B'; let query = '/samples?status%5B%5D=validated&status=new&filters%5B%5D=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.group%22%2C%22values%22%3A%5B';
let temp = ''; let temp = '';
this.keys.forEach(key => { this.keys.forEach(key => {
temp += key.active ? '%22' + key.id.split("%").join("%25") + '%22%2C' : ""; // Replace split().join() with replaceAll() temp += key.active ? '%22' + key.id.split("%").join("%25") + '%22%2C' : ""; // Replace split().join() with replaceAll()
}); });
if (temp === '') { if (temp === '') {
this.countSamples(''); this.countSamples('');
} else { } else {
query = query + temp.substr(0, temp.length - 3) + '%5D%7D&fields%5B%5D=material.group'; query = query + temp.substr(0, temp.length - 3) + '%5D%7D&fields%5B%5D=material.group';
this.fetchData(query, data => this.countSamples(data)); this.fetchData(query, data => this.countSamples(data));
} }
} }
// Loop through samples and count // Loop through samples and count
countSamples(data: any) { countSamples(data: any) {
this.keys.map(key => key.count = 0); this.keys.map(key => key.count = 0);
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
this.keys.forEach(key => { this.keys.forEach(key => {
if (key.id === data[i].material.group) { if (key.id === data[i].material.group) {
key.count += 1; key.count += 1;
} }
}); });
} }
this.updateGraph(); this.updateGraph();
} }
// Preset select // Preset select
calcFieldSelectKeys() { calcFieldSelectKeys() {
this.keys.forEach(key => { this.keys.forEach(key => {
this.isActiveKey[key.id] = key.active; this.isActiveKey[key.id] = key.active;
}); });
} }
// Update keys based on select // Update keys based on select
updateGroups(activeKeys: any) { updateGroups(activeKeys: any) {
this.keys.forEach(key => { this.keys.forEach(key => {
if (activeKeys.hasOwnProperty(key.id)) { if (activeKeys.hasOwnProperty(key.id)) {
key.active = activeKeys[key.id]; key.active = activeKeys[key.id];
} }
}); });
this.getSamples(); this.getSamples();
} }
// Get data for graph based on active keys // Get data for graph based on active keys
updateGraph() { updateGraph() {
let nameList: string[] = []; let nameList: string[] = [];
let dataList: number[] = []; let dataList: number[] = [];
this.keys.forEach(key => { this.keys.forEach(key => {
if (key.active) { if (key.active) {
nameList.push(key.id); nameList.push(key.id);
dataList.push(key.count); dataList.push(key.count);
} }
}) })
this.myChart.data.labels = nameList; this.myChart.data.labels = nameList;
this.myChart.data.datasets[0].data = dataList; this.myChart.data.datasets[0].data = dataList;
this.myChart.update(); this.myChart.update();
} }
// Initialize graph // Initialize graph
async initChart() { async initChart() {
this.myChart = new Chart("myChart", { this.myChart = new Chart("myChart", {
type: 'bar', type: 'bar',
data: { data: {
labels: [], labels: [],
datasets: [{ datasets: [{
label: 'Number of samples per group', label: 'Number of samples per group',
data: [], data: [],
backgroundColor: 'rgba(0, 86, 145, 1)' backgroundColor: 'rgba(0, 86, 145, 1)'
}] }]
}, },
options: { options: {
scales: { scales: {
yAxes: [{ yAxes: [{
ticks: { ticks: {
beginAtZero: true beginAtZero: true
} }
}] }]
} }
} }
}); });
} }
} }

View File

@ -1,17 +1,17 @@
<div class="img-container"> <div class="img-container">
<div class="magnifier" <div class="magnifier"
[ngStyle]="{ [ngStyle]="{
'background-size': backgroundSize, 'background-size': backgroundSize,
'background-image': 'url(\'' + src + '\')', 'background-image': 'url(\'' + src + '\')',
'background-position': '-' + magnifierPos.x * zoom + 'px -' + magnifierPos.y * zoom + 'px', 'background-position': '-' + magnifierPos.x * zoom + 'px -' + magnifierPos.y * zoom + 'px',
left: magnifierPos.x + 'px', left: magnifierPos.x + 'px',
top: magnifierPos.y + 'px', top: magnifierPos.y + 'px',
width: magnifierSize.width + 'px', width: magnifierSize.width + 'px',
height: magnifierSize.height + 'px' height: magnifierSize.height + 'px'
}" }"
(mousemove)="calcPos($event)" (mousemove)="calcPos($event)"
(mouseleave)="showMagnifier = false" (mouseleave)="showMagnifier = false"
*ngIf="showMagnifier"> *ngIf="showMagnifier">
</div> </div>
<img [src]="src" alt="" (mousemove)="calcPos($event)" (mouseenter)="showMagnifier = true" #mainImg> <img [src]="src" alt="" (mousemove)="calcPos($event)" (mouseenter)="showMagnifier = true" #mainImg>
</div> </div>

View File

@ -1,18 +1,18 @@
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; @import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
.img-container { .img-container {
position:relative; position:relative;
overflow: hidden; overflow: hidden;
& > img { & > img {
width: 100%; width: 100%;
} }
} }
.magnifier { .magnifier {
position: absolute; position: absolute;
background: #FFF no-repeat -500px -500px; background: #FFF no-repeat -500px -500px;
z-index: 99; z-index: 99;
border: 1px solid #FFF; border: 1px solid #FFF;
box-shadow: 10px 10px 25px $color-bosch-light-gray-b25; box-shadow: 10px 10px 25px $color-bosch-light-gray-b25;
} }

View File

@ -1,48 +1,48 @@
import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
@Component({ @Component({
selector: 'app-img-magnifier', selector: 'app-img-magnifier',
templateUrl: './img-magnifier.component.html', templateUrl: './img-magnifier.component.html',
styleUrls: ['./img-magnifier.component.scss'] styleUrls: ['./img-magnifier.component.scss']
}) })
export class ImgMagnifierComponent implements OnInit, AfterViewInit { export class ImgMagnifierComponent implements OnInit, AfterViewInit {
@Input() src: string; // Image source @Input() src: string; // Image source
@Input() zoom: number; // Zoom level @Input() zoom: number; // Zoom level
@Input() magnifierSize: {width: number, height: number}; // Size of the magnifier @Input() magnifierSize: {width: number, height: number}; // Size of the magnifier
@ViewChild('mainImg') mainImg: ElementRef; @ViewChild('mainImg') mainImg: ElementRef;
backgroundSize; backgroundSize;
magnifierPos = {x: 0, y: 0}; // Position of the magnifier magnifierPos = {x: 0, y: 0}; // Position of the magnifier
showMagnifier = false; showMagnifier = false;
constructor( constructor(
private window: Window private window: Window
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
} }
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => { setTimeout(() => {
this.calcBackgroundSize(); this.calcBackgroundSize();
}, 1); }, 1);
} }
calcPos(event) { // Calculate the current magnifier position calcPos(event) { // Calculate the current magnifier position
const img = this.mainImg.nativeElement.getBoundingClientRect(); const img = this.mainImg.nativeElement.getBoundingClientRect();
this.magnifierPos.x = Math.min( this.magnifierPos.x = Math.min(
img.width - this.magnifierSize.width, img.width - this.magnifierSize.width,
Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2) Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2)
); );
this.magnifierPos.y = Math.min( this.magnifierPos.y = Math.min(
img.height - this.magnifierSize.height + 7, img.height - this.magnifierSize.height + 7,
Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2) Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2)
); );
} }
calcBackgroundSize() { calcBackgroundSize() {
this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) + this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) +
'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0'; 'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0';
} }
} }

View File

@ -1,25 +1,25 @@
<div class="login-wrapper"> <div class="login-wrapper">
<h2>Please log in</h2> <h2>Please log in</h2>
<form #loginForm="ngForm"> <form #loginForm="ngForm">
<rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username" <rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username"
#usernameInput="ngModel"> #usernameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{usernameInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{usernameInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input *ngIf="!passreset" type="password" name="password" label="password" appValidate="password" required <rb-form-input *ngIf="!passreset" type="password" name="password" label="password" appValidate="password" required
[(ngModel)]="password" #passwordInput="ngModel"> [(ngModel)]="password" #passwordInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{passwordInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{passwordInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email" <rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email"
#emailInput="ngModel"> #emailInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a> <a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a>
<button class="rb-btn rb-primary login-button" (click)="userLogin()" type="submit" <button class="rb-btn rb-primary login-button" (click)="userLogin()" type="submit"
[disabled]="!loginForm.form.valid"> [disabled]="!loginForm.form.valid">
{{passreset ? 'Send' : 'Login'}} {{passreset ? 'Send' : 'Login'}}
</button> </button>
<div class="message">{{message}}</div> <div class="message">{{message}}</div>
</form> </form>
</div> </div>

View File

@ -1,17 +1,17 @@
.login-wrapper { .login-wrapper {
max-width: 250px; max-width: 250px;
} }
.message { .message {
font-size: 13px; font-size: 13px;
margin-top: 10px; margin-top: 10px;
} }
.login-button { .login-button {
margin-right: 10px; margin-right: 10px;
} }
.forgot-pass { .forgot-pass {
display: block; display: block;
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@ -6,57 +6,57 @@ import {ApiService} from '../services/api.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'] styleUrls: ['./login.component.scss']
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
username = ''; // Credentials username = ''; // Credentials
password = ''; password = '';
email = ''; email = '';
message = ''; // Message below login fields message = ''; // Message below login fields
passreset = false; // To toggle between normal login and password reset form passreset = false; // To toggle between normal login and password reset form
@ViewChild('loginForm') loginForm; @ViewChild('loginForm') loginForm;
constructor( constructor(
private validate: ValidationService, private validate: ValidationService,
private login: LoginService, private login: LoginService,
private api: ApiService, private api: ApiService,
private router: Router private router: Router
) { } ) { }
ngOnInit() { ngOnInit() {
} }
userLogin() { userLogin() {
if (this.passreset) { // Reset password if (this.passreset) { // Reset password
this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => { this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => {
if (err) { if (err) {
this.message = 'Could not find a valid user'; this.message = 'Could not find a valid user';
} }
else { else {
this.message = 'Password reset, check your inbox'; this.message = 'Password reset, check your inbox';
} }
}); });
} }
else { else {
this.login.login(this.username, this.password).then(ok => { this.login.login(this.username, this.password).then(ok => {
if (ok) { if (ok) {
this.message = 'Login successful'; this.message = 'Login successful';
if (this.login.isLevel.read) { if (this.login.isLevel.read) {
this.router.navigate(['/samples']); this.router.navigate(['/samples']);
} }
else { // Navigate prediction users to prediction as they cannot access samples else { // Navigate prediction users to prediction as they cannot access samples
this.router.navigate(['/prediction']); this.router.navigate(['/prediction']);
} }
} }
else { else {
this.message = 'Wrong credentials!'; this.message = 'Wrong credentials!';
} }
}); });
} }
} }
} }

View File

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

View File

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

View File

@ -11,132 +11,132 @@ import {ValidationService} from '../services/validation.service';
import {ErrorComponent} from '../error/error.component'; import {ErrorComponent} from '../error/error.component';
@Component({ @Component({
selector: 'app-material', selector: 'app-material',
templateUrl: './material.component.html', templateUrl: './material.component.html',
styleUrls: ['./material.component.scss'] styleUrls: ['./material.component.scss']
}) })
export class MaterialComponent implements OnInit, AfterContentChecked { export class MaterialComponent implements OnInit, AfterContentChecked {
@ViewChild('materialForm') materialForm: NgForm; @ViewChild('materialForm') materialForm: NgForm;
material: MaterialModel; // Material to edit material: MaterialModel; // Material to edit
materialNames: string[] = []; // All other material names for unique validation materialNames: string[] = []; // All other material names for unique validation
modalText = {list: '', suggestion: ''}; // Modal for group and supplier correction modalText = {list: '', suggestion: ''}; // Modal for group and supplier correction
loading = 0; // Number of loading instances loading = 0; // Number of loading instances
checkFormAfterInit = true; // Revalidate all fields on the next AfterContentChecked checkFormAfterInit = true; // Revalidate all fields on the next AfterContentChecked
constructor( constructor(
private api: ApiService, private api: ApiService,
private route: ActivatedRoute, private route: ActivatedRoute,
public d: DataService, public d: DataService,
private modal: ModalService, private modal: ModalService,
public autocomplete: AutocompleteService, public autocomplete: AutocompleteService,
private router: Router, private router: Router,
private validation: ValidationService private validation: ValidationService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loading = 5; this.loading = 5;
this.api.get<MaterialModel>('/material/' + this.route.snapshot.paramMap.get('id'), data => { this.api.get<MaterialModel>('/material/' + this.route.snapshot.paramMap.get('id'), data => {
this.material = new MaterialModel().deserialize(data); this.material = new MaterialModel().deserialize(data);
this.loading--; this.loading--;
this.d.load('materials', () => { this.d.load('materials', () => {
// Filter out name of the edited material as it can stay the same // Filter out name of the edited material as it can stay the same
this.materialNames = this.d.arr.materials.map(e => e.name).filter(e => e !== this.material.name); this.materialNames = this.d.arr.materials.map(e => e.name).filter(e => e !== this.material.name);
this.loading--; this.loading--;
}); });
}); });
this.d.load('materialSuppliers', () => { this.d.load('materialSuppliers', () => {
this.loading--; this.loading--;
}); });
this.d.load('materialGroups', () => { this.d.load('materialGroups', () => {
this.loading--; this.loading--;
}); });
this.d.load('materialTemplates', () => { this.d.load('materialTemplates', () => {
this.loading--; this.loading--;
}); });
} }
ngAfterContentChecked() { ngAfterContentChecked() {
// Attach validators // Attach validators
if (this.materialForm && this.material.properties.material_template) { // Material template is set 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.d.id.materialTemplates[this.material.properties.material_template].parameters.forEach((parameter, i) => {
this.attachValidator(this.materialForm, 'materialParameter' + i, parameter.range); this.attachValidator(this.materialForm, 'materialParameter' + i, parameter.range);
}); });
} }
// Revalidate // Revalidate
if (this.checkFormAfterInit && this.materialForm !== undefined && this.materialForm.form.get('propertiesSelect')) { if (this.checkFormAfterInit && this.materialForm !== undefined && this.materialForm.form.get('propertiesSelect')) {
this.checkFormAfterInit = false; this.checkFormAfterInit = false;
Object.keys(this.materialForm.form.controls).forEach(field => { Object.keys(this.materialForm.form.controls).forEach(field => {
this.materialForm.form.get(field).updateValueAndValidity(); this.materialForm.form.get(field).updateValueAndValidity();
}); });
} }
} }
// Attach validators specified in range to input with name // Attach validators specified in range to input with name
attachValidator(form, name: string, range: {[prop: string]: any}) { attachValidator(form, name: string, range: {[prop: string]: any}) {
if (form && form.form.get(name)) { if (form && form.form.get(name)) {
const validators = []; const validators = [];
if (range.hasOwnProperty('required')) { if (range.hasOwnProperty('required')) {
validators.push(Validators.required); validators.push(Validators.required);
} }
if (range.hasOwnProperty('values')) { if (range.hasOwnProperty('values')) {
validators.push(this.validation.generate('stringOf', [range.values])); validators.push(this.validation.generate('stringOf', [range.values]));
} }
else if (range.hasOwnProperty('min') && range.hasOwnProperty('max')) { else if (range.hasOwnProperty('min') && range.hasOwnProperty('max')) {
validators.push(this.validation.generate('minMax', [range.min, range.max])); validators.push(this.validation.generate('minMax', [range.min, range.max]));
} }
else if (range.hasOwnProperty('min')) { else if (range.hasOwnProperty('min')) {
validators.push(this.validation.generate('min', [range.min])); validators.push(this.validation.generate('min', [range.min]));
} }
else if (range.hasOwnProperty('max')) { else if (range.hasOwnProperty('max')) {
validators.push(this.validation.generate('max', [range.max])); validators.push(this.validation.generate('max', [range.max]));
} }
form.form.get(name).setValidators(validators); form.form.get(name).setValidators(validators);
} }
} }
materialSave() { materialSave() {
this.api.put('/material/' + this.material._id, this.material.sendFormat(), () => { this.api.put('/material/' + this.material._id, this.material.sendFormat(), () => {
delete this.d.arr.materials; // Reload materials delete this.d.arr.materials; // Reload materials
this.d.load('materials'); this.d.load('materials');
this.router.navigate(['/materials']); this.router.navigate(['/materials']);
}); });
} }
deleteConfirm(modal) { deleteConfirm(modal) {
this.modal.open(modal).then(result => { this.modal.open(modal).then(result => {
if (result) { if (result) {
this.api.delete('/material/' + this.material._id, (ignore, error) => { this.api.delete('/material/' + this.material._id, (ignore, error) => {
if (error) { // Material cannot be deleted as it is still referenced by active samples if (error) { // Material cannot be deleted as it is still referenced by active samples
const modalRef = this.modal.openComponent(ErrorComponent); const modalRef = this.modal.openComponent(ErrorComponent);
modalRef.instance.message = 'Cannot delete material as it is still in use!'; modalRef.instance.message = 'Cannot delete material as it is still in use!';
} }
else { else {
this.router.navigate(['/materials']); this.router.navigate(['/materials']);
} }
}); });
} }
}); });
} }
checkTypo(event, list, mKey, modal: TemplateRef<any>) { checkTypo(event, list, mKey, modal: TemplateRef<any>) {
// User did not click on suggestion and entry is not in list // User did not click on suggestion and entry is not in list
if (!(event.relatedTarget && (event.relatedTarget.className.indexOf('rb-dropdown-item') >= 0 || if (!(event.relatedTarget && (event.relatedTarget.className.indexOf('rb-dropdown-item') >= 0 ||
event.relatedTarget.className.indexOf('close-btn rb-btn rb-passive-link') >= 0)) && event.relatedTarget.className.indexOf('close-btn rb-btn rb-passive-link') >= 0)) &&
this.d.arr[list].indexOf(this.material[mKey]) < 0) { this.d.arr[list].indexOf(this.material[mKey]) < 0) {
this.modalText.list = mKey; this.modalText.list = mKey;
this.modalText.suggestion = this.d.arr[list] // Find possible entry from list this.modalText.suggestion = this.d.arr[list] // Find possible entry from list
.map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[mKey])})) .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[mKey])}))
.sort((a, b) => b.s - a.s)[0].v; .sort((a, b) => b.s - a.s)[0].v;
this.modal.open(modal).then(result => { this.modal.open(modal).then(result => {
if (result) { // Use suggestion if (result) { // Use suggestion
this.material[mKey] = this.modalText.suggestion; this.material[mKey] = this.modalText.suggestion;
} }
}); });
} }
} }
} }

View File

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

View File

@ -1,63 +1,63 @@
.paging { .paging {
height: 50px; height: 50px;
float: left; float: left;
rb-form-input { rb-form-input {
max-width: 65px; max-width: 65px;
} }
> * { > * {
float: left; float: left;
} }
> button { > button {
margin-top: 18px; margin-top: 18px;
} }
> span { > span {
margin-top: 20px; margin-top: 20px;
margin-left: 5px; margin-left: 5px;
} }
} }
.status-selection { .status-selection {
overflow: hidden; overflow: hidden;
margin-bottom: 10px; margin-bottom: 10px;
float: left; float: left;
margin-right: 15px; margin-right: 15px;
label { label {
display: block; display: block;
font-weight: 700; font-weight: 700;
font-size: 10px; font-size: 10px;
} }
rb-form-checkbox { rb-form-checkbox {
float: left; float: left;
margin-right: 10px; margin-right: 10px;
margin-top: -10px; margin-top: -10px;
} }
} }
.header-addnew { .header-addnew {
& > * { & > * {
display: inline; display: inline;
margin-bottom: 10px; margin-bottom: 10px;
} }
rb-icon-button { rb-icon-button {
float: right; float: right;
} }
} }
.material-search { .material-search {
width: 300px; width: 300px;
float: left; float: left;
position: relative; position: relative;
span { span {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 24px; top: 24px;
} }
} }

View File

@ -6,92 +6,92 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
@Component({ @Component({
selector: 'app-materials', selector: 'app-materials',
templateUrl: './materials.component.html', templateUrl: './materials.component.html',
styleUrls: ['./materials.component.scss'] styleUrls: ['./materials.component.scss']
}) })
export class MaterialsComponent implements OnInit { export class MaterialsComponent implements OnInit {
materials: MaterialModel[] = []; // All materials materials: MaterialModel[] = []; // All materials
templateKeys: {key: string, label: string}[] = []; // Material template keys templateKeys: {key: string, label: string}[] = []; // Material template keys
materialStatus = {validated: true, new: true, deleted: false}; // Material statuses to show materialStatus = {validated: true, new: true, deleted: false}; // Material statuses to show
materialSearch = ''; // Material name search string materialSearch = ''; // Material name search string
sampleSelect = false; // Set to true to show checkboxes for validation sampleSelect = false; // Set to true to show checkboxes for validation
page = 1; // Page settings page = 1; // Page settings
pages = 0; pages = 0;
pageSize = 25; pageSize = 25;
constructor( constructor(
private api: ApiService, private api: ApiService,
public d: DataService, public d: DataService,
private modal: ModalService private modal: ModalService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadMaterials(); this.loadMaterials();
this.d.load('materialTemplates', () => { this.d.load('materialTemplates', () => {
this.d.arr.materialTemplates.forEach(template => { this.d.arr.materialTemplates.forEach(template => {
template.parameters.forEach(parameter => { template.parameters.forEach(parameter => {
this.templateKeys.push({key: parameter.name, label: `${this.ucFirst(template.name)} ${parameter.name}`}); this.templateKeys.push({key: parameter.name, label: `${this.ucFirst(template.name)} ${parameter.name}`});
}); });
}); });
// Filter out duplicates // Filter out duplicates
this.templateKeys = this.templateKeys.filter((e, i, a) => !a.slice(0, i).find(el => el.key === e.key)); this.templateKeys = this.templateKeys.filter((e, i, a) => !a.slice(0, i).find(el => el.key === e.key));
}); });
} }
loadMaterials() { loadMaterials() {
this.api.get<MaterialModel[]>('/materials?' + this.api.get<MaterialModel[]>('/materials?' +
Object.entries(this.materialStatus).filter(e => e[1]).map(e => 'status[]=' + e[0]).join('&'), data => { 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.materials = data.map(e => new MaterialModel().deserialize(e));
this.pages = Math.ceil(this.materials.length / this.pageSize); this.pages = Math.ceil(this.materials.length / this.pageSize);
this.page = 1; this.page = 1;
}); });
} }
validate() { validate() {
if (this.sampleSelect) { // Selection was done do actual validation if (this.sampleSelect) { // Selection was done do actual validation
this.materials.forEach(sample => { this.materials.forEach(sample => {
if (sample.selected) { if (sample.selected) {
this.api.put('/material/validate/' + sample._id); this.api.put('/material/validate/' + sample._id);
} }
}); });
this.loadMaterials(); this.loadMaterials();
this.sampleSelect = false; this.sampleSelect = false;
} }
else { // Activate validation mode else { // Activate validation mode
this.sampleSelect = true; this.sampleSelect = true;
} }
} }
selectAll(event) { // Toggle selection for all items except deleted ones selectAll(event) { // Toggle selection for all items except deleted ones
this.materials.forEach(material => { this.materials.forEach(material => {
if (material.status !== 'deleted') { if (material.status !== 'deleted') {
material.selected = event.target.checked; material.selected = event.target.checked;
} }
else { else {
material.selected = false; material.selected = false;
} }
}); });
} }
restoreMaterial(id, modal) { restoreMaterial(id, modal) {
this.modal.open(modal).then(res => { this.modal.open(modal).then(res => {
if (res) { if (res) {
this.api.put('/sample/restore/' + id, {}, ignore => { this.api.put('/sample/restore/' + id, {}, ignore => {
this.materials.find(e => e._id === id).status = 'new'; this.materials.find(e => e._id === id).status = 'new';
}); });
} }
}); });
} }
ucFirst(string) { // Convert first character of string to uppercase ucFirst(string) { // Convert first character of string to uppercase
return string[0].toUpperCase() + string.slice(1); return string[0].toUpperCase() + string.slice(1);
} }
materialFilter(ms) { // Filter function for material names materialFilter(ms) { // Filter function for material names
return e => e.name.indexOf(ms) >= 0; return e => e.name.indexOf(ms) >= 0;
} }
} }

View File

@ -1,73 +1,73 @@
<rb-icon-button icon="add" mode="primary" (click)="newModel = !newModel; oldModelGroup = ''" class="space-below"> <rb-icon-button icon="add" mode="primary" (click)="newModel = !newModel; oldModelGroup = ''" class="space-below">
New model New model
</rb-icon-button> </rb-icon-button>
<form *ngIf="newModel" #modelForm="ngForm"> <form *ngIf="newModel" #modelForm="ngForm">
<rb-form-input name="group" label="group" appValidate="string" required [(ngModel)]="modelGroup" #groupInput="ngModel" <rb-form-input name="group" label="group" appValidate="string" required [(ngModel)]="modelGroup" #groupInput="ngModel"
[rbFormInputAutocomplete]="autocomplete.bind(this, groups)" [rbFormInputAutocomplete]="autocomplete.bind(this, groups)"
[rbDebounceTime]="0" [rbInitialOpen]="true"> [rbDebounceTime]="0" [rbInitialOpen]="true">
<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="name" label="name" appValidate="string" required [(ngModel)]="model.name" #nameInput="ngModel"> <rb-form-input name="name" label="name" appValidate="string" required [(ngModel)]="model.name" #nameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="url" label="URL" appValidate="url" required [(ngModel)]="model.url" #urlInput="ngModel"> <rb-form-input name="url" label="URL" appValidate="url" required [(ngModel)]="model.url" #urlInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{urlInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{urlInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!modelForm.form.valid" (click)="saveModel()"> <rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!modelForm.form.valid" (click)="saveModel()">
Save model Save model
</rb-icon-button> </rb-icon-button>
</form> </form>
<rb-table class="space-above space-below"> <rb-table class="space-above space-below">
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>URL</th> <th>URL</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
<ng-container *ngFor="let group of d.arr.modelGroups"> <ng-container *ngFor="let group of d.arr.modelGroups">
<tr><th>{{group.group}}</th><th></th><th></th><th></th><th></th></tr> <tr><th>{{group.group}}</th><th></th><th></th><th></th><th></th></tr>
<tr *ngFor="let modelItem of group.models"> <tr *ngFor="let modelItem of group.models">
<td>{{modelItem.name}}</td> <td>{{modelItem.name}}</td>
<td>{{modelItem.url}}</td> <td>{{modelItem.url}}</td>
<td> <td>
<span class="rb-ic rb-ic-edit clickable" <span class="rb-ic rb-ic-edit clickable"
(click)="modelGroup = group.group; (click)="modelGroup = group.group;
oldModelGroup = group.group; oldModelGroup = group.group;
oldModelName = modelItem.name; oldModelName = modelItem.name;
model = modelItem; model = modelItem;
newModel = true;"> newModel = true;">
</span> </span>
</td> </td>
<td> <td>
<span class="rb-ic rb-ic-delete clickable" <span class="rb-ic rb-ic-delete clickable"
(click)="delete(modalDeleteConfirm, modelItem.name, group.group)"></span> (click)="delete(modalDeleteConfirm, modelItem.name, group.group)"></span>
</td> </td>
</tr> </tr>
</ng-container> </ng-container>
</rb-table> </rb-table>
<rb-table> <rb-table>
<tr> <tr>
<th>Model files</th> <th>Model files</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
<tr *ngFor="let file of d.arr.modelFiles"> <tr *ngFor="let file of d.arr.modelFiles">
<td>{{file.name}}</td> <td>{{file.name}}</td>
<td>{{file.size | size:'M'}}</td> <td>{{file.size | size:'M'}}</td>
<td><span class="rb-ic rb-ic-delete clickable" (click)="delete(modalDeleteConfirm, file.name)"></span></td> <td><span class="rb-ic rb-ic-delete clickable" (click)="delete(modalDeleteConfirm, file.name)"></span></td>
</tr> </tr>
</rb-table> </rb-table>
<ng-template #modalDeleteConfirm> <ng-template #modalDeleteConfirm>
<rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete model" cancelBtnLabel="Cancel"> <rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete model" cancelBtnLabel="Cancel">
Do you really want to delete? Do you really want to delete?
</rb-alert> </rb-alert>
</ng-template> </ng-template>

View File

@ -7,77 +7,77 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
@Component({ @Component({
selector: 'app-model-templates', selector: 'app-model-templates',
templateUrl: './model-templates.component.html', templateUrl: './model-templates.component.html',
styleUrls: ['./model-templates.component.scss'] styleUrls: ['./model-templates.component.scss']
}) })
export class ModelTemplatesComponent implements OnInit { export class ModelTemplatesComponent implements OnInit {
newModel = false; // Display new model dialog newModel = false; // Display new model dialog
modelGroup = ''; // Group of the edited model modelGroup = ''; // Group of the edited model
oldModelGroup = ''; // Group of the edited model before editing started oldModelGroup = ''; // Group of the edited model before editing started
oldModelName = ''; // Name of the edited model before editing started oldModelName = ''; // Name of the edited model before editing started
model = new ModelItemModel().models[0]; // Edited model model = new ModelItemModel().models[0]; // Edited model
groups = []; // All model group names groups = []; // All model group names
constructor( constructor(
private api: ApiService, private api: ApiService,
public autocomplete: AutocompleteService, public autocomplete: AutocompleteService,
public d: DataService, public d: DataService,
private modal: ModalService private modal: ModalService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadGroups(); this.loadGroups();
} }
loadGroups() { loadGroups() {
delete this.d.arr.modelGroups; delete this.d.arr.modelGroups;
this.d.load('modelGroups', () => { this.d.load('modelGroups', () => {
this.groups = this.d.arr.modelGroups.map(e => e.group); this.groups = this.d.arr.modelGroups.map(e => e.group);
}); });
this.d.load('modelFiles'); this.d.load('modelFiles');
} }
saveModel() { saveModel() {
// Group was changed, delete model in old group // Group was changed, delete model in old group
if (this.oldModelGroup !== '' && this.modelGroup !== this.oldModelGroup) { if (this.oldModelGroup !== '' && this.modelGroup !== this.oldModelGroup) {
this.delete(null, this.oldModelName, this.oldModelGroup); this.delete(null, this.oldModelName, this.oldModelGroup);
} }
this.api.post('/model/' + this.modelGroup, omit(this.model, '_id'), () => { this.api.post('/model/' + this.modelGroup, omit(this.model, '_id'), () => {
this.newModel = false; // Reset model edit parameters this.newModel = false; // Reset model edit parameters
this.loadGroups(); this.loadGroups();
this.modelGroup = ''; this.modelGroup = '';
this.oldModelGroup = ''; this.oldModelGroup = '';
this.oldModelName = ''; this.oldModelName = '';
this.model = new ModelItemModel().models[0]; this.model = new ModelItemModel().models[0];
}); });
} }
delete(modal, name, group = null) { delete(modal, name, group = null) {
new Promise(resolve => { new Promise(resolve => {
if (modal) { // If modal was given, wait for result if (modal) { // If modal was given, wait for result
this.modal.open(modal).then(result => { this.modal.open(modal).then(result => {
resolve(result); resolve(result);
}); });
} }
else { else {
resolve(true); resolve(true);
} }
}).then(res => { }).then(res => {
if (res) { if (res) {
if (group) { // Delete model group if given if (group) { // Delete model group if given
this.api.delete(`/model/${group}/${name}`, () => { this.api.delete(`/model/${group}/${name}`, () => {
this.loadGroups(); this.loadGroups();
}); });
} }
else { // Delete model file else { // Delete model file
this.api.delete(`/model/file/${name}`, () => { this.api.delete(`/model/file/${name}`, () => {
this.d.arr.modelFiles.splice(this.d.arr.modelFiles.findIndex(e => e.name === name), 1); this.d.arr.modelFiles.splice(this.d.arr.modelFiles.findIndex(e => e.name === name), 1);
}); });
} }
} }
}); });
} }
} }

View File

@ -1,7 +1,7 @@
import { BaseModel } from './base.model'; import { BaseModel } from './base.model';
describe('BaseModel', () => { describe('BaseModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new BaseModel()).toBeTruthy(); expect(new BaseModel()).toBeTruthy();
}); });
}); });

View File

@ -1,10 +1,10 @@
export class BaseModel { export class BaseModel {
deserialize(input: any): this { deserialize(input: any): this {
Object.assign(this, input); Object.assign(this, input);
return this; return this;
} }
sendFormat(): this { sendFormat(): this {
return this; return this;
} }
} }

View File

@ -1,7 +1,7 @@
import { ChangelogModel } from './changelog.model'; import { ChangelogModel } from './changelog.model';
describe('ChangelogModel', () => { describe('ChangelogModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new ChangelogModel()).toBeTruthy(); expect(new ChangelogModel()).toBeTruthy();
}); });
}); });

View File

@ -2,10 +2,10 @@ import {BaseModel} from './base.model';
import {IdModel} from './id.model'; import {IdModel} from './id.model';
export class ChangelogModel extends BaseModel { export class ChangelogModel extends BaseModel {
_id: IdModel = null; _id: IdModel = null;
date: Date; date: Date;
action: string; action: string;
collection: string; collection: string;
conditions: {[key: string]: any}; conditions: {[key: string]: any};
data: {[key: string]: any}; data: {[key: string]: any};
} }

View File

@ -1,7 +1,7 @@
import { CustomFieldsModel } from './custom-fields.model'; import { CustomFieldsModel } from './custom-fields.model';
describe('CustomFieldsModel', () => { describe('CustomFieldsModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new CustomFieldsModel()).toBeTruthy(); expect(new CustomFieldsModel()).toBeTruthy();
}); });
}); });

View File

@ -2,6 +2,6 @@ import {BaseModel} from './base.model';
export class CustomFieldsModel extends BaseModel { export class CustomFieldsModel extends BaseModel {
name = ''; name = '';
qty = 0; qty = 0;
} }

View File

@ -1,7 +1,7 @@
import { HelpModel } from './help.model'; import { HelpModel } from './help.model';
describe('Help.Model', () => { describe('Help.Model', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new HelpModel()).toBeTruthy(); expect(new HelpModel()).toBeTruthy();
}); });
}); });

View File

@ -1,6 +1,6 @@
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class HelpModel extends BaseModel { export class HelpModel extends BaseModel {
text = ''; text = '';
level = 'none'; level = 'none';
} }

View File

@ -1,7 +1,7 @@
import { MaterialModel } from './material.model'; import { MaterialModel } from './material.model';
describe('MaterialModel', () => { describe('MaterialModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new MaterialModel()).toBeTruthy(); expect(new MaterialModel()).toBeTruthy();
}); });
}); });

View File

@ -3,16 +3,16 @@ import {IdModel} from './id.model';
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class MaterialModel extends BaseModel { export class MaterialModel extends BaseModel {
_id: IdModel = null; _id: IdModel = null;
name = ''; name = '';
supplier = ''; supplier = '';
group = ''; group = '';
properties: {material_template: string, [prop: string]: string} = {material_template: null}; properties: {material_template: string, [prop: string]: string} = {material_template: null};
numbers: string[] = ['']; numbers: string[] = [''];
selected = false; selected = false;
status = ''; status = '';
sendFormat() { sendFormat() {
return pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']); return pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']);
} }
} }

View File

@ -1,7 +1,7 @@
import { MeasurementModel } from './measurement.model'; import { MeasurementModel } from './measurement.model';
describe('MeasurementModel', () => { describe('MeasurementModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new MeasurementModel()).toBeTruthy(); expect(new MeasurementModel()).toBeTruthy();
}); });
}); });

View File

@ -4,28 +4,28 @@ import {IdModel} from './id.model';
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class MeasurementModel extends BaseModel { export class MeasurementModel extends BaseModel {
_id: IdModel = null; _id: IdModel = null;
sample_id: IdModel = null; sample_id: IdModel = null;
measurement_template: IdModel; measurement_template: IdModel;
values: {[prop: string]: any} = {}; values: {[prop: string]: any} = {};
status = ''; status = '';
constructor(measurementTemplate: IdModel = null) { constructor(measurementTemplate: IdModel = null) {
super(); super();
this.measurement_template = measurementTemplate; this.measurement_template = measurementTemplate;
} }
deserialize(input: any): this { deserialize(input: any): this {
Object.assign(this, input); Object.assign(this, input);
Object.keys(this.values).forEach(key => { Object.keys(this.values).forEach(key => {
if (this.values[key] === null) { if (this.values[key] === null) {
this.values[key] = ''; this.values[key] = '';
} }
}); });
return this; return this;
} }
sendFormat(omitValues = []) { sendFormat(omitValues = []) {
return omit(pick(this, ['sample_id', 'measurement_template', 'values']), omitValues); return omit(pick(this, ['sample_id', 'measurement_template', 'values']), omitValues);
} }
} }

View File

@ -1,7 +1,7 @@
import { ModelFileModel } from './model-file.model'; import { ModelFileModel } from './model-file.model';
describe('ModelFile.Model', () => { describe('ModelFile.Model', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new ModelFileModel()).toBeTruthy(); expect(new ModelFileModel()).toBeTruthy();
}); });
}); });

View File

@ -1,6 +1,6 @@
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class ModelFileModel extends BaseModel { export class ModelFileModel extends BaseModel {
name = ''; name = '';
size = 0; size = 0;
} }

View File

@ -1,7 +1,7 @@
import { ModelItemModel } from './model-item.model'; import { ModelItemModel } from './model-item.model';
describe('ModelItemModel', () => { describe('ModelItemModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new ModelItemModel()).toBeTruthy(); expect(new ModelItemModel()).toBeTruthy();
}); });
}); });

View File

@ -1,9 +1,9 @@
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class ModelItemModel extends BaseModel { export class ModelItemModel extends BaseModel {
group = ''; group = '';
models = [{ models = [{
name: '', name: '',
url: '' url: ''
}]; }];
} }

View File

@ -1,7 +1,7 @@
import { SampleModel } from './sample.model'; import { SampleModel } from './sample.model';
describe('SampleModel', () => { describe('SampleModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new SampleModel()).toBeTruthy(); expect(new SampleModel()).toBeTruthy();
}); });
}); });

View File

@ -6,63 +6,63 @@ import {MeasurementModel} from './measurement.model';
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class SampleModel extends BaseModel { export class SampleModel extends BaseModel {
_id: IdModel = null; _id: IdModel = null;
color = ''; color = '';
number = ''; number = '';
type = ''; type = '';
batch = ''; batch = '';
condition: {condition_template: string, [prop: string]: string} = {condition_template: null}; condition: {condition_template: string, [prop: string]: string} = {condition_template: null};
material_id: IdModel = null; material_id: IdModel = null;
material: MaterialModel; material: MaterialModel;
measurements: MeasurementModel[] = []; measurements: MeasurementModel[] = [];
note_id: IdModel = null; note_id: IdModel = null;
user_id: IdModel = null; user_id: IdModel = null;
selected = false; selected = false;
notes: { notes: {
comment: string, comment: string,
sample_references: {sample_id: IdModel, relation: string}[], sample_references: {sample_id: IdModel, relation: string}[],
custom_fields: {[prop: string]: string} custom_fields: {[prop: string]: string}
} = {comment: '', sample_references: [], custom_fields: {}}; } = {comment: '', sample_references: [], custom_fields: {}};
status = ''; status = '';
added: Date = null; added: Date = null;
deserialize(input: any): this { deserialize(input: any): this {
Object.assign(this, input); Object.assign(this, input);
if (input.hasOwnProperty('material')) { if (input.hasOwnProperty('material')) {
this.material = new MaterialModel().deserialize(input.material); this.material = new MaterialModel().deserialize(input.material);
this.material_id = input.material._id; this.material_id = input.material._id;
} }
if (input.hasOwnProperty('measurements')) { if (input.hasOwnProperty('measurements')) {
this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e)); this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e));
} }
if (input.hasOwnProperty('added')) { if (input.hasOwnProperty('added')) {
this.added = new Date(input.added); this.added = new Date(input.added);
} }
return this; return this;
} }
sendFormat(pickCondition = true) { sendFormat(pickCondition = true) {
const pickFields = ['color', 'type', 'batch', 'material_id', 'notes']; const pickFields = ['color', 'type', 'batch', 'material_id', 'notes'];
if (pickCondition) { if (pickCondition) {
pickFields.push('condition'); pickFields.push('condition');
} }
const tmp = pick(this.conditionTemplateCheck(), pickFields); const tmp = pick(this.conditionTemplateCheck(), pickFields);
Object.keys(tmp).forEach(key => { Object.keys(tmp).forEach(key => {
if (tmp[key] === undefined) { if (tmp[key] === undefined) {
delete tmp[key]; delete tmp[key];
} }
}); });
if (this.material && this.material.name === undefined) { if (this.material && this.material.name === undefined) {
delete tmp.material_id; delete tmp.material_id;
} }
return tmp; return tmp;
} }
private conditionTemplateCheck() { private conditionTemplateCheck() {
const res = cloneDeep(this); const res = cloneDeep(this);
if (res.condition.condition_template === null) { if (res.condition.condition_template === null) {
res.condition = {}; res.condition = {};
} }
return res; return res;
} }
} }

View File

@ -1,7 +1,7 @@
import { TemplateModel } from './template.model'; import { TemplateModel } from './template.model';
describe('TemplateModel', () => { describe('TemplateModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new TemplateModel()).toBeTruthy(); expect(new TemplateModel()).toBeTruthy();
}); });
}); });

View File

@ -2,9 +2,9 @@ import {IdModel} from './id.model';
import {BaseModel} from './base.model'; import {BaseModel} from './base.model';
export class TemplateModel extends BaseModel { export class TemplateModel extends BaseModel {
_id: IdModel = null; _id: IdModel = null;
name = ''; name = '';
version = 0; version = 0;
first_id: IdModel = null; first_id: IdModel = null;
parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = []; parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = [];
} }

View File

@ -1,7 +1,7 @@
import { UserModel } from './user.model'; import { UserModel } from './user.model';
describe('UserModel', () => { describe('UserModel', () => {
it('should create an instance', () => { it('should create an instance', () => {
expect(new UserModel()).toBeTruthy(); expect(new UserModel()).toBeTruthy();
}); });
}); });

View File

@ -3,31 +3,31 @@ import {BaseModel} from './base.model';
import {IdModel} from './id.model'; import {IdModel} from './id.model';
export class UserModel extends BaseModel{ export class UserModel extends BaseModel{
_id: IdModel = null; _id: IdModel = null;
name = ''; name = '';
origName = ''; origName = '';
email = ''; email = '';
level = ''; level = '';
location = ''; location = '';
devices = ['']; devices = [''];
models = ['']; models = [''];
status = 'new'; status = 'new';
edit = false; edit = false;
deserialize(input: any): this { deserialize(input: any): this {
Object.assign(this, input); Object.assign(this, input);
this.origName = this.name; this.origName = this.name;
return this; return this;
} }
sendFormat(mode = 'user') { sendFormat(mode = 'user') {
const keys = ['name', 'email', 'location', 'devices']; const keys = ['name', 'email', 'location', 'devices'];
if (mode === 'admin') { if (mode === 'admin') {
keys.push('level'); keys.push('level');
keys.push('models'); keys.push('models');
this.devices = this.devices.filter(e => e); this.devices = this.devices.filter(e => e);
this.models = this.models.filter(e => e); this.models = this.models.filter(e => e);
} }
return pick(this, keys); return pick(this, keys);
} }
} }

View File

@ -2,8 +2,8 @@ import { ObjectPipe } from './object.pipe';
describe('ObjectPipe', () => { describe('ObjectPipe', () => {
it('create an instance', () => { it('create an instance', () => {
const pipe = new ObjectPipe(); const pipe = new ObjectPipe();
expect(pipe).toBeTruthy(); expect(pipe).toBeTruthy();
}); });
}); });

View File

@ -2,14 +2,14 @@ import { Pipe, PipeTransform } from '@angular/core';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
@Pipe({ @Pipe({
name: 'object', name: 'object',
pure: true pure: true
}) })
export class ObjectPipe implements PipeTransform { export class ObjectPipe implements PipeTransform {
transform(value: object, omitValue: string[] = []): string { transform(value: object, omitValue: string[] = []): string {
const res = omit(value, omitValue); const res = omit(value, omitValue);
return res && Object.keys(res).length ? JSON.stringify(res) : ''; return res && Object.keys(res).length ? JSON.stringify(res) : '';
} }
} }

View File

@ -2,8 +2,8 @@ import { ParametersPipe } from './parameters.pipe';
describe('ParametersPipe', () => { describe('ParametersPipe', () => {
it('create an instance', () => { it('create an instance', () => {
const pipe = new ParametersPipe(); const pipe = new ParametersPipe();
expect(pipe).toBeTruthy(); expect(pipe).toBeTruthy();
}); });
}); });

View File

@ -1,13 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'parameters' name: 'parameters'
}) })
export class ParametersPipe implements PipeTransform { export class ParametersPipe implements PipeTransform {
transform(value: {name: string, range: object}[]): string { transform(value: {name: string, range: object}[]): string {
return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any') return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any')
.replace(/["{}]/g, '')}>`).join(', ')}}`; .replace(/["{}]/g, '')}>`).join(', ')}}`;
} }
} }

View File

@ -1,93 +1,93 @@
<rb-tab-panel (tabChanged)="groupChange($event)"> <rb-tab-panel (tabChanged)="groupChange($event)">
<ng-container *ngFor="let group of d.arr.modelGroups; index as i"> <ng-container *ngFor="let group of d.arr.modelGroups; index as i">
<div *rbTabPanelItem="group.group; id: i"></div> <div *rbTabPanelItem="group.group; id: i"></div>
</ng-container> </ng-container>
</rb-tab-panel> </rb-tab-panel>
<rb-form-select label="Model" (change)="result = undefined" [(ngModel)]="activeModelIndex"> <rb-form-select label="Model" (change)="result = undefined" [(ngModel)]="activeModelIndex">
<option *ngFor="let model of activeGroup.models; index as i" [value]="i">{{model.name}}</option> <option *ngFor="let model of activeGroup.models; index as i" [value]="i">{{model.name}}</option>
</rb-form-select> </rb-form-select>
<div *ngIf="result" class="result" [@inOut]> <div *ngIf="result" class="result" [@inOut]>
<ng-container *ngIf="multipleSamples; else singleSampleResult"> <ng-container *ngIf="multipleSamples; else singleSampleResult">
<h4 *ngFor="let prediction of result.predictions; index as i"> <h4 *ngFor="let prediction of result.predictions; index as i">
{{spectrumNames[i]}}: {{spectrumNames[i]}}:
<span *ngFor="let predictionEntry of prediction"> <span *ngFor="let predictionEntry of prediction">
{{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}} {{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}}
</span> </span>
<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a> <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
</h4> </h4>
</ng-container> </ng-container>
<ng-template #singleSampleResult> <ng-template #singleSampleResult>
<h4> <h4>
Average result: Average result:
<span *ngFor="let predictionEntry of result.mean"> <span *ngFor="let predictionEntry of result.mean">
{{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}} &nbsp;{{( predictionEntry.std !== '' ? ' (standard deviation: '+ predictionEntry.std+')' : '') }} {{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}} &nbsp;{{( predictionEntry.std !== '' ? ' (standard deviation: '+ predictionEntry.std+')' : '') }}
</span> </span>
<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a> <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
</h4> </h4>
<a 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"> <div *ngIf="triggerDetails.open" class="space-below">
<p *ngFor="let prediction of result.predictions; index as i"> <p *ngFor="let prediction of result.predictions; index as i">
{{spectrumNames[i]}}: {{spectrumNames[i]}}:
<span *ngFor="let predictionEntry of prediction"> <span *ngFor="let predictionEntry of prediction">
{{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}} {{predictionEntry.category}}&nbsp;<span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span>&nbsp;{{predictionEntry.label}}
</span> </span>
<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a> <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
</p> </p>
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div class="file-input space-below"> <div class="file-input space-below">
<rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" multiple <rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" multiple
(ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel> (ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel>
</rb-form-file> </rb-form-file>
<rb-loading-spinner *ngIf="loading; else predictButton"></rb-loading-spinner> <rb-loading-spinner *ngIf="loading; else predictButton"></rb-loading-spinner>
<ng-template #predictButton> <ng-template #predictButton>
<rb-icon-button icon="forward-right" mode="primary" *ngIf="spectrumNames.length; else placeholder" <rb-icon-button icon="forward-right" mode="primary" *ngIf="spectrumNames.length; else placeholder"
(click)="loadPrediction()"> (click)="loadPrediction()">
Predict Predict
</rb-icon-button> </rb-icon-button>
<ng-template #placeholder><div></div></ng-template> <ng-template #placeholder><div></div></ng-template>
</ng-template> </ng-template>
<div> <div>
Prediction of: Prediction of:
<rb-form-radio name="multiple-samples" label="Single sample" [(ngModel)]="multipleSamples" [value]="false"> <rb-form-radio name="multiple-samples" label="Single sample" [(ngModel)]="multipleSamples" [value]="false">
</rb-form-radio> </rb-form-radio>
<rb-form-radio name="multiple-samples" label="Multiple samples" [(ngModel)]="multipleSamples" [value]="true"> <rb-form-radio name="multiple-samples" label="Multiple samples" [(ngModel)]="multipleSamples" [value]="true">
</rb-form-radio> </rb-form-radio>
</div> </div>
</div> </div>
<!-- CSV export --> <!-- CSV export -->
<rb-icon-button icon="forward-right" mode="secondary" (click)="exportCSV()" *ngIf="spectrumNames.length" style="margin-right: 0.5rem"> <rb-icon-button icon="forward-right" mode="secondary" (click)="exportCSV()" *ngIf="spectrumNames.length" style="margin-right: 0.5rem">
Export to CSV Export to CSV
</rb-icon-button> </rb-icon-button>
<!-- PDF exprot --> <!-- PDF exprot -->
<rb-icon-button icon="forward-right" mode="secondary" (click)="exportPDF()" *ngIf="spectrumNames.length"> <rb-icon-button icon="forward-right" mode="secondary" (click)="exportPDF()" *ngIf="spectrumNames.length">
Export to PDF Export to PDF
</rb-icon-button> </rb-icon-button>
<div class="dpt-chart space-below"> <div class="dpt-chart space-below">
<canvas baseChart <canvas baseChart
class="dpt-chart" class="dpt-chart"
[datasets]="chart" [datasets]="chart"
[labels]="[]" [labels]="[]"
[options]="chartOptions" [options]="chartOptions"
[legend]="false" [legend]="false"
chartType="scatter"> chartType="scatter">
</canvas> </canvas>
</div> </div>
<div class="shaded-container" id="disclaimer"> <div class="shaded-container" id="disclaimer">
<h4><sup>#</sup>Disclaimer: This tool is still under development</h4> <h4><sup>#</sup>Disclaimer: This tool is still under development</h4>
<p> <p>
The prediction and classification of material parameters are validated only for certain conditions. The prediction and classification of material parameters are validated only for certain conditions.
These results may therefore under no circumstances be used to evaluate quality-relevant issues. <br> These results may therefore under no circumstances be used to evaluate quality-relevant issues. <br>
For more details please contact <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a>. For more details please contact <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a>.
</p> </p>
</div> </div>

View File

@ -1,20 +1,20 @@
.dpt-chart { .dpt-chart {
max-width: 800px; max-width: 800px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.file-input { .file-input {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-column-gap: 1rem; grid-column-gap: 1rem;
} }
.result { .result {
margin: 30px 0; margin: 30px 0;
h4 { h4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
} }

View File

@ -15,267 +15,266 @@ import * as pdfFonts from 'pdfmake/build/vfs_fonts';
@Component({ @Component({
selector: 'app-prediction', selector: 'app-prediction',
templateUrl: './prediction.component.html', templateUrl: './prediction.component.html',
styleUrls: ['./prediction.component.scss'], styleUrls: ['./prediction.component.scss'],
animations: [ animations: [
trigger( trigger(
'inOut', [ 'inOut', [
transition(':enter', [ transition(':enter', [
style({height: 0, opacity: 0}), style({height: 0, opacity: 0}),
animate('0.5s ease-out', style({height: '*', opacity: 1})) animate('0.5s ease-out', style({height: '*', opacity: 1}))
]), ]),
transition(':leave', [ transition(':leave', [
style({height: '*', opacity: 1}), style({height: '*', opacity: 1}),
animate('0.5s ease-in', style({height: 0, opacity: 0})) animate('0.5s ease-in', style({height: 0, opacity: 0}))
]) ])
] ]
) )
] ]
}) })
export class PredictionComponent implements OnInit { export class PredictionComponent implements OnInit {
result: { predictions: any[], mean: any[] }; // Prediction result from python container result: { predictions: any[], mean: any[] }; // Prediction result from python container
loading = false; loading = false;
activeGroup: ModelItemModel = new ModelItemModel(); activeGroup: ModelItemModel = new ModelItemModel();
activeModelIndex = 0; activeModelIndex = 0;
// If true, spectra belong to different samples, otherwise multiple spectra from the same sample are given // If true, spectra belong to different samples, otherwise multiple spectra from the same sample are given
multipleSamples = false; multipleSamples = false;
spectrumNames: string[] = []; spectrumNames: string[] = [];
spectrum: string[][] = [[]]; spectrum: string[][] = [[]];
flattenedSpectra = []; flattenedSpectra = [];
chart = []; chart = [];
readonly chartInit = { readonly chartInit = {
data: [], data: [],
label: 'Spectrum', label: 'Spectrum',
showLine: true, showLine: true,
fill: false, fill: false,
pointRadius: 0, pointRadius: 0,
borderColor: '#00a8b0', borderColor: '#00a8b0',
borderWidth: 2 borderWidth: 2
}; };
readonly chartOptions: ChartOptions = { readonly chartOptions: ChartOptions = {
scales: { scales: {
xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}], xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}],
yAxes: [{ticks: {}}] yAxes: [{ticks: {}}]
}, },
responsive: true, responsive: true,
tooltips: {enabled: false}, tooltips: {enabled: false},
hover: {mode: null}, hover: {mode: null},
maintainAspectRatio: true, maintainAspectRatio: true,
plugins: {datalabels: {display: false}} plugins: {datalabels: {display: false}}
}; };
constructor( constructor(
private api: ApiService, private api: ApiService,
public d: DataService, public d: DataService,
public login: LoginService public login: LoginService
) { ) {
this.chart[0] = cloneDeep(this.chartInit); this.chart[0] = cloneDeep(this.chartInit);
} }
ngOnInit(): void { ngOnInit(): void {
this.d.load('modelGroups', () => { this.d.load('modelGroups', () => {
this.activeGroup = this.d.arr.modelGroups[0]; this.activeGroup = this.d.arr.modelGroups[0];
}); });
} }
fileToArray(files) { fileToArray(files) {
this.loading = true; this.loading = true;
this.flattenedSpectra = []; this.flattenedSpectra = [];
this.chart = []; this.chart = [];
let load = files.length; let load = files.length;
this.spectrumNames = files.map(e => e.name); this.spectrumNames = files.map(e => e.name);
for (const i in files) { for (const i in files) {
if (files.hasOwnProperty(i)) { if (files.hasOwnProperty(i)) {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = () => { fileReader.onload = () => {
// Parse to database spectrum representation // Parse to database spectrum representation
this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',').map(el => parseFloat(el))) this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',').map(el => parseFloat(el)))
.filter(el => el.length === 2) as any; .filter(el => el.length === 2) as any;
// Flatten to format needed for prediction // Flatten to format needed for prediction
this.flattenedSpectra[i] = {labels: this.spectrum.map(e => e[0]), values: this.spectrum.map(e => e[1])}; this.flattenedSpectra[i] = {labels: this.spectrum.map(e => e[0]), values: this.spectrum.map(e => e[1])};
// Add to chart // Add to chart
this.chart[i] = cloneDeep(this.chartInit); this.chart[i] = cloneDeep(this.chartInit);
this.chart[i].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); this.chart[i].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])}));
load --; load --;
if (load <= 0) { // All loaded if (load <= 0) { // All loaded
this.loadPrediction(); this.loadPrediction();
} }
}; };
fileReader.readAsText(files[i]); fileReader.readAsText(files[i]);
} }
} }
} }
loadPrediction() { loadPrediction() {
this.loading = true; this.loading = true;
this.api.post<any>(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => { this.api.post<any>(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => {
let tmp = Object.entries(omit(data, ['mean', 'std', 'label'])) // Form: [[label, [{value, color}]]] let tmp = Object.entries(omit(data, ['mean', 'std', 'label'])) // Form: [[label, [{value, color}]]]
.map((entry: any) => entry[1].map(e => ({category: entry[0], label: data.label[entry[0]], value: e.value, color: e.color}))); // Form: [[{category, label, value, color}]] .map((entry: any) => entry[1].map(e => ({category: entry[0], label: data.label[entry[0]], value: e.value, color: e.color}))); // Form: [[{category, label, value, color}]]
this.result = { this.result = {
predictions: tmp[0].map((ignore, columnIndex) => tmp.map(row => row[columnIndex])), // Transpose tmp predictions: tmp[0].map((ignore, columnIndex) => tmp.map(row => row[columnIndex])), // Transpose tmp
mean: Object.entries(data.mean) mean: Object.entries(data.mean)
.map((entry:any) => ({category: entry[0], label: data.label[entry[0]], value: entry[1].value, color: entry[1].color, std: data.std[entry[0]]})) // Form: [{category, label, value, color}] .map((entry:any) => ({category: entry[0], label: data.label[entry[0]], value: entry[1].value, color: entry[1].color, std: data.std[entry[0]]})) // Form: [{category, label, value, color}]
}; };
this.loading = false; this.loading = false;
}); });
} }
groupChange(index) { // Group was changed groupChange(index) { // Group was changed
this.activeGroup = this.d.arr.modelGroups[index]; this.activeGroup = this.d.arr.modelGroups[index];
this.activeModelIndex = 0; this.activeModelIndex = 0;
this.result = undefined; this.result = undefined;
} }
// Aggregates spectrum names and prediction values into an associative array // Aggregates spectrum names and prediction values into an associative array
prepareExport() { prepareExport() {
const zip = (a, b) => a.map((k, i) => [k, b[i]]); const zip = (a, b) => a.map((k, i) => [k, b[i]]);
const values = this.result.predictions const values = this.result.predictions
.map(prediction => prediction .map(prediction => prediction
.filter(field => field.category === 'Prediction') .filter(field => field.category === 'Prediction')
.map(field => field.value)); .map(field => field.value));
return zip(this.spectrumNames, values); return zip(this.spectrumNames, values);
} }
// Generates a timestamp suitable for file naming // Generates a timestamp suitable for file naming
generateTimestamp() { generateTimestamp() {
let d = new Date(); let d = new Date();
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + '--' + d.getHours() + "-" + d.getMinutes(); return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + '--' + d.getHours() + "-" + d.getMinutes();
} }
// Converts the prediction results to a CSV file // Converts the prediction results to a CSV file
exportCSV() { exportCSV() {
const predictions = this.prepareExport(); const predictions = this.prepareExport();
const csv = predictions.map(line => line.join(";")).join("\n"); const csv = predictions.map(line => line.join(";")).join("\n");
FileSaver.saveAs(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'predictions-' + this.generateTimestamp() + '.csv'); FileSaver.saveAs(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'predictions-' + this.generateTimestamp() + '.csv');
} }
// Converts the prediction results to a PDF file // Converts the prediction results to a PDF file
exportPDF() { exportPDF() {
const doc = { const doc = {
content: [ content: [
{ {
text: 'DeFinMa - Decoding the Fingerprint of Materials by AI', text: 'DeFinMa - Decoding the Fingerprint of Materials by AI',
style: 'header' style: 'header'
}, },
{ {
table: { table: {
widths: ['auto', '*', 'auto', 'auto', 'auto'], widths: ['auto', '*', 'auto', 'auto', 'auto'],
body: [ body: [[
[ {text: new Date().toLocaleDateString(), style: 'tableHeader'},
{text: new Date().toLocaleDateString(), style: 'tableHeader'}, {text: 'Customer', style: 'tableHeader'},
{text: 'Customer', style: 'tableHeader'}, {text: 'Security class', style: 'tableHeader'},
{text: 'Security class', style: 'tableHeader'}, {text: 'Person in charge', style: 'tableHeader'},
{text: 'Person in charge', style: 'tableHeader'}, {text: 'Phone', style: 'tableHeader'}],
{text: 'Phone', style: 'tableHeader'}], [
[ this.activeGroup.group,
this.activeGroup.group, this.login.username,
this.login.username, 'Intern',
'Intern', {
{ stack: [
stack: [ 'CR/APS1-Lotter',
'CR/APS1-Lotter', 'CR/APS1-Lingenfelser'
'CR/APS1-Lingenfelser' ]
] },
}, {
{ stack: [
stack: [ '0711/811-49017',
'0711/811-49017', '0711/811-6897'
'0711/811-6897' ]
] }
} ]
] ]
] }
} },
}, {
{ text: 'Prediction of ' + this.activeGroup.group + ' (' + this.activeGroup.models[this.activeModelIndex].name + ')*',
text: 'Prediction of ' + this.activeGroup.group + ' (' + this.activeGroup.models[this.activeModelIndex].name + ')*', style: 'subheader'
style: 'subheader' },
}, {
{ text: this.result.mean.map(e => e.category + ' ' + e.value + ' ' + e.label + ' ' + (e.std !== '' ? (' (standard deviation: ' + e.std + ')') : ''))
text: this.result.mean.map(e => e.category + ' ' + e.value + ' ' + e.label + ' ' + (e.std !== '' ? (' (standard deviation: ' + e.std + ')') : '')) },
}, {
{ table: {
table: { widths: ['*', 'auto'],
widths: ['*', 'auto'], body: [
body: [ [{text: 'Input Data / Sample Name', style: 'tableHeader'}, {text: 'Prediction*', style: 'tableHeader'}]
[{text: 'Input Data / Sample Name', style: 'tableHeader'}, {text: 'Prediction*', style: 'tableHeader'}] ].concat(this.prepareExport())
].concat(this.prepareExport()) }
} },
}, {
{ text: 'Reference Data',
text: 'Reference Data', style: 'subheader'
style: 'subheader' },
}, {
{ image: document.getElementsByTagName('canvas')[0].toDataURL('image/png'),
image: document.getElementsByTagName('canvas')[0].toDataURL('image/png'), width: 500
width: 500 },
}, {
{ table: {
table: { body: [[{
body: [[{ stack: [
stack: [ {
{ text: '*Disclaimer: This tool is still under development and Testing',
text: '*Disclaimer: This tool is still under development and Testing', style: 'subsubheader'
style: 'subsubheader' },
}, {
{ text: [
text: [ 'The prediction and classification of material parameters are validated only for certain conditions.',
'The prediction and classification of material parameters are validated only for certain conditions.', 'These results may therefore under no circumstances be used to evaluate quality-relevant issues.',
'These results may therefore under no circumstances be used to evaluate quality-relevant issues.', 'For more details please contact ',
'For more details please contact ', {
{ text: 'CR/APS1-Lingenfelser',
text: 'CR/APS1-Lingenfelser', link: 'mailto:dominic.lingenfelser@bosch.com'
link: 'mailto:dominic.lingenfelser@bosch.com' },
}, '.'
'.' ]
] },
}, ]
] }]],
}]], },
}, margin: [25, 20]
margin: [25, 20] },
}, {
{ table: {
table: { widths: ['*', '*', 'auto'],
widths: ['*', '*', 'auto'], body: [
body: [ [{text: 'Pr\u00fcfung', style: 'tableHeader'}, {text: 'Freigabe', style: 'tableHeader'}, {text: 'Datum', style: 'tableHeader'}],
[{text: 'Pr\u00fcfung', style: 'tableHeader'}, {text: 'Freigabe', style: 'tableHeader'}, {text: 'Datum', style: 'tableHeader'}], ['CR/APS1-Lotter', 'CR/APS1-Lingenfelser', new Date().toLocaleDateString()]
['CR/APS1-Lotter', 'CR/APS1-Lingenfelser', new Date().toLocaleDateString()] ],
], }
} }
} ],
], footer: {
footer: { text: '\u00a9 Alle Rechte bei Robert Bosch GmbH, auch f\u00fcr den Fall von Schutzreichtsanmeldungen. Jede Verf\u00fcgungsbefugnis, wie Kopier- und Weitergaberecht, bei uns.',
text: '\u00a9 Alle Rechte bei Robert Bosch GmbH, auch f\u00fcr den Fall von Schutzreichtsanmeldungen. Jede Verf\u00fcgungsbefugnis, wie Kopier- und Weitergaberecht, bei uns.', fontSize: 8,
fontSize: 8, alignment: 'center'
alignment: 'center' },
}, styles: {
styles: { header: {
header: { fontSize: 18,
fontSize: 18, bold: true,
bold: true, margin: [0, 10]
margin: [0, 10] },
}, subheader: {
subheader: { fontSize: 15,
fontSize: 15, bold: true,
bold: true, margin: [0, 8]
margin: [0, 8] },
}, subsubheader: {
subsubheader: { bold: true,
bold: true, margin: [0, 5]
margin: [0, 5] },
}, tableHeader: {
tableHeader: { bold: true,
bold: true, fontSize: 13,
fontSize: 13, color: 'black'
color: 'black' }
} },
}, pageMargins: [50, 50, 50, 15]
pageMargins: [50, 50, 50, 15] };
};
pdfMake.createPdf(doc).download('predictions-' + this.generateTimestamp() + '.pdf'); pdfMake.createPdf(doc).download('predictions-' + this.generateTimestamp() + '.pdf');
} }
} }

View File

@ -2,25 +2,25 @@ import { Injectable } from '@angular/core';
import {Observable, Subject} from 'rxjs'; import {Observable, Subject} from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ArrayInputHelperService { export class ArrayInputHelperService {
com: Subject<{ id: string, index: number, value: any }> = new Subject(); com: Subject<{ id: string, index: number, value: any }> = new Subject();
constructor() { } constructor() { }
values(id: string) { // Observable which returns new values as they come for subscribed id values(id: string) { // Observable which returns new values as they come for subscribed id
return new Observable<{index: number, value: any}>(observer => { return new Observable<{index: number, value: any}>(observer => {
this.com.subscribe(data => { this.com.subscribe(data => {
if (data.id === id) { if (data.id === id) {
observer.next({index: data.index, value: data.value}); observer.next({index: data.index, value: data.value});
} }
}); });
}); });
} }
newValue(id: string, index: number, value: any) { // Set new value newValue(id: string, index: number, value: any) { // Set new value
this.com.next({id, index, value}); this.com.next({id, index, value});
} }
} }

View File

@ -1,3 +1,3 @@
<ng-container *ngFor="let ignore of [].constructor(values.length); index as i"> <ng-container *ngFor="let ignore of [].constructor(values.length); index as i">
<ng-container *ngTemplateOutlet="item.templateRef; context: {$implicit: {i: i, value: values[i]}}"></ng-container> <ng-container *ngTemplateOutlet="item.templateRef; context: {$implicit: {i: i, value: values[i]}}"></ng-container>
</ng-container> </ng-container>

View File

@ -1,13 +1,13 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ContentChild, ContentChild,
Directive, Directive,
forwardRef, forwardRef,
HostListener, HostListener,
Input, Input,
OnInit, OnInit,
TemplateRef TemplateRef
} from '@angular/core'; } from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
@ -15,150 +15,150 @@ import {ArrayInputHelperService} from './array-input-helper.service';
@Directive({ // Directive for template and input values @Directive({ // Directive for template and input values
// tslint:disable-next-line:directive-selector // tslint:disable-next-line:directive-selector
selector: '[rbArrayInputItem]' selector: '[rbArrayInputItem]'
}) })
export class RbArrayInputItemDirective { export class RbArrayInputItemDirective {
constructor(public templateRef: TemplateRef<any>) { constructor(public templateRef: TemplateRef<any>) {
} }
} }
@Directive({ // Directive for change detection @Directive({ // Directive for change detection
// tslint:disable-next-line:directive-selector // tslint:disable-next-line:directive-selector
selector: '[rbArrayInputListener]' selector: '[rbArrayInputListener]'
}) })
export class RbArrayInputListenerDirective { export class RbArrayInputListenerDirective {
@Input() rbArrayInputListener: string; @Input() rbArrayInputListener: string;
@Input() index; @Input() index;
constructor( constructor(
private helperService: ArrayInputHelperService private helperService: ArrayInputHelperService
) { } ) { }
@HostListener('ngModelChange', ['$event']) @HostListener('ngModelChange', ['$event'])
onChange(event) { // Emit new value onChange(event) { // Emit new value
this.helperService.newValue(this.rbArrayInputListener, this.index, event); this.helperService.newValue(this.rbArrayInputListener, this.index, event);
} }
} }
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
selector: 'rb-array-input', selector: 'rb-array-input',
templateUrl: './rb-array-input.component.html', templateUrl: './rb-array-input.component.html',
styleUrls: ['./rb-array-input.component.scss'], styleUrls: ['./rb-array-input.component.scss'],
providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}] providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}]
}) })
export class RbArrayInputComponent implements ControlValueAccessor, OnInit, AfterViewInit { export class RbArrayInputComponent implements ControlValueAccessor, OnInit, AfterViewInit {
pushTemplate: any = ''; // Array element template pushTemplate: any = ''; // Array element template
@Input('pushTemplate') set _pushTemplate(value) { @Input('pushTemplate') set _pushTemplate(value) {
this.pushTemplate = value; this.pushTemplate = value;
if (this.values.length) { if (this.values.length) {
this.updateArray(); this.updateArray();
} }
} }
@Input() pushPath: string = null; @Input() pushPath: string = null;
@ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective; @ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective;
@ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective; @ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective;
values = []; // Main array to display values = []; // Main array to display
onChange = (ignore?: any): void => {}; onChange = (ignore?: any): void => {};
onTouched = (ignore?: any): void => {}; onTouched = (ignore?: any): void => {};
constructor( constructor(
private helperService: ArrayInputHelperService private helperService: ArrayInputHelperService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
} }
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => { // Needed to find reference setTimeout(() => { // Needed to find reference
this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => { // Action on value change this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => { // Action on value change
// Assign value // Assign value
if (this.pushPath) { if (this.pushPath) {
this.values[data.index][this.pushPath] = data.value; this.values[data.index][this.pushPath] = data.value;
} }
else { else {
this.values[data.index] = data.value; this.values[data.index] = data.value;
} }
this.updateArray(); this.updateArray();
}); });
}, 0); }, 0);
} }
updateArray() { updateArray() {
let res; let res;
// Adjust fields if pushTemplate is specified // Adjust fields if pushTemplate is specified
if (this.pushTemplate !== null) { if (this.pushTemplate !== null) {
if (this.pushPath) { if (this.pushPath) {
// Remove last element if last two are empty // Remove last element if last two are empty
if (this.values[this.values.length - 1][this.pushPath] === '' && if (this.values[this.values.length - 1][this.pushPath] === '' &&
this.values[this.values.length - 2][this.pushPath] === '') { this.values[this.values.length - 2][this.pushPath] === '') {
this.values.pop(); this.values.pop();
} }
// Add element if last all are filled // Add element if last all are filled
else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) { else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) {
this.values.push(cloneDeep(this.pushTemplate)); this.values.push(cloneDeep(this.pushTemplate));
} }
res = this.values.filter(e => e[this.pushPath] !== ''); res = this.values.filter(e => e[this.pushPath] !== '');
} }
else { else {
// Remove last element if last two are empty // Remove last element if last two are empty
if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') { if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') {
this.values.pop(); this.values.pop();
} }
else if (this.values.filter(e => e !== '').length === this.values.length) { // Add element if all are is filled else if (this.values.filter(e => e !== '').length === this.values.length) { // Add element if all are is filled
this.values.push(cloneDeep(this.pushTemplate)); this.values.push(cloneDeep(this.pushTemplate));
} }
res = this.values.filter(e => e !== ''); res = this.values.filter(e => e !== '');
} }
} }
else { else {
this.values = [this.values[0]]; this.values = [this.values[0]];
res = this.values; res = this.values;
} }
if (!res.length) { if (!res.length) {
res = ['']; res = [''];
} }
this.onChange(res); // Trigger ngModel with filled elements this.onChange(res); // Trigger ngModel with filled elements
} }
writeValue(obj: any) { // Add empty value on init writeValue(obj: any) { // Add empty value on init
if (obj) { if (obj) {
if (this.pushTemplate !== null) { if (this.pushTemplate !== null) {
// Filter out empty values // Filter out empty values
if (this.pushPath) { if (this.pushPath) {
this.values = [...obj.filter(e => e[this.pushPath] !== ''), cloneDeep(this.pushTemplate)]; this.values = [...obj.filter(e => e[this.pushPath] !== ''), cloneDeep(this.pushTemplate)];
} }
else { else {
this.values = [...obj.filter(e => e !== ''), cloneDeep(this.pushTemplate)]; this.values = [...obj.filter(e => e !== ''), cloneDeep(this.pushTemplate)];
} }
} }
else { else {
this.values = obj; this.values = obj;
} }
} }
else { else {
if (this.pushTemplate !== null) { if (this.pushTemplate !== null) {
this.values = [cloneDeep(this.pushTemplate)]; this.values = [cloneDeep(this.pushTemplate)];
} }
else { else {
this.values = ['']; this.values = [''];
} }
} }
} }
registerOnChange(fn: any) { registerOnChange(fn: any) {
this.onChange = fn; this.onChange = fn;
} }
registerOnTouched(fn: any) { registerOnTouched(fn: any) {
this.onTouched = fn; this.onTouched = fn;
} }
} }

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RbTableComponent } from './rb-table/rb-table.component'; import { RbTableComponent } from './rb-table/rb-table.component';
import {RbArrayInputComponent, RbArrayInputListenerDirective, RbArrayInputItemDirective} from import {RbArrayInputComponent, RbArrayInputListenerDirective, RbArrayInputItemDirective} from
'./rb-array-input/rb-array-input.component'; './rb-array-input/rb-array-input.component';
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component'; import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component';
@ -10,24 +10,24 @@ import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component
@NgModule({ @NgModule({
declarations: [ declarations: [
RbTableComponent, RbTableComponent,
RbArrayInputComponent, RbArrayInputComponent,
RbArrayInputListenerDirective, RbArrayInputListenerDirective,
RbArrayInputItemDirective, RbArrayInputItemDirective,
RbIconButtonComponent RbIconButtonComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
RbUiComponentsModule RbUiComponentsModule
], ],
exports: [ exports: [
RbTableComponent, RbTableComponent,
RbArrayInputComponent, RbArrayInputComponent,
RbArrayInputListenerDirective, RbArrayInputListenerDirective,
RbArrayInputItemDirective, RbArrayInputItemDirective,
RbIconButtonComponent RbIconButtonComponent
] ]
}) })
export class RbCustomInputsModule { } export class RbCustomInputsModule { }

View File

@ -1,4 +1,4 @@
<button class="rb-btn rb" [ngClass]="'rb-' + mode" [type]="type" [disabled]="disabled"> <button class="rb-btn rb" [ngClass]="'rb-' + mode" [type]="type" [disabled]="disabled">
<span class="rb-ic" [ngClass]="'rb-ic-' + icon" [class.icon-space]="iconOnly === undefined"></span> <span class="rb-ic" [ngClass]="'rb-ic-' + icon" [class.icon-space]="iconOnly === undefined"></span>
<ng-content></ng-content> <ng-content></ng-content>
</button> </button>

View File

@ -1,8 +1,8 @@
.icon-space { .icon-space {
margin-right: 0.1em !important; margin-right: 0.1em !important;
} }
button.rb-btn > span:not(.icon-space) { button.rb-btn > span:not(.icon-space) {
margin-right: -10px; margin-right: -10px;
margin-left: -10px; margin-left: -10px;
} }

View File

@ -2,22 +2,22 @@ import {Component, Input, OnInit} from '@angular/core';
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
selector: 'rb-icon-button', selector: 'rb-icon-button',
templateUrl: './rb-icon-button.component.html', templateUrl: './rb-icon-button.component.html',
styleUrls: ['./rb-icon-button.component.scss'] styleUrls: ['./rb-icon-button.component.scss']
}) })
export class RbIconButtonComponent implements OnInit { export class RbIconButtonComponent implements OnInit {
@Input() icon: string; @Input() icon: string;
@Input() mode: string; @Input() mode: string;
@Input() iconOnly; @Input() iconOnly;
@Input() disabled; @Input() disabled;
@Input() type = 'button'; @Input() type = 'button';
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -1,5 +1,5 @@
<div class="table-wrapper space-below" [class.scroll-top]="scrollTop !== undefined"> <div class="table-wrapper space-below" [class.scroll-top]="scrollTop !== undefined">
<table [class.ellipsis]="ellipsis !== undefined"> <table [class.ellipsis]="ellipsis !== undefined">
<ng-content></ng-content> <ng-content></ng-content>
</table> </table>
</div> </div>

View File

@ -1,38 +1,38 @@
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; @import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
.table-wrapper.scroll-top { .table-wrapper.scroll-top {
overflow-x: auto; overflow-x: auto;
width: 100%; width: 100%;
&, & > table { // scrollbar at the top &, & > table { // scrollbar at the top
transform:rotateX(180deg); transform:rotateX(180deg);
-ms-transform:rotateX(180deg); /* IE 9 */ -ms-transform:rotateX(180deg); /* IE 9 */
-webkit-transform:rotateX(180deg); /* Safari and Chrome */ -webkit-transform:rotateX(180deg); /* Safari and Chrome */
} }
} }
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
::ng-deep tr { ::ng-deep tr {
border-bottom: 1px solid $color-gray-mercury; border-bottom: 1px solid $color-gray-mercury;
::ng-deep td, ::ng-deep th { ::ng-deep td, ::ng-deep th {
padding: 8px 5px; padding: 8px 5px;
} }
::ng-deep th { ::ng-deep th {
text-align: left; text-align: left;
} }
} }
} }
table.ellipsis { table.ellipsis {
::ng-deep td, ::ng-deep th { ::ng-deep td, ::ng-deep th {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
max-width: 200px; max-width: 200px;
} }
} }

View File

@ -1,19 +1,19 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
selector: 'rb-table', selector: 'rb-table',
templateUrl: './rb-table.component.html', templateUrl: './rb-table.component.html',
styleUrls: ['./rb-table.component.scss'] styleUrls: ['./rb-table.component.scss']
}) })
export class RbTableComponent implements OnInit { export class RbTableComponent implements OnInit {
@Input() scrollTop; @Input() scrollTop;
@Input() ellipsis; @Input() ellipsis;
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
} }
} }

View File

@ -4,335 +4,335 @@
<ng-template #content> <ng-template #content>
<!--BASE--> <!--BASE-->
<form #sampleForm="ngForm" *ngIf="view.base"> <form #sampleForm="ngForm" *ngIf="view.base">
<div class="sample"> <div class="sample">
<div> <div>
<rb-form-input name="materialname" label="product name" [rbDebounceTime]="0" [rbInitialOpen]="true" <rb-form-input name="materialname" label="product name" [rbDebounceTime]="0" [rbInitialOpen]="true"
[rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf" [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf"
(keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel (keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel
[appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true" [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true"
*ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)" *ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)"
title="trade name of the material, eg. Ultradur B4300 G6"> title="trade name of the material, eg. Ultradur B4300 G6">
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
<ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template> <ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template>
</rb-form-input> </rb-form-input>
<rb-icon-button class="set-new-material space-below" icon="add" mode="secondary" <rb-icon-button class="set-new-material space-below" icon="add" mode="secondary"
(click)="setNewMaterial(!newMaterial)" (click)="setNewMaterial(!newMaterial)"
*ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)"> *ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)">
New material New material
</rb-icon-button> </rb-icon-button>
</div> </div>
<div class="material shaded-container" *ngIf="newMaterial" [@inOut]> <div class="material shaded-container" *ngIf="newMaterial" [@inOut]>
<h4>Material properties</h4> <h4>Material properties</h4>
<rb-form-input name="supplier" label="supplier" <rb-form-input name="supplier" label="supplier"
[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)" [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.supplier" #supplierInput="ngModel" [(ngModel)]="material.supplier" #supplierInput="ngModel"
(focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)" (focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
title="material supplier, eg. BASF"> title="material supplier, eg. BASF">
<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="group" label="group" <rb-form-input name="group" label="group"
[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)" [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.group" #groupInput="ngModel" [(ngModel)]="material.group" #groupInput="ngModel"
(focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)" (focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
title="chemical material type, eg. PA66"> title="chemical material type, eg. PA66">
<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<ng-template #modalWarning> <ng-template #modalWarning>
<rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value"> <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
The specified {{modalText.list}} could not be found in the list. <br> The specified {{modalText.list}} could not be found in the list. <br>
Did you mean {{modalText.suggestion}}? Did you mean {{modalText.suggestion}}?
</rb-alert> </rb-alert>
</ng-template> </ng-template>
<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''"> <rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i" <rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
label="material number" appValidate="string" [name]="'materialNumber-' + item.i" label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
[ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input> [ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
</rb-array-input> </rb-array-input>
<rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties" <rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
[(ngModel)]="material.properties.material_template"> [(ngModel)]="material.properties.material_template">
<option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option> <option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
</rb-form-select> </rb-form-select>
<rb-form-input *ngFor="let parameter of <rb-form-input *ngFor="let parameter of
d.id.materialTemplates[material.properties.material_template].parameters; d.id.materialTemplates[material.properties.material_template].parameters;
index as i" [name]="'materialParameter' + i" index as i" [name]="'materialParameter' + i"
[label]="parameter.name" appValidate="string" required [label]="parameter.name" appValidate="string" required
[(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel"> [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
</div> </div>
<div> <div>
<rb-form-select name="type" label="type" required [(ngModel)]="baseSample.type" ngModel <rb-form-select name="type" label="type" required [(ngModel)]="baseSample.type" ngModel
*ngIf="baseSample.type !== undefined" *ngIf="baseSample.type !== undefined"
title="material status of the sample"> title="material status of the sample">
<option value="as-delivered/raw">as-delivered/raw</option> <option value="as-delivered/raw">as-delivered/raw</option>
<option value="processed">processed</option> <option value="processed">processed</option>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-select> </rb-form-select>
<rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="baseSample.color" <rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="baseSample.color"
*ngIf="baseSample.color !== undefined" #colorInput="ngModel" title="sample color, eg. black"> *ngIf="baseSample.color !== undefined" #colorInput="ngModel" title="sample color, eg. black">
<ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="baseSample.batch" <rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="baseSample.batch"
#batchInput="ngModel" *ngIf="baseSample.batch !== undefined" #batchInput="ngModel" *ngIf="baseSample.batch !== undefined"
title="batch number the sample was from, eg. 2264486614"> title="batch number the sample was from, eg. 2264486614">
<ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
</div> </div>
</div> </div>
<div class="notes" *ngIf="baseSample.notes !== undefined"> <div class="notes" *ngIf="baseSample.notes !== undefined">
<rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]"
[(ngModel)]="baseSample.notes.comment" #commentInput="ngModel" [(ngModel)]="baseSample.notes.comment" #commentInput="ngModel"
title="general remarks that cannot be expressed in additional properties, eg. stabilized"> title="general remarks that cannot be expressed in additional properties, eg. stabilized">
<ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<h5>Sample references</h5> <h5>Sample references</h5>
<div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]> <div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]>
<div> <div>
<rb-form-input [name]="'sr-id' + i" label="sample number" <rb-form-input [name]="'sr-id' + i" label="sample number"
[rbFormInputAutocomplete]="sampleReferenceListBind()" [rbFormInputAutocomplete]="sampleReferenceListBind()"
[rbDebounceTime]="300" appValidate="stringOf" [rbDebounceTime]="300" appValidate="stringOf"
[appValidateArgs]="[sampleReferenceAutocomplete[i]]" [appValidateArgs]="[sampleReferenceAutocomplete[i]]"
(ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel
title="sample number of the referenced sample, eg. An31"> title="sample number of the referenced sample, eg. An31">
<ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template> <ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template>
</rb-form-input> </rb-form-input>
</div> </div>
<rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''" <rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''"
[(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to"> [(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to">
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
</div> </div>
<h5>Additional properties</h5> <h5>Additional properties</h5>
<rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0" <rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0"
class="two-col" [@inOut]> class="two-col" [@inOut]>
<ng-container *rbArrayInputItem="let item"> <ng-container *rbArrayInputItem="let item">
<div> <div>
<rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i" <rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i"
[rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)"
[rbDebounceTime]="0" [rbDebounceTime]="0"
[rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]" [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]"
[(ngModel)]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz"> [(ngModel)]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz">
<ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
</div> </div>
<rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string" <rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string"
[required]="item.value[0] !== ''" [required]="item.value[0] !== ''"
[(ngModel)]="item.value[1]" title="value of additional property, eg. 0 min"> [(ngModel)]="item.value[1]" title="value of additional property, eg. 0 min">
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
</ng-container> </ng-container>
</rb-array-input> </rb-array-input>
</div> </div>
<div *ngIf="samples.length; else generateSamples" class="space-below"> <div *ngIf="samples.length; else generateSamples" class="space-below">
<rb-icon-button icon="save" mode="primary" type="submit" (click)="saveSample()" <rb-icon-button icon="save" mode="primary" type="submit" (click)="saveSample()"
[disabled]="!sampleForm.form.valid"> [disabled]="!sampleForm.form.valid">
Save sample Save sample
</rb-icon-button> </rb-icon-button>
<rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length > 1" <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length > 1"
(click)="deleteConfirm(modalDeleteConfirm)"> (click)="deleteConfirm(modalDeleteConfirm)">
Delete samples Delete samples
</rb-icon-button> </rb-icon-button>
</div> </div>
<ng-template #generateSamples> <ng-template #generateSamples>
<rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required <rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required
rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count" rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count"
title="number of samples to create with this base information"> title="number of samples to create with this base information">
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
<ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template> <ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template>
</rb-form-input> </rb-form-input>
<button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()" <button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()"
[disabled]="!sampleForm.form.valid"> [disabled]="!sampleForm.form.valid">
Generate sample{{sampleCount > 1 ? 's' : ''}} Generate sample{{sampleCount > 1 ? 's' : ''}}
</button> </button>
</ng-template> </ng-template>
</form> </form>
<!--BASE SUMMARY--> <!--BASE SUMMARY-->
<div *ngIf="view.baseSum"> <div *ngIf="view.baseSum">
<h3 *ngIf="mode === 'new'">Successfully added samples:</h3> <h3 *ngIf="mode === 'new'">Successfully added samples:</h3>
<span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.base = true; view.baseSum = false"> <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.base = true; view.baseSum = false">
</span> </span>
<rb-table id="response-data"> <rb-table id="response-data">
<tr><td>Sample Number</td><td>{{baseSample.number}}</td></tr> <tr><td>Sample Number</td><td>{{baseSample.number}}</td></tr>
<tr><td>Material</td><td>{{material.name}}</td></tr> <tr><td>Material</td><td>{{material.name}}</td></tr>
<tr><td>Type</td><td>{{baseSample.type}}</td></tr> <tr><td>Type</td><td>{{baseSample.type}}</td></tr>
<tr><td>Color</td><td>{{baseSample.color}}</td></tr> <tr><td>Color</td><td>{{baseSample.color}}</td></tr>
<tr><td>Batch</td><td>{{baseSample.batch}}</td></tr> <tr><td>Batch</td><td>{{baseSample.batch}}</td></tr>
<tr><td>Comment</td><td>{{baseSample.notes | exists:'comment'}}</td></tr> <tr><td>Comment</td><td>{{baseSample.notes | exists:'comment'}}</td></tr>
<tr *ngFor="let reference of sampleReferences.slice(0, -1)"> <tr *ngFor="let reference of sampleReferences.slice(0, -1)">
<td>Sample reference</td><td>{{reference[0]}} - {{reference[1]}}</td> <td>Sample reference</td><td>{{reference[0]}} - {{reference[1]}}</td>
</tr> </tr>
<tr *ngFor="let field of customFields"><td>{{field[0]}}</td><td>{{field[1]}}</td></tr> <tr *ngFor="let field of customFields"><td>{{field[0]}}</td><td>{{field[1]}}</td></tr>
</rb-table> </rb-table>
</div> </div>
<!--CM--> <!--CM-->
<div *ngIf="view.cm"> <div *ngIf="view.cm">
<form #cmForm="ngForm" [ngClass]="{'cm-form': samples.length > 1}"> <form #cmForm="ngForm" [ngClass]="{'cm-form': samples.length > 1}">
<rb-tab-panel (tabChanged)="cmSampleIndex = $event" class="space-below" *ngIf="samples.length > 1"> <rb-tab-panel (tabChanged)="cmSampleIndex = $event" class="space-below" *ngIf="samples.length > 1">
<ng-container *ngFor="let sample of samples; index as i"> <ng-container *ngFor="let sample of samples; index as i">
<div *rbTabPanelItem="sample.number; id: i"></div> <div *rbTabPanelItem="sample.number; id: i"></div>
</ng-container> </ng-container>
</rb-tab-panel> </rb-tab-panel>
<div *ngFor="let sample of samples; index as gIndex" <div *ngFor="let sample of samples; index as gIndex"
[ngStyle]="{display: gIndex == cmSampleIndex || gIndex == cmSampleIndex + 1 ? 'initial' : 'none'}"> [ngStyle]="{display: gIndex == cmSampleIndex || gIndex == cmSampleIndex + 1 ? 'initial' : 'none'}">
<h4 *ngIf="samples.length > 1">{{sample.number}}</h4> <h4 *ngIf="samples.length > 1">{{sample.number}}</h4>
<div class="conditions shaded-container space-below"> <div class="conditions shaded-container space-below">
<h5> <h5>
Condition Condition
<button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(sample)"> <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(sample)">
{{sample.condition.condition_template ? 'Do not set condition' : 'Set condition'}} {{sample.condition.condition_template ? 'Do not set condition' : 'Set condition'}}
</button> </button>
</h5> </h5>
<div *ngIf="sample.condition.condition_template" [@inOut]> <div *ngIf="sample.condition.condition_template" [@inOut]>
<rb-form-select [name]="'conditionSelect-' + gIndex" label="Condition" <rb-form-select [name]="'conditionSelect-' + gIndex" label="Condition"
[(ngModel)]="sample.condition.condition_template" [(ngModel)]="sample.condition.condition_template"
(ngModelChange)="reValidate()"> (ngModelChange)="reValidate()">
<option [value]="sample.condition.condition_template"> <option [value]="sample.condition.condition_template">
{{d.id.conditionTemplates[sample.condition.condition_template].name}} - current {{d.id.conditionTemplates[sample.condition.condition_template].name}} - current
</option> </option>
<option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option> <option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option>
</rb-form-select> </rb-form-select>
<ng-container *ngFor="let parameter of <ng-container *ngFor="let parameter of
d.id.conditionTemplates[sample.condition.condition_template].parameters; index as i" d.id.conditionTemplates[sample.condition.condition_template].parameters; index as i"
[ngSwitch]="(parameter.range.values ? 1 : 0)"> [ngSwitch]="(parameter.range.values ? 1 : 0)">
<rb-form-select *ngSwitchCase="1" <rb-form-select *ngSwitchCase="1"
[name]="'conditionParameter-' + gIndex + '-' + i" [name]="'conditionParameter-' + gIndex + '-' + i"
[label]="parameter.name" [(ngModel)]="sample.condition[parameter.name]" ngModel> [label]="parameter.name" [(ngModel)]="sample.condition[parameter.name]" ngModel>
<option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option> <option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-select> </rb-form-select>
<rb-form-input *ngSwitchDefault (focus)="checkFormAfterInit = true" <rb-form-input *ngSwitchDefault (focus)="checkFormAfterInit = true"
[name]="'conditionParameter-' + gIndex + '-' + i" [name]="'conditionParameter-' + gIndex + '-' + i"
[label]="parameter.name" appValidate="string" required [label]="parameter.name" appValidate="string" required
[(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel"> [(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
</ng-container> </ng-container>
</div> </div>
</div> </div>
<div class="measurements shaded-container space-below"> <div class="measurements shaded-container space-below">
<h5> <h5>
Measurements Measurements
<rb-icon-button icon="undo" mode="secondary" *ngIf="measurementRestoreData.length" <rb-icon-button icon="undo" mode="secondary" *ngIf="measurementRestoreData.length"
class="restore-measurements" (click)="restoreMeasurements()"> class="restore-measurements" (click)="restoreMeasurements()">
Restore measurements Restore measurements
</rb-icon-button> </rb-icon-button>
</h5> </h5>
<div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut] class="space-below"> <div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut] class="space-below">
<rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template" <rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template"
[(ngModel)]="measurement.measurement_template" [(ngModel)]="measurement.measurement_template"
(ngModelChange)="clearMeasurement(gIndex, mIndex)"> (ngModelChange)="clearMeasurement(gIndex, mIndex)">
<option [value]="sample.condition.condition_template"> <option [value]="sample.condition.condition_template">
{{d.id.measurementTemplates[measurement.measurement_template].name}} - old {{d.id.measurementTemplates[measurement.measurement_template].name}} - old
</option> </option>
<option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option> <option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option>
</rb-form-select> </rb-form-select>
<div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters; <div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters;
index as pIndex"> index as pIndex">
<ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)"> <ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)">
<rb-form-file *ngSwitchCase="1" <rb-form-file *ngSwitchCase="1"
[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
[label]="parameter.name" maxSize="10000000" multiple [label]="parameter.name" maxSize="10000000" multiple
[required]="measurement.values[parameter.name] && [required]="measurement.values[parameter.name] &&
!measurement.values[parameter.name].length" !measurement.values[parameter.name].length"
(ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)" (ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)"
placeholder="Select file or drag and drop" dragDrop ngModel> placeholder="Select file or drag and drop" dragDrop ngModel>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-file> </rb-form-file>
<rb-form-select *ngSwitchCase="2" <rb-form-select *ngSwitchCase="2"
[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
[label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel> [label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel>
<option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option> <option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-select> </rb-form-select>
<rb-form-input *ngSwitchDefault <rb-form-input *ngSwitchDefault
[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
[label]="parameter.name" appValidate="string" [label]="parameter.name" appValidate="string"
[(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel"> [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
</ng-container> </ng-container>
<canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0" <canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0"
class="dpt-chart" class="dpt-chart"
[@inOut] [@inOut]
[datasets]="charts[gIndex][mIndex]" [datasets]="charts[gIndex][mIndex]"
[labels]="[]" [labels]="[]"
[options]="chartOptions" [options]="chartOptions"
[legend]="false" [legend]="false"
chartType="scatter"> chartType="scatter">
</canvas> </canvas>
</div> </div>
<rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)"> <rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)">
Delete measurement Delete measurement
</rb-icon-button> </rb-icon-button>
</div> </div>
<div> <div>
<rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)"> <rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)">
New measurement New measurement
</rb-icon-button> </rb-icon-button>
</div> </div>
</div> </div>
</div> </div>
<div class="buttons"> <div class="buttons">
<rb-icon-button icon="summary" mode="primary" type="submit" (click)="view.cm = false; view.cmSum = true" <rb-icon-button icon="summary" mode="primary" type="submit" (click)="view.cm = false; view.cmSum = true"
[disabled]="!cmForm.form.valid"> [disabled]="!cmForm.form.valid">
Summary Summary
</rb-icon-button> </rb-icon-button>
<rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length === 1" <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length === 1"
(click)="deleteConfirm(modalDeleteConfirm)"> (click)="deleteConfirm(modalDeleteConfirm)">
Delete sample Delete sample
</rb-icon-button> </rb-icon-button>
</div> </div>
</form> </form>
</div> </div>
<!--CM SUMMARY--> <!--CM SUMMARY-->
<div *ngIf="view.cmSum"> <div *ngIf="view.cmSum">
<span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.cm = true; view.cmSum = false"> <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.cm = true; view.cmSum = false">
</span> </span>
<rb-table> <rb-table>
<ng-container *ngFor="let sample of samples; index as gIndex"> <ng-container *ngFor="let sample of samples; index as gIndex">
<tr><th>{{sample.number}}</th><th></th></tr> <tr><th>{{sample.number}}</th><th></th></tr>
<tr *ngFor="let parameter of (d.id.conditionTemplates[sample.condition.condition_template] || {parameters: []}) <tr *ngFor="let parameter of (d.id.conditionTemplates[sample.condition.condition_template] || {parameters: []})
.parameters"> .parameters">
<td>{{parameter.name}}</td><td>{{sample.condition[parameter.name]}}</td> <td>{{parameter.name}}</td><td>{{sample.condition[parameter.name]}}</td>
</tr> </tr>
<ng-container *ngFor="let measurement of sample.measurements"> <ng-container *ngFor="let measurement of sample.measurements">
<tr *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters"> <tr *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters">
<td>{{parameter.name}}</td><td>{{measurement.values[parameter.name]}}</td> <td>{{parameter.name}}</td><td>{{measurement.values[parameter.name]}}</td>
</tr> </tr>
</ng-container> </ng-container>
</ng-container> </ng-container>
</rb-table> </rb-table>
<rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()"> <rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()">
Save sample{{samples.length > 1 ? 's' : ''}} Save sample{{samples.length > 1 ? 's' : ''}}
</rb-icon-button> </rb-icon-button>
</div> </div>
</ng-template> </ng-template>
<ng-template #modalDeleteConfirm> <ng-template #modalDeleteConfirm>
<rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete sample' + (samples.length > 1 ? 's' : '')" <rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete sample' + (samples.length > 1 ? 's' : '')"
cancelBtnLabel="Cancel"> cancelBtnLabel="Cancel">
Do you really want to delete {{(samples.length > 1 ? 'samples ' : 'sample ') + sampleNames()}}? Do you really want to delete {{(samples.length > 1 ? 'samples ' : 'sample ') + sampleNames()}}?
</rb-alert> </rb-alert>
</ng-template> </ng-template>

View File

@ -1,49 +1,49 @@
::ng-deep rb-table#response-data > table { ::ng-deep rb-table#response-data > table {
width: auto !important; width: auto !important;
} }
td:first-child { td:first-child {
font-weight: bold; font-weight: bold;
} }
.condition-set { .condition-set {
float: right; float: right;
} }
.two-col { .two-col {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-column-gap: 10px; grid-column-gap: 10px;
} }
.dpt-chart { .dpt-chart {
max-width: 400px; max-width: 400px;
} }
.sample-count { .sample-count {
max-width: 150px; max-width: 150px;
margin-right: 20px; margin-right: 20px;
display: inline-block; display: inline-block;
} }
.set-new-material { .set-new-material {
display: block; display: block;
} }
.delete-sample { .delete-sample {
float: right; float: right;
} }
.cm-form { .cm-form {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-column-gap: 1rem; grid-column-gap: 1rem;
rb-tab-panel, div.buttons { rb-tab-panel, div.buttons {
grid-column: span 2; grid-column: span 2;
} }
} }
.restore-measurements { .restore-measurements {
float: right; float: right;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,292 +1,292 @@
<div class="header-addnew"> <div class="header-addnew">
<a routerLink="/samples/new" *ngIf="login.isLevel.write"> <a routerLink="/samples/new" *ngIf="login.isLevel.write">
<rb-icon-button icon="add" mode="primary" class="space-left">New sample</rb-icon-button> <rb-icon-button icon="add" mode="primary" class="space-left">New sample</rb-icon-button>
</a> </a>
<rb-icon-button *ngIf="sampleSelect === 2" mode="secondary" icon="close" (click)="sampleSelect = 0" <rb-icon-button *ngIf="sampleSelect === 2" mode="secondary" icon="close" (click)="sampleSelect = 0"
class="validation-close" iconOnly></rb-icon-button> class="validation-close" iconOnly></rb-icon-button>
<rb-icon-button *ngIf="login.isLevel.dev" [icon]="sampleSelect === 2 ? 'checkmark' : 'clear-all'" mode="secondary" <rb-icon-button *ngIf="login.isLevel.dev" [icon]="sampleSelect === 2 ? 'checkmark' : 'clear-all'" mode="secondary"
(click)="validate()"> (click)="validate()">
{{sampleSelect === 2 ? 'Validate' : 'Validation'}} {{sampleSelect === 2 ? 'Validate' : 'Validation'}}
</rb-icon-button> </rb-icon-button>
</div> </div>
<!--FILTERS--> <!--FILTERS-->
<rb-accordion> <rb-accordion>
<rb-accordion-title [open]="false"><span class="rb-ic rb-ic-filter"></span>&nbsp; Filter</rb-accordion-title> <rb-accordion-title [open]="false"><span class="rb-ic rb-ic-filter"></span>&nbsp; Filter</rb-accordion-title>
<rb-accordion-body> <rb-accordion-body>
<form class="filters"> <form class="filters">
<div class="status-selection"> <div class="status-selection">
<label class="label">Status</label> <label class="label">Status</label>
<rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated" <rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated"
[disabled]="!filters.status.new && !filters.status.deleted" (ngModelChange)="loadSamples({firstPage: true})"> [disabled]="!filters.status.new && !filters.status.deleted" (ngModelChange)="loadSamples({firstPage: true})">
validated validated
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" <rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new"
[disabled]="!filters.status.validated && !filters.status.deleted" [disabled]="!filters.status.validated && !filters.status.deleted"
(ngModelChange)="loadSamples({firstPage: true})"> (ngModelChange)="loadSamples({firstPage: true})">
new new
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox name="status-deleted" [(ngModel)]="filters.status.deleted" <rb-form-checkbox name="status-deleted" [(ngModel)]="filters.status.deleted"
[disabled]="!filters.status.validated && !filters.status.new" (ngModelChange)="loadSamples({firstPage: true})" [disabled]="!filters.status.validated && !filters.status.new" (ngModelChange)="loadSamples({firstPage: true})"
*ngIf="login.isLevel.dev"> *ngIf="login.isLevel.dev">
deleted deleted
</rb-form-checkbox> </rb-form-checkbox>
</div> </div>
<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection" <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection"
(ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection> (ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection>
<option value="3">3</option> <option value="3">3</option>
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
<option value="250">250</option> <option value="250">250</option>
<option value="500">500</option> <option value="500">500</option>
</rb-form-select> </rb-form-select>
<div id="multi"> <div id="multi">
<rb-form-multi-select *ngFor="let item of categories | keyvalue: originalOrder" name="fieldSelect" idField="id" <rb-form-multi-select *ngFor="let item of categories | keyvalue: originalOrder" name="fieldSelect" idField="id"
[items]="item.value" [(ngModel)]="isActiveKey" [label]="item.key" class="selection" [items]="item.value" [(ngModel)]="isActiveKey" [label]="item.key" class="selection"
(ngModelChange)="loadSamples({}, $event, item.key)"> (ngModelChange)="loadSamples({}, $event, item.key)">
<span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span> <span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span>
</rb-form-multi-select> </rb-form-multi-select>
</div> </div>
<rb-icon-button icon="reset" mode="secondary" (click)="resetPreferences()" class="reset-preferences"> <rb-icon-button icon="reset" mode="secondary" (click)="resetPreferences()" class="reset-preferences">
Reset to default Reset to default
</rb-icon-button> </rb-icon-button>
<div class="fieldfilters"> <div class="fieldfilters">
<div *ngFor="let filter of filters.filters"> <div *ngFor="let filter of filters.filters">
<ng-container *ngIf="isActiveKey[filter.field]"> <ng-container *ngIf="isActiveKey[filter.field]">
<rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active"></rb-form-checkbox> <rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active"></rb-form-checkbox>
<rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode" <rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode"
(ngModelChange)="updateFilterFields(filter.field)"> (ngModelChange)="updateFilterFields(filter.field)">
<option value="eq" title="field is equal to value">=</option> <option value="eq" title="field is equal to value">=</option>
<option value="ne" title="field is not equal to value">&ne;</option> <option value="ne" title="field is not equal to value">&ne;</option>
<option value="lt" title="field is lower than value">&lt;</option> <option value="lt" title="field is lower than value">&lt;</option>
<option value="lte" title="field is lower than or equal to value">&le;</option> <option value="lte" title="field is lower than or equal to value">&le;</option>
<option value="gt" title="field is greater than value">&gt;</option> <option value="gt" title="field is greater than value">&gt;</option>
<option value="gte" title="field is greater than or equal to value">&ge;</option> <option value="gte" title="field is greater than or equal to value">&ge;</option>
<option value="stringin" title="field contains value">&supe;</option> <option value="stringin" title="field contains value">&supe;</option>
<option value="in" title="field is one of the values">&isin;</option> <option value="in" title="field is one of the values">&isin;</option>
<option value="nin" title="field is not one of the values">&notin;</option> <option value="nin" title="field is not one of the values">&notin;</option>
<option value="null" title="field is null">&empty;</option> <option value="null" title="field is null">&empty;</option>
</rb-form-select> </rb-form-select>
<div class="filter-inputs"> <div class="filter-inputs">
<rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field" <rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field"
[pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''" [pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''"
(ngModelChange)="updateFilterFields(filter.field)"> (ngModelChange)="updateFilterFields(filter.field)">
<ng-container *rbArrayInputItem="let item" [ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') + <ng-container *rbArrayInputItem="let item" [ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') +
(filter.field == 'added' ? 'date' : (filter.field == 'type' ? 'type' : ''))"> (filter.field == 'added' ? 'date' : (filter.field == 'type' ? 'type' : ''))">
<rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field" <rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label" [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
[(ngModel)]="item.value" [disabled]="filter.mode === 'null'"></rb-form-date-input> [(ngModel)]="item.value" [disabled]="filter.mode === 'null'"></rb-form-date-input>
<rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field" <rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label" [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
[(ngModel)]="item.value" [disabled]="filter.mode === 'null'"> [(ngModel)]="item.value" [disabled]="filter.mode === 'null'">
</rb-form-input> </rb-form-input>
<rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field" <rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label" [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
[(ngModel)]="item.value" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')" [(ngModel)]="item.value" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')"
[rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)" [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)"
[disabled]="filter.mode === 'null'" ngModel></rb-form-input> [disabled]="filter.mode === 'null'" ngModel></rb-form-input>
<rb-form-select *ngSwitchCase="'type'" [rbArrayInputListener]="'filter-' + filter.field" <rb-form-select *ngSwitchCase="'type'" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label" [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
[(ngModel)]="item.value"> [(ngModel)]="item.value">
<option value="as-delivered/raw">as-delivered/raw</option> <option value="as-delivered/raw">as-delivered/raw</option>
<option value="processed">processed</option> <option value="processed">processed</option>
</rb-form-select> </rb-form-select>
</ng-container> </ng-container>
</rb-array-input> </rb-array-input>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<rb-form-checkbox name="no-condition" [(ngModel)]="filters.no.condition" class="space-right"> <rb-form-checkbox name="no-condition" [(ngModel)]="filters.no.condition" class="space-right">
has no condition has no condition
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox name="no-measurements" [(ngModel)]="filters.no.measurements" class="space-right"> <rb-form-checkbox name="no-measurements" [(ngModel)]="filters.no.measurements" class="space-right">
has no measurements has no measurements
</rb-form-checkbox> </rb-form-checkbox>
</div> </div>
</form> </form>
<rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})"> <rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})">
Apply filters Apply filters
</rb-icon-button> </rb-icon-button>
</rb-accordion-body> </rb-accordion-body>
</rb-accordion> </rb-accordion>
<ng-container *ngTemplateOutlet="paging"></ng-container> <ng-container *ngTemplateOutlet="paging"></ng-container>
<div class="samples-loading" *ngIf="loading"> <div class="samples-loading" *ngIf="loading">
<rb-loading-spinner></rb-loading-spinner> <rb-loading-spinner></rb-loading-spinner>
<div>Loading...</div> <div>Loading...</div>
</div> </div>
<!--DOWNLOAD BUTTONS--> <!--DOWNLOAD BUTTONS-->
<div class="download space-below" *ngIf="login.isLevel.dev"> <div class="download space-below" *ngIf="login.isLevel.dev">
<rb-icon-button class="space-right" icon="download" mode="secondary" [rbModal]="linkModal"> <rb-icon-button class="space-right" icon="download" mode="secondary" [rbModal]="linkModal">
JSON download link JSON download link
</rb-icon-button> </rb-icon-button>
<ng-template #linkModal> <ng-template #linkModal>
<div class="link-dialog"> <div class="link-dialog">
<label for="jsonUrl">URL for JSON download</label> <label for="jsonUrl">URL for JSON download</label>
<textarea class="linkmodal" id="jsonUrl" #linkarea [value]="sampleUrl({export: true, host: true})" <textarea class="linkmodal" id="jsonUrl" #linkarea [value]="sampleUrl({export: true, host: true})"
(keydown)="preventDefault($event)"></textarea> (keydown)="preventDefault($event)"></textarea>
<rb-form-checkbox class="space-right" name="download-spectra" [(ngModel)]="downloadSpectra"> <rb-form-checkbox class="space-right" name="download-spectra" [(ngModel)]="downloadSpectra">
add spectra add spectra
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox class="space-right" name="download-conditions" [(ngModel)]="downloadCondition"> <rb-form-checkbox class="space-right" name="download-conditions" [(ngModel)]="downloadCondition">
add conditions add conditions
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox class="space-right" name="download-flatten" [(ngModel)]="downloadFlatten"> <rb-form-checkbox class="space-right" name="download-flatten" [(ngModel)]="downloadFlatten">
flatten object flatten object
</rb-form-checkbox> </rb-form-checkbox>
<rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button> <rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button>
</div> </div>
</ng-template> </ng-template>
<a [href]="csvUrl" download="samples.csv"> <a [href]="csvUrl" download="samples.csv">
<rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})"> <rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})">
Download result as CSV Download result as CSV
</rb-icon-button> </rb-icon-button>
</a> </a>
</div> </div>
<!--TABLE--> <!--TABLE-->
<rb-table class="samples-table" scrollTop ellipsis> <rb-table class="samples-table" scrollTop ellipsis>
<tr> <tr>
<th *ngIf="login.isLevel.write"> <th *ngIf="login.isLevel.write">
<span class="rb-ic rb-ic-edit clickable" *ngIf="login.isLevel.dev" (click)="batchEdit()"></span> <span class="rb-ic rb-ic-edit clickable" *ngIf="login.isLevel.dev" (click)="batchEdit()"></span>
<span class="rb-ic rb-ic-close clickable" *ngIf="sampleSelect === 1" (click)="sampleSelect = 0"></span> <span class="rb-ic rb-ic-close clickable" *ngIf="sampleSelect === 1" (click)="sampleSelect = 0"></span>
</th> </th>
<th *ngIf="sampleSelect"> <th *ngIf="sampleSelect">
<rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox> <rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
</th> </th>
<th *ngFor="let key of activeKeys" [title]="key.label"> <th *ngFor="let key of activeKeys" [title]="key.label">
<div class="sort-header"> <div class="sort-header">
<span>{{key.label}}</span> <span>{{key.label}}</span>
<div *ngIf="key.sortable"> <div *ngIf="key.sortable">
<span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')"> <span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')">
<span *ngIf="filters.sort === key.id + '-' + 'desc'"></span> <span *ngIf="filters.sort === key.id + '-' + 'desc'"></span>
</span> </span>
<span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')"> <span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')">
<span *ngIf="filters.sort === key.id + '-' + 'asc'"></span> <span *ngIf="filters.sort === key.id + '-' + 'asc'"></span>
</span> </span>
</div> </div>
</div> </div>
</th> </th>
</tr> </tr>
<tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)"> <tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)">
<td *ngIf="login.isLevel.write"> <td *ngIf="login.isLevel.write">
<a [routerLink]="'/samples/edit/' + sample._id" *ngIf="sample.status !== 'deleted' && <a [routerLink]="'/samples/edit/' + sample._id" *ngIf="sample.status !== 'deleted' &&
(login.isLevel.dev || (login.isLevel.write && sample.user_id === login.userId))"> (login.isLevel.dev || (login.isLevel.write && sample.user_id === login.userId))">
<span class="rb-ic rb-ic-edit clickable"></span> <span class="rb-ic rb-ic-edit clickable"></span>
</a> </a>
<span class="rb-ic rb-ic-undo clickable" *ngIf="sample.status === 'deleted' && login.isLevel.dev" <span class="rb-ic rb-ic-undo clickable" *ngIf="sample.status === 'deleted' && login.isLevel.dev"
(click)="restoreSample(sample._id, restoreConfirm, $event)"></span> (click)="restoreSample(sample._id, restoreConfirm, $event)"></span>
</td> </td>
<td *ngIf="sampleSelect"> <td *ngIf="sampleSelect">
<rb-form-checkbox *ngIf="sample.status !== 'deleted'" [name]="'validate-' + i" (click)="stopPropagation($event)" <rb-form-checkbox *ngIf="sample.status !== 'deleted'" [name]="'validate-' + i" (click)="stopPropagation($event)"
[(ngModel)]="sample.selected"> [(ngModel)]="sample.selected">
</rb-form-checkbox> </rb-form-checkbox>
</td> </td>
<td *ngFor="let item of activeKeys; index as j"> <td *ngFor="let item of activeKeys; index as j">
{{data[i].data[j]}} {{data[i].data[j]}}
</td> </td>
</tr> </tr>
</rb-table> </rb-table>
<ng-container *ngTemplateOutlet="paging"></ng-container> <ng-container *ngTemplateOutlet="paging"></ng-container>
<ng-template #paging> <ng-template #paging>
<div class="paging"> <div class="paging">
<button class="rb-btn rb-link" type="button" (click)="loadPage(-1)" [disabled]="page === 1"> <button class="rb-btn rb-link" type="button" (click)="loadPage(-1)" [disabled]="page === 1">
<span class="rb-ic rb-ic-back-left"></span> <span class="rb-ic rb-ic-back-left"></span>
</button> </button>
<rb-form-input label="page" (ngModelChange)="loadPage($event - page)" [ngModel]="page"> <rb-form-input label="page" (ngModelChange)="loadPage($event - page)" [ngModel]="page">
</rb-form-input> </rb-form-input>
<span> <span>
of {{pages}} ({{totalSamples}} samples) of {{pages}} ({{totalSamples}} samples)
</span> </span>
<button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages"> <button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages">
<span class="rb-ic rb-ic-forward-right"></span> <span class="rb-ic rb-ic-forward-right"></span>
</button> </button>
</div> </div>
</ng-template> </ng-template>
<!--SAMPLE DETAILS--> <!--SAMPLE DETAILS-->
<ng-template #sampleModal> <ng-template #sampleModal>
<rb-loading-spinner *ngIf="sampleDetailsSample === null; else sampleDetailsTemplate"></rb-loading-spinner> <rb-loading-spinner *ngIf="sampleDetailsSample === null; else sampleDetailsTemplate"></rb-loading-spinner>
<ng-template #sampleDetailsTemplate> <ng-template #sampleDetailsTemplate>
<h3>{{sampleDetailsSample.number}}</h3> <h3>{{sampleDetailsSample.number}}</h3>
<rb-table class="sample-details-table"> <rb-table class="sample-details-table">
<tr> <tr>
<th>Material</th> <th>Material</th>
<td>{{sampleDetailsSample.material.name}}</td> <td>{{sampleDetailsSample.material.name}}</td>
</tr> </tr>
<tr> <tr>
<th>Supplier</th> <th>Supplier</th>
<td>{{sampleDetailsSample.material.supplier}}</td> <td>{{sampleDetailsSample.material.supplier}}</td>
</tr> </tr>
<tr> <tr>
<th>Group</th> <th>Group</th>
<td>{{sampleDetailsSample.material.group}}</td> <td>{{sampleDetailsSample.material.group}}</td>
</tr> </tr>
<tr> <tr>
<th>Type</th> <th>Type</th>
<td>{{sampleDetailsSample.type}}</td> <td>{{sampleDetailsSample.type}}</td>
</tr> </tr>
<tr> <tr>
<th>color</th> <th>color</th>
<td>{{sampleDetailsSample.color}}</td> <td>{{sampleDetailsSample.color}}</td>
</tr> </tr>
<tr> <tr>
<th>Batch</th> <th>Batch</th>
<td>{{sampleDetailsSample.batch}}</td> <td>{{sampleDetailsSample.batch}}</td>
</tr> </tr>
<tr> <tr>
<th>Comment</th> <th>Comment</th>
<td>{{sampleDetailsSample.notes.comment | exists}}</td> <td>{{sampleDetailsSample.notes.comment | exists}}</td>
</tr> </tr>
<tr *ngFor="let customField of sampleDetailsSample.notes.custom_fields_entries"> <tr *ngFor="let customField of sampleDetailsSample.notes.custom_fields_entries">
<th>{{customField[0]}}</th> <th>{{customField[0]}}</th>
<td>{{customField[1]}}</td> <td>{{customField[1]}}</td>
</tr> </tr>
<tr *ngFor="let reference of sampleDetailsSample.notes.sample_references"> <tr *ngFor="let reference of sampleDetailsSample.notes.sample_references">
<th>{{reference.relation}}</th> <th>{{reference.relation}}</th>
<td> <td>
<button class="rb-btn rb-link" (click)="sampleDetails(reference.sample_id, sampleModal)"> <button class="rb-btn rb-link" (click)="sampleDetails(reference.sample_id, sampleModal)">
{{reference.number}} {{reference.number}}
</button> </button>
</td> </td>
</tr> </tr>
<tr *ngFor="let conditionField of sampleDetailsSample.condition_entries"> <tr *ngFor="let conditionField of sampleDetailsSample.condition_entries">
<th>{{conditionField[0]}}</th> <th>{{conditionField[0]}}</th>
<td>{{conditionField[1]}}</td> <td>{{conditionField[1]}}</td>
</tr> </tr>
<tr *ngFor="let measurement of sampleDetailsSample.measurement_entries"> <tr *ngFor="let measurement of sampleDetailsSample.measurement_entries">
<th>{{measurement.name}}</th> <th>{{measurement.name}}</th>
<td>{{measurement.value}}</td> <td>{{measurement.value}}</td>
</tr> </tr>
<tr> <tr>
<th>User</th> <th>User</th>
<td>{{sampleDetailsSample.user}}</td> <td>{{sampleDetailsSample.user}}</td>
</tr> </tr>
<tr> <tr>
<th>Status</th> <th>Status</th>
<td>{{sampleDetailsSample.status}}</td> <td>{{sampleDetailsSample.status}}</td>
</tr> </tr>
</rb-table> </rb-table>
</ng-template> </ng-template>
</ng-template> </ng-template>
<ng-template #restoreConfirm> <ng-template #restoreConfirm>
<rb-dialog dialogTitle="Restore sample"> <rb-dialog dialogTitle="Restore sample">
Do you really want to restore this sample? Do you really want to restore this sample?
</rb-dialog> </rb-dialog>
</ng-template> </ng-template>

View File

@ -1,249 +1,249 @@
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; @import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
.header-addnew { .header-addnew {
margin-bottom: 40px; margin-bottom: 40px;
height: 42px; height: 42px;
& > * { & > * {
display: inline; display: inline;
margin-bottom: 10px; margin-bottom: 10px;
} }
rb-icon-button { rb-icon-button {
float: right; float: right;
} }
} }
rb-table { rb-table {
width: 100%; width: 100%;
} }
td .clickable.rb-ic { td .clickable.rb-ic {
font-size: 1.1rem; font-size: 1.1rem;
color: $color-gray-silver-sand; color: $color-gray-silver-sand;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: #000; color: #000;
} }
} }
rb-form-multi-select{ rb-form-multi-select{
min-width: 9rem; min-width: 9rem;
} }
.status-selection { .status-selection {
overflow: hidden; overflow: hidden;
margin-bottom: 10px; margin-bottom: 10px;
float: left; float: left;
margin-right: 15px; margin-right: 15px;
label { label {
display: block; display: block;
font-weight: 700; font-weight: 700;
font-size: 10px; font-size: 10px;
} }
rb-form-checkbox { rb-form-checkbox {
float: left; float: left;
margin-right: 10px; margin-right: 10px;
margin-top: -10px; margin-top: -10px;
} }
} }
.selection { .selection {
max-width: 230px; max-width: 230px;
float: left; float: left;
} }
.paging { .paging {
height: 50px; height: 50px;
float: left; float: left;
rb-form-input { rb-form-input {
max-width: 65px; max-width: 65px;
} }
> * { > * {
float: left; float: left;
} }
> button { > button {
margin-top: 18px; margin-top: 18px;
} }
> span { > span {
margin-top: 20px; margin-top: 20px;
margin-left: 5px; margin-left: 5px;
} }
} }
.sort-header { .sort-header {
width: 100%; width: 100%;
position: relative; position: relative;
& > span:first-child { & > span:first-child {
max-width: 180px; max-width: 180px;
overflow: hidden; overflow: hidden;
display: block; display: block;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: 20px; margin-right: 20px;
} }
div { div {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
background: #FFF; background: #FFF;
:nth-child(1) { :nth-child(1) {
margin-bottom: -3px; margin-bottom: -3px;
cursor: pointer; cursor: pointer;
} }
:nth-child(2) { :nth-child(2) {
margin-top: -3px; margin-top: -3px;
cursor: pointer; cursor: pointer;
} }
} }
} }
.filters:after { .filters:after {
content:""; content:"";
clear:both; clear:both;
display:block; display:block;
} }
.download { .download {
margin-top: 5px; margin-top: 5px;
float: right; float: right;
& > rb-form-checkbox { & > rb-form-checkbox {
display: inline-block; display: inline-block;
} }
button { button {
margin-right: 10px; margin-right: 10px;
} }
} }
.sort-arr-up { .sort-arr-up {
position: relative; position: relative;
& > span { & > span {
width: 0; width: 0;
height: 0; height: 0;
border-left: 6.3px solid transparent; border-left: 6.3px solid transparent;
border-right: 6.3px solid transparent; border-right: 6.3px solid transparent;
border-bottom: 6.3px solid #000; border-bottom: 6.3px solid #000;
position: absolute; position: absolute;
top: 5px; top: 5px;
display: block; display: block;
left: 2px; left: 2px;
} }
} }
.sort-arr-down { .sort-arr-down {
position: relative; position: relative;
& > span { & > span {
width: 0; width: 0;
height: 0; height: 0;
border-left: 6.3px solid transparent; border-left: 6.3px solid transparent;
border-right: 6.3px solid transparent; border-right: 6.3px solid transparent;
border-top: 6.3px solid #000; border-top: 6.3px solid #000;
position: absolute; position: absolute;
top: 5px; top: 5px;
display: block; display: block;
left: 2px; left: 2px;
} }
} }
.fieldfilters { .fieldfilters {
clear: both; clear: both;
& > div { & > div {
display: grid; display: grid;
grid-template-columns: auto auto 1fr; grid-template-columns: auto auto 1fr;
float: left; float: left;
margin-right: 30px; margin-right: 30px;
} }
& > rb-form-checkbox { & > rb-form-checkbox {
float: left; float: left;
} }
} }
.filtermode { .filtermode {
max-width: 82px; max-width: 82px;
} }
textarea.linkmodal { textarea.linkmodal {
display: block; display: block;
min-width: 600px; min-width: 600px;
min-height: 200px; min-height: 200px;
border: none; border: none;
} }
.filter-inputs > * { .filter-inputs > * {
display: inline-block; display: inline-block;
width: 220px; width: 220px;
} }
.sample-details-table { .sample-details-table {
td { td {
max-width: none; max-width: none;
} }
} }
.validation-close { .validation-close {
margin-left: -1px; margin-left: -1px;
} }
.samples-table tr.clickable { .samples-table tr.clickable {
background: none; background: none;
transition: background-color 0.5s; transition: background-color 0.5s;
&:hover { &:hover {
background: $color-gray-mercury; background: $color-gray-mercury;
} }
} }
::ng-deep .samples-table rb-form-checkbox .input-wrapper { ::ng-deep .samples-table rb-form-checkbox .input-wrapper {
padding-top: 0; padding-top: 0;
margin-top: -4.5px; margin-top: -4.5px;
} }
.link-dialog { .link-dialog {
rb-form-checkbox { rb-form-checkbox {
display: inline-block; display: inline-block;
} }
rb-icon-button { rb-icon-button {
float: right; float: right;
} }
} }
.samples-loading { .samples-loading {
float: left; float: left;
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;
grid-template-rows: 1fr auto 1fr; grid-template-rows: 1fr auto 1fr;
rb-loading-spinner { rb-loading-spinner {
grid-row: span 3; grid-row: span 3;
transform: scale(0.5); transform: scale(0.5);
} }
& > div { & > div {
grid-column: 2 / 3; grid-column: 2 / 3;
grid-row: 2 / 3; grid-row: 2 / 3;
} }
} }
.reset-preferences { .reset-preferences {
float: right; float: right;
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,93 +7,93 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
private host = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com'; private host = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com';
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private storage: LocalStorageService, private storage: LocalStorageService,
private modalService: ModalService, private modalService: ModalService,
private window: Window private window: Window
) { } ) { }
get hostName() { get hostName() {
return this.host; return this.host;
} }
// Main HTTP methods // Main HTTP methods
get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) { get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
this.requestErrorHandler<T>(this.http.get(this.url(url), this.options()), f); this.requestErrorHandler<T>(this.http.get(this.url(url), this.options()), f);
} }
post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) { post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
this.requestErrorHandler<T>(this.http.post(this.url(url), data, this.options()), f); this.requestErrorHandler<T>(this.http.post(this.url(url), data, this.options()), f);
} }
put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) { put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
this.requestErrorHandler<T>(this.http.put(this.url(url), data, this.options()), f); this.requestErrorHandler<T>(this.http.put(this.url(url), data, this.options()), f);
} }
delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) { delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
this.requestErrorHandler<T>(this.http.delete(this.url(url), this.options()), f); this.requestErrorHandler<T>(this.http.delete(this.url(url), this.options()), f);
} }
// Execute request and handle errors // Execute request and handle errors
private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) { private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
observable.subscribe(data => { // Successful request observable.subscribe(data => { // Successful request
f( f(
data.body, data.body,
undefined, undefined,
data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {}) data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {})
); );
}, err => { // Error }, err => { // Error
if (f.length > 1) { // Pass on error if (f.length > 1) { // Pass on error
f(undefined, err, undefined); f(undefined, err, undefined);
} }
else { // Handle directly else { // Handle directly
this.requestError(err); this.requestError(err);
} }
}); });
} }
requestError(err) { // Network error dialog requestError(err) { // Network error dialog
const modalRef = this.modalService.openComponent(ErrorComponent); const modalRef = this.modalService.openComponent(ErrorComponent);
modalRef.instance.message = 'Network request failed!'; modalRef.instance.message = 'Network request failed!';
const details = [err.error.status]; const details = [err.error.status];
if (err.error.details) { if (err.error.details) {
details.push(err.error.details); details.push(err.error.details);
} }
modalRef.instance.details = details; modalRef.instance.details = details;
modalRef.result.then(() => { modalRef.result.then(() => {
this.window.location.reload(); this.window.location.reload();
}); });
} }
private url(url) { // Detect if host was given, otherwise use default host private url(url) { // Detect if host was given, otherwise use default host
if (/http[s]?:\/\//.test(url)) { if (/http[s]?:\/\//.test(url)) {
return url; return url;
} }
else { else {
return this.host + url; return this.host + url;
} }
} }
// Generate request options // Generate request options
private options(): {headers: HttpHeaders, observe: 'body'} { private options(): {headers: HttpHeaders, observe: 'body'} {
return {headers: this.authOptions(), observe: 'response' as 'body'}; return {headers: this.authOptions(), observe: 'response' as 'body'};
} }
// Generate Basic Auth // Generate Basic Auth
private authOptions(): HttpHeaders { private authOptions(): HttpHeaders {
const auth = this.storage.get('basicAuth'); const auth = this.storage.get('basicAuth');
if (auth) { if (auth) {
return new HttpHeaders({Authorization: 'Basic ' + auth}); return new HttpHeaders({Authorization: 'Basic ' + auth});
} }
else { else {
return new HttpHeaders(); return new HttpHeaders();
} }
} }
} }

View File

@ -3,18 +3,18 @@ import {QuickScore} from 'quick-score';
import {of} from 'rxjs'; import {of} from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AutocompleteService { export class AutocompleteService {
constructor() { } constructor() { }
bind(ref, list: string[]) { bind(ref, list: string[]) {
return this.search.bind(ref, list); return this.search.bind(ref, list);
} }
search(arr: string[], str: string) { search(arr: string[], str: string) {
const qs = new QuickScore(arr); const qs = new QuickScore(arr);
return of(str === '' ? [] : qs.search(str).map(e => e.item)); return of(str === '' ? [] : qs.search(str).map(e => e.item));
} }
} }

View File

@ -8,77 +8,77 @@ import {ModelItemModel} from '../models/model-item.model';
import {ModelFileModel} from '../models/model-file.model'; import {ModelFileModel} from '../models/model-file.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
constructor( constructor(
private api: ApiService private api: ApiService
) { } ) { }
private collectionMap = { // List of available collections private collectionMap = { // List of available collections
materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'}, materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'},
materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'}, materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'},
materialGroups: {path: '/material/groups', model: null, type: 'idArray'}, materialGroups: {path: '/material/groups', model: null, type: 'idArray'},
materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'}, materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'},
measurementTemplates: {path: '/template/measurements', model: TemplateModel, type: 'template'}, measurementTemplates: {path: '/template/measurements', model: TemplateModel, type: 'template'},
conditionTemplates: {path: '/template/conditions', model: TemplateModel, type: 'template'}, conditionTemplates: {path: '/template/conditions', model: TemplateModel, type: 'template'},
sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'idArray'}, sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'idArray'},
users: {path: '/users', model: UserModel, type: 'idArray'}, users: {path: '/users', model: UserModel, type: 'idArray'},
modelGroups: {path: '/model/groups', model: ModelItemModel, type: 'array'}, modelGroups: {path: '/model/groups', model: ModelItemModel, type: 'array'},
modelFiles: {path: '/model/files', model: ModelFileModel, type: 'array'}, modelFiles: {path: '/model/files', model: ModelFileModel, type: 'array'},
user: {path: '/user', model: UserModel, type: 'string'}, user: {path: '/user', model: UserModel, type: 'string'},
userKey: {path: '/user/key', model: BaseModel, type: 'string'} userKey: {path: '/user/key', model: BaseModel, type: 'string'}
}; };
arr: {[key: string]: any[]} = {}; // Array of data arr: {[key: string]: any[]} = {}; // Array of data
latest: {[key: string]: any[]} = {}; // Array of latest template versions latest: {[key: string]: any[]} = {}; // Array of latest template versions
id: {[key: string]: {[id: string]: any}} = {}; // Data in format _id: data id: {[key: string]: {[id: string]: any}} = {}; // Data in format _id: data
d: {[key: string]: any} = {}; // Data not in array format d: {[key: string]: any} = {}; // Data not in array format
contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'}; // Global contact data contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'}; // Global contact data
load(collection, f = () => {}) { // Load data load(collection, f = () => {}) { // Load data
if (this.arr[collection]) { // Data already loaded if (this.arr[collection]) { // Data already loaded
f(); f();
} }
else { // Load data else { // Load data
this.api.get<any>(this.collectionMap[collection].path, data => { this.api.get<any>(this.collectionMap[collection].path, data => {
if (this.collectionMap[collection].type !== 'string') { // Array data if (this.collectionMap[collection].type !== 'string') { // Array data
this.arr[collection] = data this.arr[collection] = data
.map( .map(
e => this.collectionMap[collection].model ? e => this.collectionMap[collection].model ?
new this.collectionMap[collection].model().deserialize(e) : e new this.collectionMap[collection].model().deserialize(e) : e
); );
// Load ids // Load ids
if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') { if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') {
this.idReload(collection); this.idReload(collection);
} }
} }
else { // Not array data else { // Not array data
this.d[collection] = new this.collectionMap[collection].model().deserialize(data); this.d[collection] = new this.collectionMap[collection].model().deserialize(data);
} }
f(); f();
}); });
} }
} }
// Generate id object // Generate id object
idReload(collection) { idReload(collection) {
this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {}); this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {});
if (this.collectionMap[collection].type === 'template') { // Generate array with latest templates if (this.collectionMap[collection].type === 'template') { // Generate array with latest templates
const tmpTemplates = {}; const tmpTemplates = {};
this.arr[collection].forEach(template => { this.arr[collection].forEach(template => {
if (tmpTemplates[template.first_id]) { // Already found another version if (tmpTemplates[template.first_id]) { // Already found another version
if (template.version > tmpTemplates[template.first_id].version) { if (template.version > tmpTemplates[template.first_id].version) {
tmpTemplates[template.first_id] = template; tmpTemplates[template.first_id] = template;
} }
} }
else { else {
tmpTemplates[template.first_id] = template; tmpTemplates[template.first_id] = template;
} }
}); });
this.latest[collection] = Object.values(tmpTemplates); this.latest[collection] = Object.values(tmpTemplates);
} }
} }
} }

View File

@ -6,130 +6,130 @@ import {Observable} from 'rxjs';
import {DataService} from './data.service'; import {DataService} from './data.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class LoginService implements CanActivate { export class LoginService implements CanActivate {
private pathPermissions = [ // Minimum level needed for the specified paths private pathPermissions = [ // Minimum level needed for the specified paths
{path: 'materials', permission: 'dev'}, {path: 'materials', permission: 'dev'},
{path: 'templates', permission: 'dev'}, {path: 'templates', permission: 'dev'},
{path: 'changelog', permission: 'dev'}, {path: 'changelog', permission: 'dev'},
{path: 'users', permission: 'admin'} {path: 'users', permission: 'admin'}
]; ];
readonly levels = [ // All user levels in ascending permissions order readonly levels = [ // All user levels in ascending permissions order
'predict', 'predict',
'read', 'read',
'write', 'write',
'dev', 'dev',
'admin' 'admin'
]; ];
// Returns true or false depending on whether the user fulfills the minimum level // Returns true or false depending on whether the user fulfills the minimum level
isLevel: {[level: string]: boolean} = {}; isLevel: {[level: string]: boolean} = {};
hasPrediction = false; // True if user has prediction models specified hasPrediction = false; // True if user has prediction models specified
userId = ''; userId = '';
private loggedIn; private loggedIn;
constructor( constructor(
private api: ApiService, private api: ApiService,
private storage: LocalStorageService, private storage: LocalStorageService,
private router: Router, private router: Router,
private d: DataService private d: DataService
) { ) {
} }
login(username = '', password = '') { login(username = '', password = '') {
return new Promise(resolve => { return new Promise(resolve => {
if (username !== '' || password !== '') { // Some credentials given if (username !== '' || password !== '') { // Some credentials given
let credentials: string[]; let credentials: string[];
const credentialString: string = this.storage.get('basicAuth'); const credentialString: string = this.storage.get('basicAuth');
if (credentialString) { // Found stored credentials if (credentialString) { // Found stored credentials
credentials = atob(credentialString).split(':'); credentials = atob(credentialString).split(':');
} }
else { else {
credentials = ['', '']; credentials = ['', ''];
} }
if (username !== '' && password !== '') { // All credentials given if (username !== '' && password !== '') { // All credentials given
this.storage.set('basicAuth', btoa(username + ':' + password)); this.storage.set('basicAuth', btoa(username + ':' + password));
} }
else if (username !== '') { // Username given else if (username !== '') { // Username given
this.storage.set('basicAuth', btoa(username + ':' + credentials[1])); this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
} }
else if (password !== '') { // Password given else if (password !== '') { // Password given
this.storage.set('basicAuth', btoa(credentials[0] + ':' + password)); this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
} }
} }
this.api.get('/authorized', (data: any, error) => { this.api.get('/authorized', (data: any, error) => {
if (!error) { if (!error) {
if (data.status === 'Authorization successful') { if (data.status === 'Authorization successful') {
this.loggedIn = true; this.loggedIn = true;
this.levels.forEach(level => { this.levels.forEach(level => {
this.isLevel[level] = this.levels.indexOf(data.level) >= this.levels.indexOf(level); this.isLevel[level] = this.levels.indexOf(data.level) >= this.levels.indexOf(level);
if (this.isLevel.dev) { // Set hasPrediction if (this.isLevel.dev) { // Set hasPrediction
this.hasPrediction = true; this.hasPrediction = true;
} }
else { else {
this.d.load('modelGroups', () => { this.d.load('modelGroups', () => {
this.hasPrediction = this.d.arr.modelGroups.length > 0; this.hasPrediction = this.d.arr.modelGroups.length > 0;
}); });
} }
}); });
this.userId = data.user_id; this.userId = data.user_id;
resolve(true); resolve(true);
} else { } else {
this.loggedIn = false; this.loggedIn = false;
this.storage.remove('basicAuth'); this.storage.remove('basicAuth');
resolve(false); resolve(false);
} }
} else { } else {
this.loggedIn = false; this.loggedIn = false;
this.storage.remove('basicAuth'); this.storage.remove('basicAuth');
resolve(false); resolve(false);
} }
}); });
}); });
} }
logout() { logout() {
this.storage.remove('basicAuth'); this.storage.remove('basicAuth');
this.loggedIn = false; this.loggedIn = false;
this.levels.forEach(level => { this.levels.forEach(level => {
this.isLevel[level] = false; this.isLevel[level] = false;
}); });
this.hasPrediction = false; this.hasPrediction = false;
} }
// CanActivate for Angular routing // CanActivate for Angular routing
canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable<boolean> {
return new Observable<boolean>(observer => { return new Observable<boolean>(observer => {
new Promise(resolve => { new Promise(resolve => {
if (this.loggedIn === undefined) { if (this.loggedIn === undefined) {
this.login().then(res => { this.login().then(res => {
resolve(res); resolve(res);
}); });
} }
else { else {
resolve(this.loggedIn); resolve(this.loggedIn);
} }
}).then(res => { }).then(res => {
const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
// Check if level is permitted for path // Check if level is permitted for path
const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]); const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]);
observer.next(ok); observer.next(ok);
observer.complete(); observer.complete();
if (!ok) { if (!ok) {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
}); });
}); });
} }
get isLoggedIn() { get isLoggedIn() {
return this.loggedIn; return this.loggedIn;
} }
get username() { get username() {
return atob(this.storage.get('basicAuth')).split(':')[0]; return atob(this.storage.get('basicAuth')).split(':')[0];
} }
} }

View File

@ -3,183 +3,183 @@ import Joi from '@hapi/joi';
import {AbstractControl} from '@angular/forms'; import {AbstractControl} from '@angular/forms';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ValidationService { export class ValidationService {
private vUsername = Joi.string() private vUsername = Joi.string()
.lowercase() .lowercase()
.pattern(new RegExp('^[a-z0-9-_.]+$')) .pattern(new RegExp('^[a-z0-9-_.]+$'))
.min(1) .min(1)
.max(128); .max(128);
private vPassword = Joi.string() private vPassword = Joi.string()
.min(8) .min(8)
.max(128); .max(128);
constructor() { } constructor() { }
generate(method, args) { // Generate a Validator function generate(method, args) { // Generate a Validator function
return (control: AbstractControl): {[key: string]: any} | null => { return (control: AbstractControl): {[key: string]: any} | null => {
let ok; let ok;
let error; let error;
if (args) { if (args) {
({ok, error} = this[method](control.value, ...args)); ({ok, error} = this[method](control.value, ...args));
} }
else { else {
({ok, error} = this[method](control.value)); ({ok, error} = this[method](control.value));
} }
return ok ? null : { failure: error }; return ok ? null : { failure: error };
}; };
} }
username(data) { username(data) {
const {ignore, error} = this.vUsername.validate(data); const {ignore, error} = this.vUsername.validate(data);
if (error) { if (error) {
return {ok: false, error: 'username must only contain a-z0-9-_.'}; return {ok: false, error: 'username must only contain a-z0-9-_.'};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
password(data) { password(data) {
const {ignore, error} = this.vPassword.validate(data); const {ignore, error} = this.vPassword.validate(data);
if (error) { if (error) {
return {ok: false, error: 'password must have at least 8 characters'}; return {ok: false, error: 'password must have at least 8 characters'};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
string(data, option = null) { string(data, option = null) {
let validator = Joi.string().max(128).allow('', true, false); let validator = Joi.string().max(128).allow('', true, false);
let errorMsg = 'must contain max 128 characters'; let errorMsg = 'must contain max 128 characters';
if (option === 'alphanum') { if (option === 'alphanum') {
validator = validator.alphanum(); validator = validator.alphanum();
errorMsg = 'must contain max 128 alphanumerical characters'; errorMsg = 'must contain max 128 alphanumerical characters';
} }
const {ignore, error} = validator.validate(data); const {ignore, error} = validator.validate(data);
if (error) { if (error) {
return {ok: false, error: errorMsg}; return {ok: false, error: errorMsg};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
stringOf(data, list) { // String must be in list stringOf(data, list) { // String must be in list
const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data); const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data);
if (error) { if (error) {
return {ok: false, error: 'must be one of ' + list.join(', ')}; return {ok: false, error: 'must be one of ' + list.join(', ')};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
stringNin(data, list) { // String must not be in list stringNin(data, list) { // String must not be in list
const {ignore, error} = Joi.string().invalid(...list).validate(data); const {ignore, error} = Joi.string().invalid(...list).validate(data);
if (error) { if (error) {
return {ok: false, error: 'value not allowed'}; return {ok: false, error: 'value not allowed'};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
stringLength(data, length) { // String with maximum length stringLength(data, length) { // String with maximum length
const {ignore, error} = Joi.string().max(length).allow('').validate(data); const {ignore, error} = Joi.string().max(length).allow('').validate(data);
if (error) { if (error) {
return {ok: false, error: 'must contain max ' + length + ' characters'}; return {ok: false, error: 'must contain max ' + length + ' characters'};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
minMax(data, min, max) { // Number between min and max minMax(data, min, max) { // Number between min and max
const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data); const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data);
if (error) { if (error) {
return {ok: false, error: `must be between ${min} and ${max}`}; return {ok: false, error: `must be between ${min} and ${max}`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
min(data, min) { // Number above min min(data, min) { // Number above min
const {ignore, error} = Joi.number().allow('').min(min).validate(data); const {ignore, error} = Joi.number().allow('').min(min).validate(data);
if (error) { if (error) {
return {ok: false, error: `must not be below ${min}`}; return {ok: false, error: `must not be below ${min}`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
max(data, max) { // Number below max max(data, max) { // Number below max
const {ignore, error} = Joi.number().allow('').max(max).validate(data); const {ignore, error} = Joi.number().allow('').max(max).validate(data);
if (error) { if (error) {
return {ok: false, error: `must not be above ${max}`}; return {ok: false, error: `must not be above ${max}`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
url(data) { url(data) {
const {ignore, error} = Joi.string().uri().validate(data); const {ignore, error} = Joi.string().uri().validate(data);
if (error) { if (error) {
return {ok: false, error: `must be a valid URL`}; return {ok: false, error: `must be a valid URL`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
unique(data, list) { unique(data, list) {
const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data); const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data);
if (error) { if (error) {
return {ok: false, error: `values must be unique`}; return {ok: false, error: `values must be unique`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
equal(data, compare) { equal(data, compare) {
if (data !== compare) { if (data !== compare) {
return {ok: false, error: `must be equal`}; return {ok: false, error: `must be equal`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
parameterName(data) { parameterName(data) {
const {ignore, error} = Joi.string() const {ignore, error} = Joi.string()
.max(128) .max(128)
.invalid('condition_template', 'material_template') .invalid('condition_template', 'material_template')
.allow('') .allow('')
.pattern(/^[^.]+$/) .pattern(/^[^.]+$/)
.required() .required()
.messages({'string.pattern.base': 'name must not contain a dot'}) .messages({'string.pattern.base': 'name must not contain a dot'})
.validate(data); .validate(data);
if (error) { if (error) {
return {ok: false, error: error.details[0].message}; return {ok: false, error: error.details[0].message};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
parameterRange(data) { parameterRange(data) {
if (data) { if (data) {
try { try {
const {ignore, error} = Joi.object({ const {ignore, error} = Joi.object({
values: Joi.array() values: Joi.array()
.min(1), .min(1),
min: Joi.number(), min: Joi.number(),
max: Joi.number(), max: Joi.number(),
type: Joi.string() type: Joi.string()
.valid('string', 'number', 'boolean', 'array'), .valid('string', 'number', 'boolean', 'array'),
required: Joi.boolean() required: Joi.boolean()
}) })
.oxor('values', 'min') .oxor('values', 'min')
.oxor('values', 'max') .oxor('values', 'max')
.required() .required()
.validate(JSON.parse(data)); .validate(JSON.parse(data));
if (error) { if (error) {
return {ok: false, error: error.details[0].message}; return {ok: false, error: error.details[0].message};
} }
} }
catch (e) { catch (e) {
return {ok: false, error: `no valid JSON`}; return {ok: false, error: `no valid JSON`};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
else { else {
return {ok: false, error: `no valid value`}; return {ok: false, error: `no valid value`};
} }
} }
} }

View File

@ -1,46 +1,46 @@
<form #userForm="ngForm"> <form #userForm="ngForm">
<rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="user.name" <rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="user.name"
#nameInput="ngModel"> #nameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="email" label="email" email required [(ngModel)]="user.email" ngModel> <rb-form-input name="email" label="email" email required [(ngModel)]="user.email" ngModel>
<ng-template rbFormValidationMessage="email">Invalid email</ng-template> <ng-template rbFormValidationMessage="email">Invalid email</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']" <rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
[(ngModel)]="user.location" #locationInput="ngModel"> [(ngModel)]="user.location" #locationInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input> </rb-form-input>
<rb-array-input [(ngModel)]="user.devices" name="devices" [pushTemplate]="''"> <rb-array-input [(ngModel)]="user.devices" name="devices" [pushTemplate]="''">
<rb-form-input *rbArrayInputItem="let item" rbArrayInputListener="devices" [index]="item.i" <rb-form-input *rbArrayInputItem="let item" rbArrayInputListener="devices" [index]="item.i"
label="device" appValidate="string" [name]="'device-' + item.i" [ngModel]="item.value" label="device" appValidate="string" [name]="'device-' + item.i" [ngModel]="item.value"
#deviceInput="ngModel"> #deviceInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
</rb-array-input> </rb-array-input>
<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveUser()"> <rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveUser()">
Save change Save change
</rb-icon-button> </rb-icon-button>
<span class="message">{{messageUser}}</span> <span class="message">{{messageUser}}</span>
</form> </form>
<h4 class="pass-heading">Change password</h4> <h4 class="pass-heading">Change password</h4>
<form #passForm="ngForm"> <form #passForm="ngForm">
<rb-form-input name="passA" type="password" label="new password" appValidate="password" required <rb-form-input name="passA" type="password" label="new password" appValidate="password" required
[(ngModel)]="password" #passAInput="ngModel"> [(ngModel)]="password" #passAInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-input name="passB" type="password" label="confirm password" appValidate="equal" <rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
[appValidateArgs]="[password]" required #passBInput="ngModel" ngModel> [appValidateArgs]="[password]" required #passBInput="ngModel" ngModel>
<ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<button class="rb-btn rb-primary" type="submit" [disabled]="!passForm.form.valid" (click)="savePass()"> <button class="rb-btn rb-primary" type="submit" [disabled]="!passForm.form.valid" (click)="savePass()">
Change password Change password
</button> </button>
<span class="message">{{messagePass}}</span> <span class="message">{{messagePass}}</span>
</form> </form>

View File

@ -1,7 +1,7 @@
.pass-heading { .pass-heading {
margin-top: 40px; margin-top: 40px;
} }
.message { .message {
margin-left: 20px; margin-left: 20px;
} }

View File

@ -6,63 +6,63 @@ import {LoginService} from '../services/login.service';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'] styleUrls: ['./settings.component.scss']
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
user: UserModel = new UserModel(); // User to edit user: UserModel = new UserModel(); // User to edit
password = ''; // New password password = ''; // New password
messageUser = ''; // Messages for user and pass part messageUser = ''; // Messages for user and pass part
messagePass = ''; messagePass = '';
constructor( constructor(
private api: ApiService, private api: ApiService,
private login: LoginService, private login: LoginService,
private router: Router private router: Router
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.api.get<UserModel>('/user', data => { this.api.get<UserModel>('/user', data => {
this.user.deserialize(data); this.user.deserialize(data);
}); });
} }
saveUser() { saveUser() {
this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => { this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => {
if (err) { if (err) {
this.messageUser = err.error.status; this.messageUser = err.error.status;
} }
else { // Login with new credentials else { // Login with new credentials
this.login.login(data.name).then(res => { this.login.login(data.name).then(res => {
if (res) { if (res) {
this.router.navigate(['/samples']); this.router.navigate(['/samples']);
} }
else { else {
this.messageUser = 'request not successful, try again'; this.messageUser = 'request not successful, try again';
} }
}); });
} }
}); });
} }
savePass() { savePass() {
this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => { this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => {
if (err) { if (err) {
this.messagePass = err.error.status; this.messagePass = err.error.status;
} }
else { // Login with new credentials else { // Login with new credentials
this.login.login('', this.password).then(res => { this.login.login('', this.password).then(res => {
if (res) { if (res) {
this.router.navigate(['/samples']); this.router.navigate(['/samples']);
} }
else { else {
this.messagePass = 'request not successful, try again'; this.messagePass = 'request not successful, try again';
} }
}); });
} }
}); });
} }
} }

View File

@ -1,8 +1,8 @@
import { SizePipe } from './size.pipe'; import { SizePipe } from './size.pipe';
describe('SizePipe', () => { describe('SizePipe', () => {
it('create an instance', () => { it('create an instance', () => {
const pipe = new SizePipe(); const pipe = new SizePipe();
expect(pipe).toBeTruthy(); expect(pipe).toBeTruthy();
}); });
}); });

View File

@ -1,16 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'size' name: 'size'
}) })
export class SizePipe implements PipeTransform { export class SizePipe implements PipeTransform {
transform(value: number, exp: string): string { transform(value: number, exp: string): string {
const divide = ['', 'k', 'M', 'G', 'T'].indexOf(exp); const divide = ['', 'k', 'M', 'G', 'T'].indexOf(exp);
for (let i = 0; i < divide; i ++) { for (let i = 0; i < divide; i ++) {
value = value / 1024; value = value / 1024;
} }
return `${value.toFixed(2)} ${exp}B`; return `${value.toFixed(2)} ${exp}B`;
} }
} }

View File

@ -1,66 +1,66 @@
<rb-form-select name="collectionSelection" label="collection" <rb-form-select name="collectionSelection" label="collection"
[(ngModel)]="collection" (ngModelChange)="loadTemplates()"> [(ngModel)]="collection" (ngModelChange)="loadTemplates()">
<option value="material">Materials</option> <option value="material">Materials</option>
<option value="measurement">Measurements</option> <option value="measurement">Measurements</option>
<option value="condition">Conditions</option> <option value="condition">Conditions</option>
</rb-form-select> </rb-form-select>
<rb-icon-button icon="add" mode="primary" (click)="newTemplate()">New template</rb-icon-button> <rb-icon-button icon="add" mode="primary" (click)="newTemplate()">New template</rb-icon-button>
<div class="list"> <div class="list">
<div class="row"> <div class="row">
<div class="header">Name</div> <div class="header">Name</div>
<div class="header">Version</div> <div class="header">Version</div>
</div> </div>
<ng-container *ngFor="let group of groupsView"> <ng-container *ngFor="let group of groupsView">
<div class="row clickable"> <div class="row clickable">
<div (click)="group.expanded = !group.expanded">{{group.name}}</div> <div (click)="group.expanded = !group.expanded">{{group.name}}</div>
<div (click)="group.expanded = !group.expanded">{{group.version}}</div> <div (click)="group.expanded = !group.expanded">{{group.version}}</div>
</div> </div>
<div class="row" *ngIf="group.expanded" [@inOut]> <div class="row" *ngIf="group.expanded" [@inOut]>
<div class="details"> <div class="details">
<ng-container *ngFor="let template of group.entries"> <ng-container *ngFor="let template of group.entries">
<div>{{template.name}}</div> <div>{{template.name}}</div>
<div>{{template.version}}</div> <div>{{template.version}}</div>
<div>{{template.parameters | parameters}}</div> <div>{{template.parameters | parameters}}</div>
</ng-container> </ng-container>
<div class="template-actions"> <div class="template-actions">
<form #templateForm="ngForm"> <form #templateForm="ngForm">
<div *ngIf="group.edit"> <div *ngIf="group.edit">
<rb-form-input [name]="'name-' + group.name" label="name" appValidate="string" required <rb-form-input [name]="'name-' + group.name" label="name" appValidate="string" required
[(ngModel)]="templateEdit[group.first_id].name" #supplierInput="ngModel"> [(ngModel)]="templateEdit[group.first_id].name" #supplierInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-array-input [(ngModel)]="templateEdit[group.first_id].parameters" [name]="'parameters-' + group.name" <rb-array-input [(ngModel)]="templateEdit[group.first_id].parameters" [name]="'parameters-' + group.name"
[pushTemplate]="{name: '', range: {}, rangeString: '{}'}" pushPath="name" [pushTemplate]="{name: '', range: {}, rangeString: '{}'}" pushPath="name"
class="parameters"> class="parameters">
<ng-container *rbArrayInputItem="let item"> <ng-container *rbArrayInputItem="let item">
<rb-form-input [rbArrayInputListener]="'parameter-name-' + group.name" appValidate="parameterName" <rb-form-input [rbArrayInputListener]="'parameter-name-' + group.name" appValidate="parameterName"
[index]="item.i" [name]="'parameter-name-' + group.name + item.i" [index]="item.i" [name]="'parameter-name-' + group.name + item.i"
label="parameter name" [ngModel]="item.value.name" #parameterName="ngModel"> label="parameter name" [ngModel]="item.value.name" #parameterName="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterName.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{parameterName.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<rb-form-textarea [name]="'parameter-range-' + group.name + item.i" label="range" <rb-form-textarea [name]="'parameter-range-' + group.name + item.i" label="range"
appValidate="parameterRange" [(ngModel)]="item.value.rangeString" appValidate="parameterRange" [(ngModel)]="item.value.rangeString"
#parameterRange="ngModel"> #parameterRange="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterRange.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{parameterRange.errors.failure}}</ng-template>
</rb-form-textarea> </rb-form-textarea>
</ng-container> </ng-container>
</rb-array-input> </rb-array-input>
</div> </div>
<rb-icon-button icon="edit" mode="secondary" (click)="group.edit = !group.edit"> <rb-icon-button icon="edit" mode="secondary" (click)="group.edit = !group.edit">
Edit template Edit template
</rb-icon-button> </rb-icon-button>
<rb-icon-button icon="save" mode="primary" (click)="saveTemplate(group.first_id)" *ngIf="group.edit" <rb-icon-button icon="save" mode="primary" (click)="saveTemplate(group.first_id)" *ngIf="group.edit"
[disabled]="templateForm.invalid"> [disabled]="templateForm.invalid">
Save template Save template
</rb-icon-button> </rb-icon-button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</ng-container> </ng-container>
</div> </div>

View File

@ -2,39 +2,39 @@
.list { .list {
.row { .row {
display: grid; display: grid;
grid-template-columns: 1fr 4fr; grid-template-columns: 1fr 4fr;
border-bottom: 1px solid $color-gray-mercury; border-bottom: 1px solid $color-gray-mercury;
overflow: hidden; overflow: hidden;
& > div { & > div {
padding: 8px 5px; padding: 8px 5px;
&.header { &.header {
font-weight: bold; font-weight: bold;
} }
&.details { &.details {
grid-column: span 2; grid-column: span 2;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 3fr; grid-template-columns: 1fr 1fr 3fr;
background: $color-gray-alabaster; background: $color-gray-alabaster;
.template-actions { .template-actions {
grid-column: span 3; grid-column: span 3;
margin-top: 10px; margin-top: 10px;
.parameters { .parameters {
display: grid; display: grid;
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
} }
rb-icon-button[icon="save"] { rb-icon-button[icon="save"] {
float: right; float: right;
} }
} }
} }
} }
} }
} }

View File

@ -8,132 +8,132 @@ import omit from 'lodash/omit';
import {DataService} from '../services/data.service'; import {DataService} from '../services/data.service';
@Component({ @Component({
selector: 'app-templates', selector: 'app-templates',
templateUrl: './templates.component.html', templateUrl: './templates.component.html',
styleUrls: ['./templates.component.scss'], styleUrls: ['./templates.component.scss'],
animations: [ animations: [
trigger( trigger(
'inOut', [ 'inOut', [
transition(':enter', [ transition(':enter', [
style({height: 0, opacity: 0}), style({height: 0, opacity: 0}),
animate('0.5s ease-out', style({height: '*', opacity: 1})) animate('0.5s ease-out', style({height: '*', opacity: 1}))
]), ]),
transition(':leave', [ transition(':leave', [
style({height: '*', opacity: 1}), style({height: '*', opacity: 1}),
animate('0.5s ease-in', style({height: 0, opacity: 0})) animate('0.5s ease-in', style({height: 0, opacity: 0}))
]) ])
] ]
) )
] ]
}) })
export class TemplatesComponent implements OnInit { export class TemplatesComponent implements OnInit {
collection = 'measurement'; // Collection to view collection = 'measurement'; // Collection to view
templates: TemplateModel[] = []; // All templates of current collection templates: TemplateModel[] = []; // All templates of current collection
templateGroups: {[first_id: string]: TemplateModel[]} = {}; // Templates grouped by first_id templateGroups: {[first_id: string]: TemplateModel[]} = {}; // Templates grouped by first_id
templateEdit: {[first_id: string]: TemplateModel} = {}; // Latest template of each first_id for editing templateEdit: {[first_id: string]: TemplateModel} = {}; // Latest template of each first_id for editing
groupsView: { // Grouped templates groupsView: { // Grouped templates
first_id: string, first_id: string,
name: string, name: string,
version: number, version: number,
expanded: boolean, expanded: boolean,
edit: boolean, edit: boolean,
entries: TemplateModel[] entries: TemplateModel[]
}[] = []; }[] = [];
constructor( constructor(
private api: ApiService, private api: ApiService,
private validate: ValidationService, private validate: ValidationService,
public d: DataService public d: DataService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadTemplates(); this.loadTemplates();
} }
loadTemplates() { loadTemplates() {
this.d.load(this.collection + 'Templates', () => { this.d.load(this.collection + 'Templates', () => {
this.templates = this.d.arr[this.collection + 'Templates']; this.templates = this.d.arr[this.collection + 'Templates'];
this.templateFormat(); this.templateFormat();
}); });
} }
templateFormat() { templateFormat() {
this.templateGroups = {}; this.templateGroups = {};
this.templateEdit = {}; this.templateEdit = {};
this.templates.forEach(template => { // Group templates this.templates.forEach(template => { // Group templates
if (this.templateGroups[template.first_id]) { if (this.templateGroups[template.first_id]) {
this.templateGroups[template.first_id].push(template); this.templateGroups[template.first_id].push(template);
} }
else { else {
this.templateGroups[template.first_id] = [template]; this.templateGroups[template.first_id] = [template];
} }
}); });
Object.keys(this.templateGroups).forEach(id => { Object.keys(this.templateGroups).forEach(id => {
this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version); 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] = cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
this.templateEdit[id].parameters = this.templateEdit[id].parameters this.templateEdit[id].parameters = this.templateEdit[id].parameters
.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; }); .map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
}); });
this.groupsView = Object.values(this.templateGroups) this.groupsView = Object.values(this.templateGroups)
.map(e => ({ .map(e => ({
first_id: e[e.length - 1].first_id, first_id: e[e.length - 1].first_id,
name: e[e.length - 1].name, name: e[e.length - 1].name,
version: e[e.length - 1].version, version: e[e.length - 1].version,
expanded: false, expanded: false,
edit: false, edit: false,
entries: e entries: e
})); }));
} }
saveTemplate(first_id) { saveTemplate(first_id) {
const template = cloneDeep(this.templateEdit[first_id]); const template = cloneDeep(this.templateEdit[first_id]);
template.parameters = template.parameters.filter(e => e.name !== ''); template.parameters = template.parameters.filter(e => e.name !== '');
let valid = true; let valid = true;
valid = valid && this.validate.string(template.name).ok; valid = valid && this.validate.string(template.name).ok;
template.parameters.forEach(parameter => { template.parameters.forEach(parameter => {
valid = valid && this.validate.parameterName(parameter.name).ok; valid = valid && this.validate.parameterName(parameter.name).ok;
valid = valid && this.validate.parameterRange(parameter.rangeString).ok; valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
if (valid) { if (valid) {
parameter.range = JSON.parse(parameter.rangeString); parameter.range = JSON.parse(parameter.rangeString);
} }
}); });
if (valid) { if (valid) {
const sendData = {name: template.name, parameters: template.parameters.map(e => omit(e, ['rangeString']))}; const sendData = {name: template.name, parameters: template.parameters.map(e => omit(e, ['rangeString']))};
if (first_id === 'null') { // New template if (first_id === 'null') { // New template
this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, () => { this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, () => {
delete this.d.arr[this.collection + 'Templates']; delete this.d.arr[this.collection + 'Templates'];
this.d.load(this.collection + 'Templates', () => { this.d.load(this.collection + 'Templates', () => {
this.templates = this.d.arr[this.collection + 'Templates']; this.templates = this.d.arr[this.collection + 'Templates'];
this.templateFormat(); this.templateFormat();
}); });
}); });
} }
else { else {
this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, () => { this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, () => {
delete this.d.arr[this.collection + 'Templates']; delete this.d.arr[this.collection + 'Templates'];
this.d.load(this.collection + 'Templates', () => { this.d.load(this.collection + 'Templates', () => {
this.templates = this.d.arr[this.collection + 'Templates']; this.templates = this.d.arr[this.collection + 'Templates'];
this.templateFormat(); this.templateFormat();
}); });
}); });
} }
} }
} }
newTemplate() { newTemplate() {
if (!this.templateEdit.null) { if (!this.templateEdit.null) {
const template = new TemplateModel(); const template = new TemplateModel();
template.name = 'new template'; template.name = 'new template';
this.groupsView.push({ this.groupsView.push({
first_id: 'null', first_id: 'null',
name: 'new template', name: 'new template',
version: 0, version: 0,
expanded: true, expanded: true,
edit: true, edit: true,
entries: [template] entries: [template]
}); });
this.templateEdit.null = new TemplateModel(); this.templateEdit.null = new TemplateModel();
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More