Merge pull request #4 in ~VLE2FE/dfop-ui from templates to development

* commit 'a4ed8888e61d56bccc1f64936e3395fdb8881adc':
  implemented icon buttons, array input, reformatting, minor sample component improvements
  settings and users dialog
  added templates component
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-07-30 12:39:06 +02:00
commit f799ecc4a6
60 changed files with 1621 additions and 230 deletions

View File

@ -26,8 +26,7 @@
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets",
{ "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }, { "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }
"src/Staticfile"
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"

View File

@ -1,4 +1,4 @@
pushstate: enabled pushstate: enabled
force_https: true force_https: true
root: UI root: UI
location_include: custom-header.conf location_include: ../../headers.conf

9
cf_config/headers.conf Normal file
View File

@ -0,0 +1,9 @@
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src https://definma-api.apps.de1.bosch-iot-cloud.com; form-action 'none'; frame-ancestors 'none'; base-uri 'self'";
add_header X-Frame-Options DENY
add_header X-DNS-Prefetch-Control off
add_header Strict-Transport-Security max-age=15552000
add_header X-Download-Options noopen
add_header X-Content-Type-Options nosniff
add_header X-Permitted-Cross-Domain-Policies none
add_header Referrer-Policy no-referrer
add_header X-XSS-Protection "1; mode=block"

View File

@ -1,9 +1,9 @@
--- ---
applications: applications:
- name: definma - name: definma
path: dist/UI path: dist
buildpacks: buildpacks:
- staticfile_buildpack - staticfile_buildpack
memory: 128M memory: 64M
instances: 1 instances: 1
stack: cflinuxfs3 stack: cflinuxfs3

104
package-lock.json generated
View File

@ -2316,6 +2316,12 @@
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true "dev": true
}, },
"acorn-walk": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
"adm-zip": { "adm-zip": {
"version": "0.4.13", "version": "0.4.13",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz",
@ -2892,6 +2898,18 @@
"callsite": "1.0.0" "callsite": "1.0.0"
} }
}, },
"bfj": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz",
"integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"check-types": "^8.0.3",
"hoopy": "^0.1.4",
"tryer": "^1.0.1"
}
},
"big.js": { "big.js": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -3393,6 +3411,12 @@
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz",
"integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g==" "integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g=="
}, },
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
"integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==",
"dev": true
},
"chokidar": { "chokidar": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
@ -4782,6 +4806,12 @@
"is-obj": "^2.0.0" "is-obj": "^2.0.0"
} }
}, },
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
"duplexify": { "duplexify": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -4810,6 +4840,12 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true "dev": true
}, },
"ejs": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.446", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
@ -5483,6 +5519,12 @@
"minimatch": "^3.0.3" "minimatch": "^3.0.3"
} }
}, },
"filesize": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
"dev": true
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -5879,6 +5921,16 @@
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true "dev": true
}, },
"gzip-size": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
"integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
"dev": true,
"requires": {
"duplexer": "^0.1.1",
"pify": "^4.0.1"
}
},
"handle-thing": { "handle-thing": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -6063,6 +6115,12 @@
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
"integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
"dev": true
},
"hosted-git-info": { "hosted-git-info": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz",
@ -9242,6 +9300,12 @@
"is-wsl": "^2.1.1" "is-wsl": "^2.1.1"
} }
}, },
"opener": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"dev": true
},
"opn": { "opn": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@ -12324,6 +12388,11 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true "dev": true
}, },
"str-compare": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/str-compare/-/str-compare-0.1.2.tgz",
"integrity": "sha1-eOaGGlccGiKhnq4Q5wmWt9z9r0Y="
},
"stream-browserify": { "stream-browserify": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@ -12914,6 +12983,12 @@
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true "dev": true
}, },
"tryer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
"dev": true
},
"ts-node": { "ts-node": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
@ -13726,6 +13801,35 @@
} }
} }
}, },
"webpack-bundle-analyzer": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz",
"integrity": "sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==",
"dev": true,
"requires": {
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1",
"bfj": "^6.1.1",
"chalk": "^2.4.1",
"commander": "^2.18.0",
"ejs": "^2.6.1",
"express": "^4.16.3",
"filesize": "^3.6.1",
"gzip-size": "^5.0.0",
"lodash": "^4.17.15",
"mkdirp": "^0.5.1",
"opener": "^1.5.1",
"ws": "^6.0.0"
},
"dependencies": {
"acorn": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz",
"integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==",
"dev": true
}
}
},
"webpack-dev-middleware": { "webpack-dev-middleware": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz",

View File

@ -5,12 +5,13 @@
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build --prod --aot", "build": "ng build --prod --aot",
"build-push": "ng build --prod --aot && cf push", "build-push": "ng build --prod --aot && copy /Y cf_config\\ dist && cf push",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"coverage": "ng test --no-watch --code-coverage", "coverage": "ng test --no-watch --code-coverage",
"api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js" "api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js",
"bundle-report": "ng build --prod --aot --stats-json && webpack-bundle-analyzer dist/UI/stats-es2015.json"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -23,7 +24,8 @@
"@angular/platform-browser-dynamic": "~9.1.7", "@angular/platform-browser-dynamic": "~9.1.7",
"@angular/router": "~9.1.7", "@angular/router": "~9.1.7",
"@hapi/joi": "^17.1.1", "@hapi/joi": "^17.1.1",
"@inst-iot/bosch-angular-ui-components": "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz", "@inst-iot/bosch-angular-ui-components":
"file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz",
"angular-2-local-storage": "^3.0.2", "angular-2-local-storage": "^3.0.2",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"chartjs-plugin-datalabels": "^0.7.0", "chartjs-plugin-datalabels": "^0.7.0",
@ -32,6 +34,7 @@
"ng2-charts": "^2.3.2", "ng2-charts": "^2.3.2",
"quick-score": "0.0.8", "quick-score": "0.0.8",
"rxjs": "~6.5.5", "rxjs": "~6.5.5",
"str-compare": "^0.1.2",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"zone.js": "~0.10.2" "zone.js": "~0.10.2"
}, },
@ -54,6 +57,7 @@
"protractor": "~5.4.0", "protractor": "~5.4.0",
"ts-node": "~7.0.0", "ts-node": "~7.0.0",
"tslint": "~5.15.0", "tslint": "~5.15.0",
"typescript": "~3.8.3" "typescript": "~3.8.3",
"webpack-bundle-analyzer": "^3.8.0"
} }
} }

View File

@ -5,6 +5,9 @@ import {LoginService} from './services/login.service';
import {SampleComponent} from './sample/sample.component'; import {SampleComponent} from './sample/sample.component';
import {SamplesComponent} from './samples/samples.component'; import {SamplesComponent} from './samples/samples.component';
import {DocumentationComponent} from './documentation/documentation.component'; import {DocumentationComponent} from './documentation/documentation.component';
import {TemplatesComponent} from './templates/templates.component';
import {SettingsComponent} from './settings/settings.component';
import {UsersComponent} from './users/users.component';
const routes: Routes = [ const routes: Routes = [
@ -13,6 +16,10 @@ const routes: Routes = [
{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: 'templates', component: TemplatesComponent, canActivate: [LoginService]},
// {path: 'users', component: UsersComponent, canActivate: [LoginService]},
{path: 'users', component: UsersComponent}, // TODO: change
{path: 'settings', component: SettingsComponent, canActivate: [LoginService]},
{path: 'documentation', component: DocumentationComponent}, {path: 'documentation', component: DocumentationComponent},
// if not authenticated // if not authenticated

View File

@ -2,6 +2,8 @@
<nav *rbMainNavItems> <nav *rbMainNavItems>
<a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a> <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
<a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isLoggedIn">Samples</a> <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isLoggedIn">Samples</a>
<a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="loginService.is('maintain')">Templates</a>
<a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="loginService.is('admin')">Users</a>
<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a> <a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
</nav> </nav>
@ -10,12 +12,9 @@
<a href="javascript:" [rbPopover]="userPopover" [anchor]="popoverAnchor"> <a href="javascript:" [rbPopover]="userPopover" [anchor]="popoverAnchor">
{{loginService.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a> {{loginService.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a>
</nav> </nav>
<ng-template #userPopover> <ng-template #userPopover let-close="close">
<div class="spacing"> <div class="spacing">
<p> <a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span>&nbsp;&nbsp;Settings</a>
<!-- Some user specific information-->
</p>
<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>

View File

@ -3,3 +3,9 @@
font-size: 32px; font-size: 32px;
margin-right: 40px; margin-right: 40px;
} }
.spacing {
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 10px;
}

View File

@ -8,6 +8,13 @@ import {Router} from '@angular/router';
// TODO: filter by not completely filled/no measurements // TODO: filter by not completely filled/no measurements
// TODO: account // TODO: account
// TODO: admin user handling, template pages, validation of samples // TODO: admin user handling, template pages, validation of samples
// TODO: activate filter on start typing
// TODO: Build IconComponent free lib version because of CSP
// TODO: more helmet headers, UI presentatin plan
// TODO: sort material numbers, filter field measurements
// TODO: get rid of chart.js (+moment.js) and lodash
// TODO: look into CSS/XHR/Anfragen tab of console
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View File

@ -11,7 +11,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {LocalStorageModule} from 'angular-2-local-storage'; import {LocalStorageModule} from 'angular-2-local-storage';
import {HttpClientModule} from '@angular/common/http'; import {HttpClientModule} from '@angular/common/http';
import { SamplesComponent } from './samples/samples.component'; import { SamplesComponent } from './samples/samples.component';
import {RbTableModule} from './rb-table/rb-table.module'; import {RbCustomInputsModule} from './rb-custom-inputs/rb-custom-inputs.module';
import { SampleComponent } from './sample/sample.component'; import { SampleComponent } from './sample/sample.component';
import { ValidateDirective } from './validate.directive'; import { ValidateDirective } from './validate.directive';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
@ -21,6 +21,10 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { DocumentationComponent } from './documentation/documentation.component'; import { DocumentationComponent } from './documentation/documentation.component';
import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component';
import { ExistsPipe } from './exists.pipe'; import { ExistsPipe } from './exists.pipe';
import { TemplatesComponent } from './templates/templates.component';
import { ParametersPipe } from './parameters.pipe';
import { SettingsComponent } from './settings/settings.component';
import { UsersComponent } from './users/users.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -34,7 +38,11 @@ import { ExistsPipe } from './exists.pipe';
ObjectPipe, ObjectPipe,
DocumentationComponent, DocumentationComponent,
ImgMagnifierComponent, ImgMagnifierComponent,
ExistsPipe ExistsPipe,
TemplatesComponent,
ParametersPipe,
SettingsComponent,
UsersComponent
], ],
imports: [ imports: [
LocalStorageModule.forRoot({ LocalStorageModule.forRoot({
@ -47,7 +55,7 @@ import { ExistsPipe } from './exists.pipe';
RbUiComponentsModule, RbUiComponentsModule,
FormsModule, FormsModule,
HttpClientModule, HttpClientModule,
RbTableModule, RbCustomInputsModule,
ReactiveFormsModule, ReactiveFormsModule,
FormFieldsModule, FormFieldsModule,
CommonModule, CommonModule,

View File

@ -6,10 +6,14 @@
<rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username" #usernameInput="ngModel"> <rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username" #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 type="password" name="password" label="password" appValidate="password" required [(ngModel)]="password" #passwordInput="ngModel"> <rb-form-input *ngIf="!passreset" type="password" name="password" label="password" appValidate="password" required [(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>
<button class="rb-btn rb-primary login-button" (click)="login()" type="submit" [disabled]="!loginForm.form.valid">Login</button> <rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email" #emailInput="ngModel">
<span class="message">{{message}}</span> <ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template>
</rb-form-input>
<a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a>
<button class="rb-btn rb-primary login-button" (click)="login()" type="submit" [disabled]="!loginForm.form.valid">{{passreset ? 'Send' : 'Login'}}</button>
<div class="message">{{message}}</div>
</form> </form>
</div> </div>

View File

@ -4,9 +4,14 @@
.message { .message {
font-size: 13px; font-size: 13px;
white-space: pre-line; margin-top: 10px;
} }
.login-button { .login-button {
margin-right: 10px; margin-right: 10px;
} }
.forgot-pass {
display: block;
margin-bottom: 1rem;
}

View File

@ -2,6 +2,7 @@ import {Component, OnInit, ViewChild} from '@angular/core';
import {ValidationService} from '../services/validation.service'; import {ValidationService} from '../services/validation.service';
import {LoginService} from '../services/login.service'; import {LoginService} from '../services/login.service';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ApiService} from '../services/api.service';
@Component({ @Component({
@ -13,13 +14,17 @@ export class LoginComponent implements OnInit {
username = ''; // credentials username = ''; // credentials
password = ''; password = '';
email = '';
message = ''; // message below login fields message = ''; // message below login fields
passreset = false;
@ViewChild('loginForm') loginForm; @ViewChild('loginForm') loginForm;
constructor( constructor(
private validate: ValidationService, private validate: ValidationService,
private loginService: LoginService, private loginService: LoginService,
private api: ApiService,
private router: Router private router: Router
) { } ) { }
@ -27,6 +32,17 @@ export class LoginComponent implements OnInit {
} }
login() { login() {
if (this.passreset) {
this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => {
if (err) {
this.message = 'Could not find a valid user';
}
else {
this.message = 'Password reset, check your inbox';
}
});
}
else {
this.loginService.login(this.username, this.password).then(ok => { this.loginService.login(this.username, this.password).then(ok => {
if (ok) { if (ok) {
this.message = 'Login successful'; this.message = 'Login successful';
@ -37,4 +53,5 @@ export class LoginComponent implements OnInit {
} }
}); });
} }
}
} }

View File

@ -17,6 +17,7 @@ export class SampleModel extends BaseModel {
note_id: IdModel = null; note_id: IdModel = null;
user_id: IdModel = null; user_id: IdModel = null;
notes: {comment: string, sample_references: {sample_id: IdModel, relation: string}[], custom_fields: {[prop: string]: string}} = {comment: '', sample_references: [], custom_fields: {}}; notes: {comment: string, sample_references: {sample_id: IdModel, relation: string}[], custom_fields: {[prop: string]: string}} = {comment: '', sample_references: [], custom_fields: {}};
added: Date = null;
deserialize(input: any): this { deserialize(input: any): this {
Object.assign(this, input); Object.assign(this, input);
@ -27,6 +28,9 @@ export class SampleModel extends BaseModel {
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')) {
this.added = new Date(input.added);
}
return this; return this;
} }

View File

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

View File

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

View File

@ -0,0 +1,28 @@
import _ from 'lodash';
import {BaseModel} from './base.model';
import {IdModel} from './id.model';
export class UserModel extends BaseModel{
_id: IdModel = null;
name = '';
origName = '';
email = '';
level = '';
location = '';
device_name = '';
edit = false;
deserialize(input: any): this {
Object.assign(this, input);
this.origName = this.name;
return this;
}
sendFormat(mode = 'user') {
const keys = ['name', 'email', 'location', 'device_name'];
if (mode === 'admin') {
keys.push('level');
}
return _.pick(this, keys);
}
}

View File

@ -1,4 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import _ from 'lodash';
@Pipe({ @Pipe({
name: 'object', name: 'object',
@ -6,8 +7,9 @@ import { Pipe, PipeTransform } from '@angular/core';
}) })
export class ObjectPipe implements PipeTransform { export class ObjectPipe implements PipeTransform {
transform(value: object): string { transform(value: object, omit: string[] = []): string {
return value ? JSON.stringify(value) : ''; const res = _.omit(value, omit);
return res && Object.keys(res).length ? JSON.stringify(res) : '';
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ArrayInputHelperService } from './array-input-helper.service';
describe('ArrayInputHelperService', () => {
let service: ArrayInputHelperService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ArrayInputHelperService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import {Observable, Subject} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ArrayInputHelperService {
com: Subject<{ id: string, index: number, value: any }> = new Subject();
constructor() { }
values(id: string) {
return new Observable<{index: number, value: any}>(observer => {
this.com.subscribe(data => {
if (data.id === id) {
observer.next({index: data.index, value: data.value});
}
});
});
}
newValue(id: string, index: number, value: any) {
this.com.next({id, index, value});
}
}

View File

@ -0,0 +1,3 @@
<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>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RbArrayInputComponent } from './rb-array-input.component';
describe('RbArrayInputComponent', () => {
let component: RbArrayInputComponent;
let fixture: ComponentFixture<RbArrayInputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RbArrayInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RbArrayInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

@ -0,0 +1,4 @@
<button class="rb-btn rb" [ngClass]="'rb-' + mode" [type]="type" [disabled]="disabled">
<span class="rb-ic" [ngClass]="'rb-ic-' + icon"></span>&nbsp;&nbsp;
<ng-content></ng-content>
</button>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RbIconButtonComponent } from './rb-icon-button.component';
describe('RbIconButtonComponent', () => {
let component: RbIconButtonComponent;
let fixture: ComponentFixture<RbIconButtonComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RbIconButtonComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RbIconButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
@Component({ @Component({
// 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']

View File

@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RbTableComponent } from './rb-table/rb-table.component';
@NgModule({
declarations: [
RbTableComponent
],
imports: [
CommonModule
],
exports: [
RbTableComponent
]
})
export class RbTableModule { }

View File

@ -1,4 +1,4 @@
<h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2> <script src="../models/template.model.ts"></script><h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2>
<rb-loading-spinner *ngIf="loading"></rb-loading-spinner> <rb-loading-spinner *ngIf="loading"></rb-loading-spinner>
@ -6,28 +6,48 @@
<!--<form #sampleForm="ngForm">--> <!--<form #sampleForm="ngForm">-->
<div class="sample"> <div class="sample">
<div> <div>
<rb-form-input name="materialname" label="material name" [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" [rbDebounceTime]="0" [rbInitialOpen]="true" (keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" appValidate="stringOf" [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true" #materialNameInput="ngModel"> <rb-form-input name="materialname" label="material name" [rbDebounceTime]="0" [rbInitialOpen]="true"
[rbFormInputAutocomplete]="autocomplete.bind(this, ac.materialNames)" appValidate="stringOf"
(keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel
[appValidateArgs]="[ac.materialName]" required [(ngModel)]="material.name" [autofocus]="true">
<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>
<button class="rb-btn rb-secondary" type="button" (click)="setNewMaterial(!newMaterial)"><span class="rb-ic rb-ic-add"></span>&nbsp;New material</button> <rb-icon-button icon="add" mode="secondary" (click)="setNewMaterial(!newMaterial)">New material</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" [rbFormInputAutocomplete]="autocomplete.bind(this, suppliers)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [(ngModel)]="material.supplier" #supplierInput="ngModel"> <rb-form-input name="supplier" label="supplier" [rbFormInputAutocomplete]="autocomplete.bind(this, ac.supplier)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.supplier" #supplierInput="ngModel"
(focusout)="checkTypo('supplier', modalWarning)">
<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" [rbFormInputAutocomplete]="autocomplete.bind(this, groups)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [(ngModel)]="material.group" #groupInput="ngModel"> <rb-form-input name="group" label="group" [rbFormInputAutocomplete]="autocomplete.bind(this, ac.mgroup)"
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
[(ngModel)]="material.group" #groupInput="ngModel"
(focusout)="checkTypo('mgroup', modalWarning)">
<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
</rb-form-input> </rb-form-input>
<div class="material-numbers"> <ng-template #modalWarning>
<rb-form-input *ngFor="let ignore of [].constructor(material.numbers.length); index as i" label="material number" appValidate="string" [name]="'material.number-' + i" (keyup)="handleMaterialNumbers()" [(ngModel)]="material.numbers[i]" ngModel></rb-form-input> <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
</div> The specified {{modalText.list}} could not be found in the list. <br>
<rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectMaterialTemplate($event)" [ngModel]="material.properties.material_template"> Did you mean {{modalText.suggestion}}?
</rb-alert>
</ng-template>
<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
[ngModel]="item.value"></rb-form-input>
</rb-array-input>
<rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectMaterialTemplate($event)"
[ngModel]="material.properties.material_template">
<option *ngFor="let m of materialTemplates" [value]="m._id">{{m.name}}</option> <option *ngFor="let m of materialTemplates" [value]="m._id">{{m.name}}</option>
</rb-form-select> </rb-form-select>
<rb-form-input *ngFor="let parameter of materialTemplate.parameters; index as i" [name]="'materialParameter' + i" [label]="parameter.name" appValidate="string" required [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel"> <rb-form-input *ngFor="let parameter of materialTemplate.parameters; index as i" [name]="'materialParameter' + i"
[label]="parameter.name" appValidate="string" required
[(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="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>
@ -36,11 +56,13 @@
&nbsp; &nbsp;
<div> <div>
<rb-form-input name="type" label="type" appValidate="string" required [(ngModel)]="sample.type" #typeInput="ngModel"> <rb-form-input name="type" label="type" appValidate="string" required [(ngModel)]="sample.type"
#typeInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{typeInput.errors.failure}}</ng-template> <ng-template rbFormValidationMessage="failure">{{typeInput.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="color" label="color" appValidate="string" required [(ngModel)]="sample.color" #colorInput="ngModel"> <rb-form-input name="color" label="color" appValidate="string" required [(ngModel)]="sample.color"
#colorInput="ngModel">
<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>
@ -51,31 +73,43 @@
</div> </div>
<div class="notes"> <div class="notes">
<rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" [(ngModel)]="sample.notes.comment" #commentInput="ngModel"> <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]"
[(ngModel)]="sample.notes.comment" #commentInput="ngModel">
<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" [rbFormInputAutocomplete]="sampleReferenceListBind()" [rbDebounceTime]="300" appValidate="stringOf" [appValidateArgs]="[sampleReferenceAutocomplete[i]]" (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" #idInput="ngModel"> <rb-form-input [name]="'sr-id' + i" label="sample number" [rbFormInputAutocomplete]="sampleReferenceListBind()"
[rbDebounceTime]="300" appValidate="stringOf"
[appValidateArgs]="[sampleReferenceAutocomplete[i]]"
(ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel>
<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] !== ''" [(ngModel)]="reference[1]"> <rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''"
[(ngModel)]="reference[1]">
<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>
<div *ngFor="let field of customFields; index as i" class="two-col" [@inOut]> <rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0"
class="two-col" [@inOut]>
<ng-container *rbArrayInputItem="let item">
<div> <div>
<rb-form-input [name]="'cf-key' + i" label="key" [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(i)]" (ngModelChange)="adjustCustomFields($event, i)" [ngModel]="field[0]" #keyInput="ngModel"> <rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i"
[rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbDebounceTime]="0"
[rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]"
[ngModel]="item.value[0]" #keyInput="ngModel">
<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' + i" label="value" appValidate="string" [required]="field[0] !== ''" [(ngModel)]="field[1]"> <rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string" [required]="item.value[0] !== ''"
[ngModel]="item.value[1]">
<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> </ng-container>
</rb-array-input>
</div> </div>
&nbsp; &nbsp;
@ -83,14 +117,18 @@
<div class="conditions shaded-container"> <div class="conditions shaded-container">
<h4> <h4>
Condition Condition
<button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition()" [disabled]="!conditionTemplates">{{condition ? 'Do not set condition' : 'Set condition'}}</button> <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition()"
[disabled]="!conditionTemplates">{{condition ? 'Do not set condition' : 'Set condition'}}</button>
</h4> </h4>
<div *ngIf="condition" [@inOut]> <div *ngIf="condition" [@inOut]>
<rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectCondition($event)" [ngModel]="condition._id"> <rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectCondition($event)"
[ngModel]="condition._id">
<option *ngFor="let c of conditionTemplates" [value]="c._id">{{c.name}}</option> <option *ngFor="let c of conditionTemplates" [value]="c._id">{{c.name}}</option>
</rb-form-select> </rb-form-select>
<rb-form-input *ngFor="let parameter of condition.parameters; index as i" [name]="'conditionParameter' + i" [label]="parameter.name" appValidate="string" required [(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel"> <rb-form-input *ngFor="let parameter of condition.parameters; index as i" [name]="'conditionParameter' + i"
[label]="parameter.name" appValidate="string" required
[(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>
@ -102,16 +140,23 @@
<div class="measurements shaded-container"> <div class="measurements shaded-container">
<h4>Measurements</h4> <h4>Measurements</h4>
<div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut]> <div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut]>
<rb-form-select name="measurementTemplateSelect" label="Template" [(ngModel)]="measurement.measurement_template" (ngModelChange)="clearChart(mIndex)"> <rb-form-select name="measurementTemplateSelect" label="Template" [(ngModel)]="measurement.measurement_template"
(ngModelChange)="clearChart(mIndex)">
<option *ngFor="let m of measurementTemplates" [value]="m._id">{{m.name}}</option> <option *ngFor="let m of measurementTemplates" [value]="m._id">{{m.name}}</option>
</rb-form-select> </rb-form-select>
<div *ngFor="let parameter of getMeasurementTemplate(measurement.measurement_template).parameters; index as pIndex"> <div *ngFor="let parameter of getMeasurementTemplate(measurement.measurement_template).parameters;
<rb-form-input *ngIf="!parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex" [label]="parameter.name" appValidate="string" [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel"> index as pIndex">
<rb-form-input *ngIf="!parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex"
[label]="parameter.name" appValidate="string" [(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>
<rb-form-file *ngIf="parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex" [label]="parameter.name" maxSize="10000000" (ngModelChange)="fileToArray($event, mIndex, parameter.name)" placeholder="Select file or drag and drop" dragDrop required ngModel> <rb-form-file *ngIf="parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex"
[label]="parameter.name" maxSize="10000000" multiple
(ngModelChange)="fileToArray($event, mIndex, parameter.name)"
placeholder="Select file or drag and drop" dragDrop required 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>
<canvas baseChart *ngIf="parameter.range.type && charts[mIndex][0].data.length > 0" class="dpt-chart" [@inOut] <canvas baseChart *ngIf="parameter.range.type && charts[mIndex][0].data.length > 0" class="dpt-chart" [@inOut]
@ -122,20 +167,26 @@
chartType="scatter"> chartType="scatter">
</canvas> </canvas>
</div> </div>
<rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(mIndex)"
<button class="rb-btn rb-danger" type="button" (click)="removeMeasurement(mIndex)"><span class="rb-ic rb-ic-delete"></span>&nbsp;Delete measurement</button> [disabled]="!measurementTemplates">
Delete measurement
</rb-icon-button>
</div> </div>
&nbsp; &nbsp;
<div> <div>
<button class="rb-btn rb-secondary" type="button" (click)="addMeasurement()" [disabled]="!measurementTemplates"><span class="rb-ic rb-ic-add"></span>&nbsp;New measurement</button> <rb-icon-button icon="add" mode="secondary" (click)="addMeasurement()" [disabled]="!measurementTemplates">
New measurement
</rb-icon-button>
</div> </div>
</div> </div>
<div> <div>
<button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid">Save sample</button> <button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid">
Save sample
</button>
</div> </div>
</form> </form>

View File

@ -2,7 +2,7 @@
// //
// import { SampleComponent } from './sample.component'; // import { SampleComponent } from './sample.component';
// //
// // TODO // // TODO: tests
// //
// describe('SampleComponent', () => { // describe('SampleComponent', () => {
// let component: SampleComponent; // let component: SampleComponent;

View File

@ -1,8 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import strCompare from 'str-compare';
import { import {
AfterContentChecked, AfterContentChecked,
Component, Component,
OnInit, OnInit, TemplateRef,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
@ -17,18 +18,14 @@ import {MeasurementModel} from '../models/measurement.model';
import { ChartOptions } from 'chart.js'; import { ChartOptions } from 'chart.js';
import {animate, style, transition, trigger} from '@angular/animations'; import {animate, style, transition, trigger} from '@angular/animations';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
import {UserModel} from '../models/user.model';
// TODO: tests
// TODO: confirmation for new group/supplier
// TODO: work on better recognition for file input // TODO: work on better recognition for file input
// TODO: only show condition (if not set) and measurements in edit sample dialog at first // TODO: only show condition (if not set) and measurements in edit sample dialog at first
// TODO: multiple spectra
// TODO: multiple samples for base data, extend multiple measurements, conditions // TODO: multiple samples for base data, extend multiple measurements, conditions
// TODO: material properties, color (in material and sample (not required))
// TODO: API $in Regex
@Component({ @Component({
selector: 'app-sample', selector: 'app-sample',
templateUrl: './sample.component.html', templateUrl: './sample.component.html',
@ -55,16 +52,18 @@ export class SampleComponent implements OnInit, AfterContentChecked {
new; // true if new sample should be created new; // true if new sample should be created
newMaterial = false; // true if new material should be created newMaterial = false; // true if new material should be created
materials: MaterialModel[] = []; // all materials materials: MaterialModel[] = []; // all materials
suppliers: string[] = []; // all suppliers ac: {[group: string]: string[]} = { // autocomplete data
groups: string[] = []; // all groups supplier: [],
group: [],
materialName: []
};
conditionTemplates: TemplateModel[]; // all conditions conditionTemplates: TemplateModel[]; // all conditions
condition: TemplateModel | null = null; // selected condition condition: TemplateModel | null = null; // selected condition
materialTemplates: TemplateModel[]; // all material templates materialTemplates: TemplateModel[]; // all material templates
materialTemplate: TemplateModel | null = null; // selected material template materialTemplate: TemplateModel | null = null; // selected material template
materialNames = []; // names of all materials
material = new MaterialModel(); // object of current selected material material = new MaterialModel(); // object of current selected material
sample = new SampleModel(); sample = new SampleModel();
customFields: [string, string][] = [['', '']]; customFields: [string, string][];
sampleReferences: [string, string, string][] = [['', '', '']]; sampleReferences: [string, string, string][] = [['', '', '']];
sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db
currentSRIndex = 0; // index of last entered sample reference currentSRIndex = 0; // index of last entered sample reference
@ -74,7 +73,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
measurementTemplates: TemplateModel[]; measurementTemplates: TemplateModel[];
loading = 0; // number of currently loading instances loading = 0; // number of currently loading instances
checkFormAfterInit = false; checkFormAfterInit = false;
charts = []; // chart data for spectrums modalText = {list: '', suggestion: ''};
charts = []; // chart data for spectra
defaultDevice = '';
readonly chartInit = [{ readonly chartInit = [{
data: [], data: [],
label: 'Spectrum', label: 'Spectrum',
@ -101,7 +102,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
private route: ActivatedRoute, private route: ActivatedRoute,
private api: ApiService, private api: ApiService,
private validation: ValidationService, private validation: ValidationService,
public autocomplete: AutocompleteService public autocomplete: AutocompleteService,
private modal: ModalService
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@ -109,15 +111,15 @@ export class SampleComponent implements OnInit, AfterContentChecked {
this.loading = 7; this.loading = 7;
this.api.get<MaterialModel[]>('/materials?status=all', (data: any) => { this.api.get<MaterialModel[]>('/materials?status=all', (data: any) => {
this.materials = data.map(e => new MaterialModel().deserialize(e)); this.materials = data.map(e => new MaterialModel().deserialize(e));
this.materialNames = data.map(e => e.name); this.ac.materialName = data.map(e => e.name);
this.loading--; this.loading--;
}); });
this.api.get<string[]>('/material/suppliers', (data: any) => { this.api.get<string[]>('/material/suppliers', (data: any) => {
this.suppliers = data; this.ac.supplier = data;
this.loading--; this.loading--;
}); });
this.api.get<string[]>('/material/groups', (data: any) => { this.api.get<string[]>('/material/groups', (data: any) => {
this.groups = data; this.ac.mgroup = data;
this.loading--; this.loading--;
}); });
this.api.get<TemplateModel[]>('/template/conditions', data => { this.api.get<TemplateModel[]>('/template/conditions', data => {
@ -129,6 +131,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
this.selectMaterialTemplate(this.materialTemplates[0]._id); this.selectMaterialTemplate(this.materialTemplates[0]._id);
this.loading--; this.loading--;
}); });
this.api.get<UserModel>('/user', data => {
this.defaultDevice = data.device_name;
});
this.api.get<TemplateModel[]>('/template/measurements', data => { this.api.get<TemplateModel[]>('/template/measurements', data => {
this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e)); this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e));
if (!this.new) { if (!this.new) {
@ -149,7 +154,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
}); });
this.material = sData.material; this.material = sData.material;
this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ?
Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']];
if (this.sample.notes.sample_references.length) { if (this.sample.notes.sample_references.length) {
this.sampleReferences = []; this.sampleReferences = [];
this.sampleReferenceAutocomplete = []; this.sampleReferenceAutocomplete = [];
@ -185,7 +191,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
ngAfterContentChecked() { ngAfterContentChecked() {
// attach validators to dynamic condition fields when all values are available and template was fully created // attach validators to dynamic condition fields when all values are available and template was fully created
if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 && this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) { if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 &&
this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) {
for (const i in this.condition.parameters) { for (const i in this.condition.parameters) {
if (this.condition.parameters[i]) { if (this.condition.parameters[i]) {
this.attachValidator('conditionParameter' + i, this.condition.parameters[i].range, true); this.attachValidator('conditionParameter' + i, this.condition.parameters[i].range, true);
@ -194,7 +201,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
// attach validators to dynamic material fields when all values are available and template was fully created // attach validators to dynamic material fields when all values are available and template was fully created
if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) { if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 &&
this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) {
for (const i in this.materialTemplate.parameters) { for (const i in this.materialTemplate.parameters) {
if (this.materialTemplate.parameters[i]) { if (this.materialTemplate.parameters[i]) {
this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true); this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true);
@ -250,7 +258,6 @@ export class SampleComponent implements OnInit, AfterContentChecked {
saveSample() { saveSample() {
new Promise<void>(resolve => { new Promise<void>(resolve => {
if (this.newMaterial) { // save material first if new one exists if (this.newMaterial) { // save material first if new one exists
this.material.numbers = this.material.numbers.filter(e => e !== '');
this.api.post<MaterialModel>('/material/new', this.material.sendFormat(), data => { this.api.post<MaterialModel>('/material/new', this.material.sendFormat(), data => {
this.materials.push(data); // add material to data this.materials.push(data); // add material to data
this.material = data; this.material = data;
@ -268,7 +275,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
this.sample.notes.custom_fields[element[0]] = element[1]; this.sample.notes.custom_fields[element[0]] = element[1];
} }
}); });
this.sample.notes.sample_references = this.sampleReferences.filter(e => e[0] && e[1] && e[2]).map(e => ({sample_id: e[2], relation: e[1]})); this.sample.notes.sample_references = this.sampleReferences
.filter(e => e[0] && e[1] && e[2])
.map(e => ({sample_id: e[2], relation: e[1]}));
new Promise<SampleModel>(resolve => { new Promise<SampleModel>(resolve => {
if (this.new) { if (this.new) {
this.api.post<SampleModel>('/sample/new', this.sample.sendFormat(), resolve); this.api.post<SampleModel>('/sample/new', this.sample.sendFormat(), resolve);
@ -289,7 +298,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
this.api.post<MeasurementModel>('/measurement/new', measurement.sendFormat()); this.api.post<MeasurementModel>('/measurement/new', measurement.sendFormat());
} }
else { // update measurement else { // update measurement
this.api.put<MeasurementModel>('/measurement/' + measurement._id, measurement.sendFormat(['sample_id', 'measurement_template'])); this.api.put<MeasurementModel>('/measurement/' + measurement._id,
measurement.sendFormat(['sample_id', 'measurement_template']));
} }
} }
else if (measurement._id !== null) { // existing measurement was left empty to delete else if (measurement._id !== null) { // existing measurement was left empty to delete
@ -321,7 +331,6 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
} }
// TODO: rework later
setNewMaterial(value = null) { setNewMaterial(value = null) {
if (value === null) { if (value === null) {
this.newMaterial = !this.sample.material_id; this.newMaterial = !this.sample.material_id;
@ -333,24 +342,12 @@ export class SampleComponent implements OnInit, AfterContentChecked {
this.sampleForm.form.get('materialname').setValidators([Validators.required]); this.sampleForm.form.get('materialname').setValidators([Validators.required]);
} }
else { else {
this.sampleForm.form.get('materialname').setValidators([Validators.required, this.validation.generate('stringOf', [this.materialNames])]); this.sampleForm.form.get('materialname')
.setValidators([Validators.required, this.validation.generate('stringOf', [this.ac.materialName])]);
} }
this.sampleForm.form.get('materialname').updateValueAndValidity(); this.sampleForm.form.get('materialname').updateValueAndValidity();
} }
handleMaterialNumbers() {
const fieldNo = this.material.numbers.length;
const filledFields = this.material.numbers.filter(e => e !== '').length;
// append new field
if (filledFields === fieldNo) {
this.material.numbers.push('');
}
// remove if two end fields are empty
if (fieldNo > 1 && this.material.numbers[fieldNo - 1] === '' && this.material.numbers[fieldNo - 2] === '') {
this.material.numbers.pop();
}
}
selectCondition(id) { selectCondition(id) {
this.condition = this.conditionTemplates.find(e => e._id === id); this.condition = this.conditionTemplates.find(e => e._id === id);
console.log(this.condition); console.log(this.condition);
@ -372,7 +369,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
addMeasurement() { addMeasurement() {
this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id)); this.sample.measurements.push(new MeasurementModel(this.measurementTemplates.filter(e => e.name === 'spectrum').reverse()[0]._id));
this.sample.measurements[this.sample.measurements.length - 1].values.device = this.defaultDevice;
this.charts.push(_.cloneDeep(this.chartInit)); this.charts.push(_.cloneDeep(this.chartInit));
} }
@ -389,12 +387,22 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
fileToArray(files, mIndex, parameter) { fileToArray(files, mIndex, parameter) {
for (const i in files) {
if (files.hasOwnProperty(i)) {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = () => { fileReader.onload = () => {
this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(',')); let index: number = mIndex;
this.generateChart(this.sample.measurements[mIndex].values[parameter], mIndex); if (Number(i) > 0) { // append further spectra
this.addMeasurement();
index = this.sample.measurements.length - 1;
}
this.sample.measurements[index].values[parameter] =
fileReader.result.toString().split('\r\n').map(e => e.split(','));
this.generateChart(this.sample.measurements[index].values[parameter], index);
}; };
fileReader.readAsText(files[0]); fileReader.readAsText(files[i]);
}
}
} }
generateChart(spectrum, index) { generateChart(spectrum, index) {
@ -411,22 +419,17 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
} }
adjustCustomFields(value, index) { checkTypo(list, modal: TemplateRef<any>) {
this.customFields[index][0] = value; if (this.ac[list].indexOf(this.material[list]) < 0) { // entry is not in lise
const fieldNo = this.customFields.length; this.modalText.list = list;
let filledFields = 0; this.modalText.suggestion = this.ac[list] // find possible entry from list
this.customFields.forEach(field => { .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[list])}))
if (field[0] !== '') { .sort((a, b) => b.s - a.s)[0].v;
filledFields ++; this.modal.open(modal).then(result => {
if (result) { // use suggestion
this.material[list] = this.modalText.suggestion;
} }
}); });
// append new field
if (filledFields === fieldNo) {
this.customFields.push(['', '']);
}
// remove if two end fields are empty
if (fieldNo > 1 && this.customFields[fieldNo - 1][0] === '' && this.customFields[fieldNo - 2][0] === '') {
this.customFields.pop();
} }
} }
@ -457,7 +460,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
sampleReferenceList(value) { sampleReferenceList(value) {
return new Observable(observer => { return new Observable(observer => {
this.api.get<{_id: string, number: string}[]>('/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => { this.api.get<{_id: string, number: string}[]>(
'/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&' +
'filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => {
console.log(data); console.log(data);
this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number); this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number);
this.sampleReferenceFinds = data; this.sampleReferenceFinds = data;
@ -483,12 +488,14 @@ export class SampleComponent implements OnInit, AfterContentChecked {
} }
uniqueCfValues(index) { // returns all names until index for unique check uniqueCfValues(index) { // returns all names until index for unique check
return this.customFields.slice(0, index).map(e => e[0]); return this.customFields ? this.customFields.slice(0, index).map(e => e[0]) : [];
} }
} }
// 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante // 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der
// Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante
// 2. Der state "dirty" soll eigentlich anzeigen, wenn ein Form-Field vom User geändert wurde; damit missbrauchst du es hier etwas // 2. Der state "dirty" soll eigentlich anzeigen, wenn ein Form-Field vom User geändert wurde; damit missbrauchst du es hier etwas
// 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^ // 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich
// abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^

View File

@ -1,7 +1,8 @@
<script src="samples.component.ts"></script>
<div class="header-addnew"> <div class="header-addnew">
<h2>Samples</h2> <h2>Samples</h2>
<a routerLink="/samples/new"> <a routerLink="/samples/new">
<button class="rb-btn rb-primary"><span class="rb-ic rb-ic-add"></span>&nbsp; New sample</button> <rb-icon-button icon="add" mode="primary">New sample</rb-icon-button>
</a> </a>
</div> </div>
@ -11,14 +12,17 @@
<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" [disabled]="!filters.status.new" (ngModelChange)="loadSamples({firstPage: true})"> <rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated"
[disabled]="!filters.status.new" (ngModelChange)="loadSamples({firstPage: true})">
validated validated
</rb-form-checkbox> </rb-form-checkbox>
<rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" [disabled]="!filters.status.validated" (ngModelChange)="loadSamples({firstPage: true})"> <rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" [disabled]="!filters.status.validated"
(ngModelChange)="loadSamples({firstPage: true})">
new new
</rb-form-checkbox> </rb-form-checkbox>
</div> </div>
<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection" (ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection> <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection"
(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>
@ -28,15 +32,18 @@
<option value="500">500</option> <option value="500">500</option>
</rb-form-select> </rb-form-select>
<rb-form-multi-select name="fieldSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Fields" class="selection" (ngModelChange)="loadSamples({}, $event)"> <rb-form-multi-select name="fieldSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Fields"
class="selection" (ngModelChange)="loadSamples({}, $event)">
<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 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" (ngModelChange)="loadSamples({firstPage: true})"></rb-form-checkbox> <rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active"
<rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode" (ngModelChange)="updateFilterFields(filter.field)"> (ngModelChange)="loadSamples({firstPage: true})"></rb-form-checkbox>
<rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode"
(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>
@ -48,13 +55,26 @@
<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>
</rb-form-select> </rb-form-select>
<div class="filter-inputs"> <div class="filter-inputs">
<ng-container *ngFor="let ignore of [].constructor(filter.values.length); index as i"> <rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field"
<rb-form-date-input *ngIf="filter.field === 'added'; else noDate" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-date-input> [pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''"
<ng-template #noDate> (ngModelChange)="updateFilterFields(filter.field)">
<rb-form-input *ngIf="!filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-input> <ng-container *rbArrayInputItem="let item"
<rb-form-input *ngIf="filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)" ngModel></rb-form-input> [ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') +
</ng-template> (filter.field == 'added' ? 'date' : '')">
<rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i"
[label]="filter.label" [(ngModel)]="item.value"></rb-form-date-input>
<rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i"
[label]="filter.label" [(ngModel)]="item.value"></rb-form-input>
<rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field"
[name]="'filter-' + filter.field + item.i" [index]="item.i"
[label]="filter.label" [(ngModel)]="item.value"
[rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')"
[rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)"
ngModel></rb-form-input>
</ng-container> </ng-container>
</rb-array-input>
</div> </div>
</ng-container> </ng-container>
</div> </div>
@ -67,18 +87,20 @@
<ng-container *ngTemplateOutlet="paging"></ng-container> <ng-container *ngTemplateOutlet="paging"></ng-container>
<div class="download"> <div class="download">
<button class="rb-btn rb-secondary" type="button" [rbModal]="linkModal" ><span class="rb-ic rb-ic-download"></span> JSON download link <rb-icon-button icon="download" mode="secondary" [rbModal]="linkModal">JSON download link</rb-icon-button>
</button> <ng-template #linkModal>
<ng-template #linkModal let-close="close"> <label for="jsonUrl">URL for JSON download</label>
URL for JSON download: <textarea class="linkmodal" id="jsonUrl" #linkarea [value]="sampleUrl({export: true, host: true})"
<textarea class="linkmodal" #linkarea [value]="sampleUrl({export: true, host: true})" (keydown)="preventDefault($event)"></textarea> (keydown)="preventDefault($event)"></textarea>
<rb-form-checkbox name="download-csv" [(ngModel)]="downloadCsv"> <rb-form-checkbox name="download-csv" [(ngModel)]="downloadCsv">
add spectra add spectra
</rb-form-checkbox> </rb-form-checkbox>
<button class="rb-btn rb-secondary" type="button" (click)="clipboard()"><span class="rb-ic rb-ic-clipboard"></span> Copy to clipboard</button> <rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button>
</ng-template> </ng-template>
<a [href]="csvUrl" download="samples.csv"> <a [href]="csvUrl" download="samples.csv">
<button class="rb-btn rb-secondary" type="button" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})"><span class="rb-ic rb-ic-download"></span> Download result as CSV</button> <rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})">
Download result as CSV
</rb-icon-button>
</a> </a>
</div> </div>
@ -87,8 +109,14 @@
<th *ngFor="let key of activeKeys"> <th *ngFor="let key of activeKeys">
<div class="sort-header"> <div class="sort-header">
<span>{{key.label}}</span> <span>{{key.label}}</span>
<span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')"><span *ngIf="filters.sort === key.id + '-' + 'desc'"></span></span> <ng-container *ngIf="key.sortable">
<span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')"><span *ngIf="filters.sort === key.id + '-' + 'asc'"></span></span> <span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')">
<span *ngIf="filters.sort === key.id + '-' + 'desc'"></span>
</span>
<span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')">
<span *ngIf="filters.sort === key.id + '-' + 'asc'"></span>
</span>
</ng-container>
</div> </div>
</th> </th>
<th></th> <th></th>
@ -100,11 +128,13 @@
<td *ngIf="isActiveKey['material.name']">{{materials[sample.material_id].name}}</td> <td *ngIf="isActiveKey['material.name']">{{materials[sample.material_id].name}}</td>
<td *ngIf="isActiveKey['material.supplier']">{{materials[sample.material_id].supplier}}</td> <td *ngIf="isActiveKey['material.supplier']">{{materials[sample.material_id].supplier}}</td>
<td *ngIf="isActiveKey['material.group']">{{materials[sample.material_id].group}}</td> <td *ngIf="isActiveKey['material.group']">{{materials[sample.material_id].group}}</td>
<td *ngFor="let key of activeTemplateKeys.material">{{materials[sample.material_id].properties[key[2]] | exists}}</td> <td *ngFor="let key of activeTemplateKeys.material">
{{materials[sample.material_id].properties[key[2]] | exists}}
</td>
<td *ngIf="isActiveKey['type']">{{sample.type}}</td> <td *ngIf="isActiveKey['type']">{{sample.type}}</td>
<td *ngIf="isActiveKey['color']">{{sample.color}}</td> <td *ngIf="isActiveKey['color']">{{sample.color}}</td>
<td *ngIf="isActiveKey['batch']">{{sample.batch}}</td> <td *ngIf="isActiveKey['batch']">{{sample.batch}}</td>
<td *ngIf="isActiveKey['notes']">{{sample.notes | object}}</td> <td *ngIf="isActiveKey['notes']">{{sample.notes | object: ['_id', 'sample_references']}}</td>
<td *ngFor="let key of activeTemplateKeys.measurements">{{sample[key[1]] | exists: key[2]}}</td> <td *ngFor="let key of activeTemplateKeys.measurements">{{sample[key[1]] | exists: key[2]}}</td>
<td *ngIf="isActiveKey['added']">{{sample.added | date:'dd/MM/yy'}}</td> <td *ngIf="isActiveKey['added']">{{sample.added | date:'dd/MM/yy'}}</td>
<td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td> <td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td>
@ -118,7 +148,8 @@
<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" (change)="loadPage({toPage: $event.target.value - page})" [ngModel]="page"></rb-form-input> <rb-form-input label="page" (change)="loadPage({toPage: $event.target.value - page})" [ngModel]="page">
</rb-form-input>
<span> <span>
of {{pages}} ({{totalSamples}} samples) of {{pages}} ({{totalSamples}} samples)
</span> </span>

View File

@ -178,5 +178,5 @@ textarea.linkmodal {
.filter-inputs > * { .filter-inputs > * {
display: inline-block; display: inline-block;
max-width: 250px; width: 220px;
} }

View File

@ -2,7 +2,7 @@
// //
// import { SamplesComponent } from './samples.component'; // import { SamplesComponent } from './samples.component';
// //
// // TODO // // TODO: tests
// //
// describe('SamplesComponent', () => { // describe('SamplesComponent', () => {
// let component: SamplesComponent; // let component: SamplesComponent;

View File

@ -2,6 +2,7 @@ import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core
import {ApiService} from '../services/api.service'; import {ApiService} from '../services/api.service';
import {AutocompleteService} from '../services/autocomplete.service'; import {AutocompleteService} from '../services/autocomplete.service';
import _ from 'lodash'; import _ from 'lodash';
import {SampleModel} from '../models/sample.model';
interface LoadSamplesOptions { interface LoadSamplesOptions {
@ -13,6 +14,7 @@ interface KeyInterface {
id: string; id: string;
label: string; label: string;
active: boolean; active: boolean;
sortable: boolean;
} }
@Component({ @Component({
@ -21,9 +23,8 @@ interface KeyInterface {
styleUrls: ['./samples.component.scss'] styleUrls: ['./samples.component.scss']
}) })
// TODO: manage branches, introduce versioning, only upload ui from master
// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
export class SamplesComponent implements OnInit { export class SamplesComponent implements OnInit {
@ -32,7 +33,7 @@ export class SamplesComponent implements OnInit {
downloadCsv = false; downloadCsv = false;
materials = {}; materials = {};
samples = []; samples: SampleModel[] = [];
totalSamples = 0; // total number of samples totalSamples = 0; // total number of samples
csvUrl = ''; // store url separate so it only has to be generated when clicking the download button csvUrl = ''; // store url separate so it only has to be generated when clicking the download button
filters = { filters = {
@ -53,7 +54,7 @@ export class SamplesComponent implements OnInit {
{field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']},
{field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']},
{field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']},
{field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]} {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']}
] ]
}; };
page = 1; page = 1;
@ -61,16 +62,16 @@ export class SamplesComponent implements OnInit {
loadSamplesQueue = []; // arguments of queued up loadSamples() calls loadSamplesQueue = []; // arguments of queued up loadSamples() calls
apiKey = ''; apiKey = '';
keys: KeyInterface[] = [ keys: KeyInterface[] = [
{id: 'number', label: 'Number', active: true}, {id: 'number', label: 'Number', active: true, sortable: true},
{id: 'material.numbers', label: 'Material numbers', active: true}, {id: 'material.numbers', label: 'Material numbers', active: true, sortable: false},
{id: 'material.name', label: 'Material name', active: true}, {id: 'material.name', label: 'Material name', active: true, sortable: true},
{id: 'material.supplier', label: 'Supplier', active: true}, {id: 'material.supplier', label: 'Supplier', active: true, sortable: true},
{id: 'material.group', label: 'Material', active: false}, {id: 'material.group', label: 'Material', active: false, sortable: true},
{id: 'type', label: 'Type', active: true}, {id: 'type', label: 'Type', active: true, sortable: true},
{id: 'color', label: 'Color', active: true}, {id: 'color', label: 'Color', active: true, sortable: true},
{id: 'batch', label: 'Batch', active: true}, {id: 'batch', label: 'Batch', active: true, sortable: true},
{id: 'notes', label: 'Notes', active: false}, {id: 'notes', label: 'Notes', active: false, sortable: false},
{id: 'added', label: 'Added', active: true} {id: 'added', label: 'Added', active: true, sortable: true},
]; ];
isActiveKey: {[key: string]: boolean} = {}; isActiveKey: {[key: string]: boolean} = {};
activeKeys: KeyInterface[] = []; activeKeys: KeyInterface[] = [];
@ -91,7 +92,8 @@ export class SamplesComponent implements OnInit {
this.materials[material._id] = material; this.materials[material._id] = material;
}); });
this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name); this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name);
this.filters.filters.find(e => e.field === 'color').autocomplete = [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))]; this.filters.filters.find(e => e.field === 'color').autocomplete =
[...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))];
this.loadSamples(); this.loadSamples();
}); });
this.api.get('/user/key', (data: {key: string}) => { this.api.get('/user/key', (data: {key: string}) => {
@ -112,8 +114,24 @@ export class SamplesComponent implements OnInit {
const templateKeys = []; const templateKeys = [];
data.forEach(item => { data.forEach(item => {
item.parameters.forEach(parameter => { item.parameters.forEach(parameter => {
templateKeys.push({id: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false}); const parameterName = encodeURIComponent(parameter.name);
this.filters.filters.push({field: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']}); // exclude spectrum
if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) {
templateKeys.push({
id: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`,
label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`,
active: false,
sortable: true
});
this.filters.filters.push({
field: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`,
label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`,
active: false,
autocomplete: [],
mode: 'eq',
values: ['']
});
}
}); });
}); });
this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys); this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys);
@ -152,10 +170,22 @@ export class SamplesComponent implements OnInit {
}); });
} }
sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples sampleUrl(options: {
paging?: boolean,
pagingOptions?: {
firstPage?: boolean,
toPage?: number,
event?: Event
},
csv?: boolean,
export?: boolean,
host?: boolean
}) { // return url to fetch samples
const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false
const query: string[] = []; const query: string[] = [];
query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))); query.push(
'status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))
);
if (options.paging) { if (options.paging) {
if (this.samples[0]) { // do not include from-id when page size was changed if (this.samples[0]) { // do not include from-id when page size was changed
if (!options.pagingOptions.firstPage) { if (!options.pagingOptions.firstPage) {
@ -178,11 +208,11 @@ export class SamplesComponent implements OnInit {
query.push('key=' + this.apiKey); query.push('key=' + this.apiKey);
} }
this.keys.forEach(key => { this.keys.forEach(key => {
if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { // do not load material properties for table // do not load material properties for table
if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) {
query.push('fields[]=' + key.id); query.push('fields[]=' + key.id);
} }
}); });
console.log(this.filters.filters);
query.push(..._.cloneDeep(this.filters.filters) query.push(..._.cloneDeep(this.filters.filters)
.map(e => { .map(e => {
@ -195,7 +225,6 @@ export class SamplesComponent implements OnInit {
.filter(e => e.active && e.values.length > 0) .filter(e => e.active && e.values.length > 0)
.map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values'])))) .map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values']))))
); );
console.log(this.filters);
if (!options.export) { if (!options.export) {
additionalTableKeys.forEach(key => { additionalTableKeys.forEach(key => {
if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added
@ -206,8 +235,9 @@ export class SamplesComponent implements OnInit {
else if (this.downloadCsv) { else if (this.downloadCsv) {
query.push('fields[]=measurements.spectrum.dpt'); query.push('fields[]=measurements.spectrum.dpt');
} }
console.log('/samples?' + query.join('&')); return (options.host && isDevMode() ? window.location.host : '') +
return (options.host && isDevMode() ? window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&'); (options.export ? this.api.hostName : '') +
'/samples?' + query.join('&');
} }
loadPage(delta) { loadPage(delta) {
@ -220,17 +250,7 @@ export class SamplesComponent implements OnInit {
updateFilterFields(field) { updateFilterFields(field) {
const filter = this.filters.filters.find(e => e.field === field); const filter = this.filters.filters.find(e => e.field === field);
if (filter.mode === 'in' || filter.mode === 'nin') { filter.active = true;
if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') {
filter.values.pop();
}
else if (filter.values[filter.values.length - 1] !== '') {
filter.values.push((filter.field === 'added' ? new Date() : '') as string & Date);
}
}
else {
filter.values = [filter.values[0] as string & Date];
}
if (filter.active) { if (filter.active) {
this.loadSamples({firstPage: true}); this.loadSamples({firstPage: true});
} }
@ -243,10 +263,13 @@ export class SamplesComponent implements OnInit {
updateActiveKeys() { // array with all activeKeys updateActiveKeys() { // array with all activeKeys
this.activeKeys = this.keys.filter(e => e.active); this.activeKeys = this.keys.filter(e => e.active);
this.activeTemplateKeys.material = this.keys.filter(e => e.id.indexOf('material.properties.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); this.activeTemplateKeys.material = this.keys
this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); .filter(e => e.id.indexOf('material.properties.') >= 0 && e.active)
console.log(this.activeTemplateKeys); .map(e => e.id.split('.')
console.log(this.keys); // TODO: glass fiber filter not working .map(el => decodeURIComponent(el)));
this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active)
.map(e => e.id.split('.')
.map(el => decodeURIComponent(el))); // TODO: glass fiber filter not working
} }
calcFieldSelectKeys() { calcFieldSelectKeys() {

View File

@ -9,7 +9,20 @@ import {Observable} from 'rxjs';
}) })
export class LoginService implements CanActivate { export class LoginService implements CanActivate {
private pathPermissions = [
{path: 'templates', permission: 'maintain'},
{path: 'users', permission: 'admin'}
];
readonly levels = [
'read',
'write',
'maintain',
'dev',
'admin'
];
private loggedIn; private loggedIn;
private level;
constructor( constructor(
private api: ApiService, private api: ApiService,
@ -20,13 +33,30 @@ export class LoginService implements CanActivate {
login(username = '', password = '') { login(username = '', password = '') {
return new Promise(resolve => { return new Promise(resolve => {
if (username !== '') { if (username !== '' || password !== '') { // some credentials given
let credentials: string[];
const credentialString: string = this.storage.get('basicAuth');
if (credentialString) { // found stored credentials
credentials = atob(credentialString).split(':');
}
else {
credentials = ['', ''];
}
if (username !== '' && password !== '') { // all credentials given
this.storage.set('basicAuth', btoa(username + ':' + password)); this.storage.set('basicAuth', btoa(username + ':' + password));
} }
else if (username !== '') { // username given
this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
}
else if (password !== '') { // password given
this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
}
}
this.api.get('/authorized', (data: any, error) => { 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.level = data.level;
resolve(true); resolve(true);
} else { } else {
this.loggedIn = false; this.loggedIn = false;
@ -49,6 +79,8 @@ export class LoginService implements CanActivate {
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 => {
const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path
if (this.loggedIn === undefined) { if (this.loggedIn === undefined) {
this.login().then(res => { this.login().then(res => {
observer.next(res as any); observer.next(res as any);
@ -59,6 +91,11 @@ export class LoginService implements CanActivate {
observer.next(this.loggedIn); observer.next(this.loggedIn);
observer.complete(); observer.complete();
} }
}
else {
observer.next(false);
observer.complete();
}
}); });
} }
@ -66,6 +103,10 @@ export class LoginService implements CanActivate {
return this.loggedIn; return this.loggedIn;
} }
is(level) {
return this.levels.indexOf(this.level) >= this.levels.indexOf(level);
}
get username() { get username() {
return atob(this.storage.get('basicAuth')).split(':')[0]; return atob(this.storage.get('basicAuth')).split(':')[0];
} }

View File

@ -66,10 +66,16 @@ export class ValidationService {
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
string(data) { string(data, option = null) {
const {ignore, error} = Joi.string().max(128).allow('').validate(data); let validator = Joi.string().max(128).allow('');
let errorMsg = 'must contain max 128 characters';
if (option === 'alphanum') {
validator = validator.alphanum();
errorMsg = 'must contain max 128 alphanumerical characters';
}
const {ignore, error} = validator.validate(data);
if (error) { if (error) {
return {ok: false, error: 'must contain max 128 characters'}; return {ok: false, error: errorMsg};
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
@ -121,4 +127,60 @@ export class ValidationService {
} }
return {ok: true, error: ''}; return {ok: true, error: ''};
} }
equal(data, compare) {
if (data !== compare) {
return {ok: false, error: `must be equal`};
}
return {ok: true, error: ''};
}
parameterName(data) {
const {ignore, error} = Joi.string()
.max(128)
.invalid('condition_template', 'material_template')
.pattern(/^[^.]+$/)
.required()
.messages({'string.pattern.base': 'name must not contain a dot'})
.validate(data);
if (error) {
return {ok: false, error: error.details[0].message};
}
return {ok: true, error: ''};
}
parameterRange(data) {
if (data) {
try {
const {ignore, error} = Joi.object({
values: Joi.array()
.min(1),
min: Joi.number(),
max: Joi.number(),
type: Joi.string()
.valid('array')
})
.oxor('values', 'min')
.oxor('values', 'max')
.oxor('type', 'values')
.oxor('type', 'min')
.oxor('type', 'max')
.required()
.validate(JSON.parse(data));
if (error) {
return {ok: false, error: error.details[0].message};
}
}
catch (e) {
return {ok: false, error: `no valid JSON`};
}
return {ok: true, error: ''};
}
else {
return {ok: false, error: `no valid value`};
}
}
} }

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SettingsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,68 @@
import { Component, OnInit } from '@angular/core';
import {ApiService} from '../services/api.service';
import {UserModel} from '../models/user.model';
import {Router} from '@angular/router';
import {LoginService} from '../services/login.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
user: UserModel = new UserModel();
password = '';
messageUser = '';
messagePass = '';
constructor(
private api: ApiService,
private login: LoginService,
private router: Router
) { }
ngOnInit(): void {
this.api.get<UserModel>('/user', data => {
this.user.deserialize(data);
});
}
saveUser() {
this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => {
if (err) {
this.messageUser = err.error.status;
}
else {
this.login.login(data.name).then(res => {
if (res) {
this.router.navigate(['/samples']);
}
else {
this.messageUser = 'request not successful, try again';
}
});
}
});
}
savePass() {
this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => {
if (err) {
this.messagePass = err.error.status;
}
else {
this.login.login('', this.password).then(res => {
if (res) {
this.router.navigate(['/samples']);
}
else {
this.messagePass = 'request not successful, try again';
}
});
}
});
}
}

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TemplatesComponent } from './templates.component';
describe('TemplatesComponent', () => {
let component: TemplatesComponent;
let fixture: ComponentFixture<TemplatesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TemplatesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TemplatesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,124 @@
import { Component, OnInit } from '@angular/core';
import {ApiService} from '../services/api.service';
import {TemplateModel} from '../models/template.model';
import {animate, style, transition, trigger} from '@angular/animations';
import {ValidationService} from '../services/validation.service';
import _ from 'lodash';
@Component({
selector: 'app-templates',
templateUrl: './templates.component.html',
styleUrls: ['./templates.component.scss'],
animations: [
trigger(
'inOut', [
transition(':enter', [
style({height: 0, opacity: 0}),
animate('0.5s ease-out', style({height: '*', opacity: 1}))
]),
transition(':leave', [
style({height: '*', opacity: 1}),
animate('0.5s ease-in', style({height: 0, opacity: 0}))
])
]
)
]
})
export class TemplatesComponent implements OnInit {
collection = 'measurement';
templates: TemplateModel[] = [];
templateGroups: {[first_id: string]: TemplateModel[]} = {}; // templates grouped by first_id
templateEdit: {[first_id: string]: TemplateModel} = {}; // latest template of each first_id for editing
groupsView: {first_id: string, name: string, version: number, expanded: boolean, edit: boolean, entries: TemplateModel[]}[] = [];
arr = ['testA', 'testB', 'testC'];
constructor(
private api: ApiService,
private validate: ValidationService
) { }
ngOnInit(): void {
this.loadTemplates();
}
loadTemplates() {
this.api.get<TemplateModel[]>(`/template/${this.collection}s`, data => {
this.templates = data;
this.templateFormat();
});
}
templateFormat() {
this.templateGroups = {};
this.templateEdit = {};
this.templates.forEach(template => {
if (this.templateGroups[template.first_id]) {
this.templateGroups[template.first_id].push(template);
}
else {
this.templateGroups[template.first_id] = [template];
}
});
Object.keys(this.templateGroups).forEach(id => {
this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version);
this.templateEdit[id] = _.cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
this.templateEdit[id].parameters = this.templateEdit[id].parameters.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
});
this.groupsView = Object.values(this.templateGroups)
.map(e => ({
first_id: e[e.length - 1].first_id,
name: e[e.length - 1].name,
version: e[e.length - 1].version,
expanded: false,
edit: false,
entries: e
}));
}
saveTemplate(first_id) {
const template = _.cloneDeep(this.templateEdit[first_id]);
template.parameters = template.parameters.filter(e => e.name !== '');
let valid = true;
valid = valid && this.validate.string(template.name).ok;
template.parameters.forEach(parameter => {
valid = valid && this.validate.parameterName(parameter.name).ok;
valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
if (valid) {
parameter.range = JSON.parse(parameter.rangeString);
}
});
if (valid) {
console.log('valid', template);
const sendData = {name: template.name, parameters: template.parameters.map(e => _.omit(e, ['rangeString']))};
if (first_id === 'null') {
this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, data => {
if (data.version > template.version) { // there were actual changes and a new version was created
this.templates.push(data);
}
this.templateFormat();
});
}
else {
this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, data => {
if (data.version > template.version) { // there were actual changes and a new version was created
this.templates.push(data);
}
this.templateFormat();
});
}
}
else {
console.log('not valid');
}
}
newTemplate() {
if (!this.templateEdit.null) {
const template = new TemplateModel();
template.name = 'new template';
this.groupsView.push({first_id: 'null', name: 'new template', version: 0, expanded: true, edit: true, entries: [template]});
this.templateEdit.null = new TemplateModel();
}
}
}

View File

@ -0,0 +1,94 @@
<h2>Users</h2>
<rb-icon-button icon="add" mode="primary" (click)="addNewUser()">New user</rb-icon-button>
<form *ngIf="newUser" #userForm="ngForm">
<rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="newUser.name"
#nameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
<rb-form-input name="email" label="email" email required [(ngModel)]="newUser.email" ngModel>
<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
<rb-form-select name="level" label="level" required [(ngModel)]="newUser.level">
<option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-select>
<rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
[(ngModel)]="newUser.location" #locationInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
<rb-form-input name="device" label="device" appValidate="string" [(ngModel)]="newUser.device_name"
#deviceInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
</rb-form-input>
<rb-form-input name="passA" type="password" label="new password" appValidate="password" required
[(ngModel)]="newUserPass" #passAInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
</rb-form-input>
<rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
[appValidateArgs]="[newUserPass]" required #passBInput="ngModel" ngModel>
<ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
</rb-form-input>
<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveNewUser()">
Save user
</rb-icon-button>
</form>
<rb-table>
<tr>
<th>Name</th>
<th>Email</th>
<th>Level</th>
<th>Location</th>
<th>Device</th>
<th></th>
</tr>
<tr *ngFor="let user of users">
<ng-container *ngIf="!user.edit; else editUser">
<td>{{user.name}}</td>
<td>{{user.email}}</td>
<td>{{user.level}}</td>
<td>{{user.location}}</td>
<td>{{user.device_name}}</td>
<td><span class="rb-ic rb-ic-edit clickable" (click)="user.edit = true"></span></td>
</ng-container>
<ng-template #editUser>
<td>
<rb-form-input [name]="'name-' + user.name" appValidate="username" required [(ngModel)]="user.name" #nameInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
</td>
<td>
<rb-form-input [name]="'email-' + user.name" email required [(ngModel)]="user.email" #emailInput="ngModel">
<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
</td>
<td>
<rb-form-select [name]="'level-' + user.name" [(ngModel)]="user.level">
<option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
</rb-form-select>
</td>
<td>
<rb-form-input [name]="'location-' + user.name" appValidate="string" required [appValidateArgs]="['alphanum']"
[(ngModel)]="user.location" #locationInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
</rb-form-input>
</td>
<td>
<rb-form-input [name]="'device-' + user.name" appValidate="string" [(ngModel)]="user.device_name"
#deviceInput="ngModel">
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
</rb-form-input>
</td>
<td><rb-icon-button icon="save" mode="primary" (click)="saveUser(user)" [disabled]="nameInput.invalid || emailInput.invalid || locationInput.invalid || deviceInput.invalid">Save</rb-icon-button></td>
</ng-template>
</tr>
</rb-table>

View File

@ -0,0 +1,3 @@
::ng-deep td .error-messages {
position: absolute;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UsersComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Component, OnInit } from '@angular/core';
import {ApiService} from '../services/api.service';
import {UserModel} from '../models/user.model';
import {LoginService} from '../services/login.service';
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss']
})
export class UsersComponent implements OnInit {
users: UserModel[] = [];
newUser: UserModel | null = null;
newUserPass = '';
constructor(
private api: ApiService,
public login: LoginService
) { }
ngOnInit(): void {
this.api.get<UserModel[]>('/users', data => {
this.users = data.map(e => new UserModel().deserialize(e));
});
}
saveUser(user: UserModel) {
this.api.put<UserModel>('/user/' + user.origName, user.sendFormat('admin'), data => {
user.deserialize(data);
user.edit = false;
});
}
saveNewUser() {
this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => {
this.newUser = null;
this.users.push(new UserModel().deserialize(data));
});
}
addNewUser() {
this.newUser = this.newUser ? null : new UserModel();
}
}

View File

@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="300" width="720" version="1.1" y="0" x="0" viewBox="0 0 720 300">
<style type="text/css">
.st0 {
fill: url("#SVGID_1_");
}
.st1 {
fill: url("#SVGID_2_");
}
.st2 {
fill: url("#SVGID_3_");
}
.st3 {
fill: url("#SVGID_4_");
}
.st4 {
fill: url("#SVGID_5_");
}
.st5 {
fill: #AF2024;
}
.st6 {
fill: url("#SVGID_6_");
}
.st7 {
fill: #941B1E;
}
.st8 {
fill: #B12739;
}
.st9 {
fill: #952432;
}
.st10 {
fill: #D42027;
}
.st11 {
fill: url("#SVGID_7_");
}
.st12 {
fill: url("#SVGID_8_");
}
.st13 {
fill: #1C9A48;
}
.st14 {
fill: url("#SVGID_9_");
}
.st15 {
fill: url("#SVGID_10_");
}
.st16 {
fill: #2A3886;
}
.st17 {
fill: url("#SVGID_11_");
}
.st18 {
fill: url("#SVGID_12_");
}
.st19 {
fill: url("#SVGID_13_");
}
.st20 {
fill: url("#SVGID_14_");
}
</style>
<g transform="translate(-1.55,-3.3)">
<linearGradient id="SVGID_1_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="842.08" x1="118.98"><stop stop-color="#952331" offset="0"/><stop stop-color="#921C1D" offset=".036094"/><stop stop-color="#B02739" offset=".084649"/><stop stop-color="#AD1F24" offset=".1237"/><stop stop-color="#C72026" offset=".1509"/><stop stop-color="#D42027" offset=".1697"/><stop stop-color="#CC2431" offset=".1758"/><stop stop-color="#B72B4C" offset=".1888"/><stop stop-color="#953371" offset=".2074"/><stop stop-color="#88357F" offset=".2142"/><stop stop-color="#853681" offset=".2436"/><stop stop-color="#6F368B" offset=".2638"/><stop stop-color="#39428F" offset=".2911"/><stop stop-color="#233D7D" offset=".3242"/><stop stop-color="#322C6F" offset=".4181"/><stop stop-color="#2A3885" offset=".494"/><stop stop-color="#1D62A1" offset=".5581"/><stop stop-color="#276CA5" offset=".5702"/><stop stop-color="#438EB3" offset=".6103"/><stop stop-color="#55A5BC" offset=".6399"/><stop stop-color="#5CAFBF" offset=".6556"/><stop stop-color="#56ABBD" offset=".6777"/><stop stop-color="#439FB8" offset=".7058"/><stop stop-color="#188EAF" offset=".7372"/><stop stop-color="#038BAE" offset=".7426"/><stop stop-color="#069292" offset=".7898"/><stop stop-color="#05A14B" offset=".8875"/><stop stop-color="#03927E" offset="1"/></linearGradient><rect width="723.1" y="0" x="0" height="306.4" class="st0" fill="url(#SVGID_1_)"/>
<linearGradient id="SVGID_2_" y2="-109.26" gradientUnits="userSpaceOnUse" y1="-109.26" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="235.98" x1="325.08"><stop stop-color="#893680" offset="0"/><stop stop-color="#893680" offset=".3354"/><stop stop-color="#8D316D" offset=".5025"/><stop stop-color="#90294D" offset=".8398"/><stop stop-color="#902541" offset="1"/></linearGradient><polygon points="175.1 153.2 117 306.4 206.1 306.4" fill="url(#SVGID_2_)" class="st1"/>
<linearGradient id="SVGID_3_" y2="-82.284" gradientUnits="userSpaceOnUse" y1="120.24" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="446.55" x1="478.93"><stop stop-color="#322C6F" offset="0"/><stop stop-color="#322C6F" offset=".2427"/><stop stop-color="#302F72" offset=".4599"/><stop stop-color="#2A3A7E" offset=".7155"/><stop stop-color="#154A93" offset=".9896"/><stop stop-color="#134B94" offset="1"/></linearGradient><polygon points="288.4 153.2 310.7 306.4 358.1 306.4 358.1 0 312.9 0" fill="url(#SVGID_3_)" class="st2"/>
<linearGradient id="SVGID_4_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="372.88" x1="294.08"><stop stop-color="#6F378D" offset="0"/><stop stop-color="#3A4291" offset="1"/></linearGradient><polygon points="175.1 153.2 206.1 306.4 253.9 153.2 209.4 0 209.4 0" fill="url(#SVGID_4_)" class="st3"/>
<linearGradient id="SVGID_5_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="325.08" x1="431.88"><stop stop-color="#233D7D" offset="0"/><stop stop-color="#293D7D" offset=".2495"/><stop stop-color="#3A3C80" offset=".5446"/><stop stop-color="#513B84" offset=".8616"/><stop stop-color="#5D3A86" offset="1"/></linearGradient><polygon points="253.9 153.2 206.1 306.4 310.7 306.4 288.4 153.2 312.9 0 209.4 0" fill="url(#SVGID_5_)" class="st4"/><polygon points="116.1 0 55.7 0 55.7 94.8 89.9 153.2 55.7 211.6 55.7 306.4 117 306.4 95.2 153.2" fill="#af2024" class="st5"/>
<linearGradient id="SVGID_6_" y2="43.937" gradientUnits="userSpaceOnUse" y1="43.937" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="232.67" x1="329.11"><stop stop-color="#893680" offset="0"/><stop stop-color="#893680" offset=".3354"/><stop stop-color="#8D316D" offset=".5025"/><stop stop-color="#90294D" offset=".8398"/><stop stop-color="#902541" offset="1"/></linearGradient><polygon points="175.1 153.2 209.4 0 116.1 0" fill="url(#SVGID_6_)" class="st6"/><polygon points="55.7 94.8 55.7 0 0 0" fill="#941b1e" class="st7"/><polygon points="55.7 211.6 89.9 153.2 55.7 94.8" fill="#b12739" class="st8"/><polygon points="55.7 211.6 0 306.4 55.7 306.4" fill="#941b1e" class="st7"/><polygon points="55.7 94.8 0 0 0 306.4 55.7 211.6" fill="#952432" class="st9"/><polygon points="116.1 0 95.2 153.2 117 306.4 175.1 153.2" fill="#d42027" class="st10"/>
<linearGradient id="SVGID_7_" y2="-186.06" gradientUnits="userSpaceOnUse" y1="120.44" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="748.96" x1="748.96"><stop stop-color="#94BE55" offset="0"/><stop stop-color="#93BD58" offset=".044340"/><stop stop-color="#8BBC6A" offset=".3891"/><stop stop-color="#86BC75" offset=".7149"/><stop stop-color="#84BC79" offset="1"/></linearGradient><path d="m641.6 259.6c1.7-25.4 10-54.6 18.8-85.6 1.4-5 2.8-10 4.2-15.1-1.4-5.5-2.8-10.9-4.2-16.2-8.8-33.3-17-64.7-18.8-92-1.4-21.2 1.4-37 8.9-50.6h-45.9c-7.5 18.3-10.3 29.1-8.9 50.3 1.7 27.3 10 58.7 18.8 92 13 49.3 28 106.2 23.2 164.2h12.9c-7.6-12.8-10.4-27.3-9-47z" class="st11" fill="url(#SVGID_7_)"/>
<linearGradient id="SVGID_8_" y2="-184.45" gradientUnits="userSpaceOnUse" y1="117.29" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="733.49" x1="653.76"><stop stop-color="#08A24B" offset="0"/><stop stop-color="#0AA14E" offset=".1678"/><stop stop-color="#0B9E57" offset=".4047"/><stop stop-color="#099A67" offset=".6827"/><stop stop-color="#04947D" offset=".9898"/><stop stop-color="#04937E" offset="1"/></linearGradient><path d="m614.5 142.3c-8.8-33.3-17-64.7-18.8-92-1.4-21.2 1.4-32 8.9-50.3h-35.4c5.7 53.9-3.8 106.7-13.6 166.8-5.7 35-11.7 71.3-13.2 100.6-1.1 21.1 0.4 32.8 1.8 39h93.5c4.8-57.9-10.3-114.8-23.2-164.1z" class="st12" fill="url(#SVGID_8_)"/><path class="st13" fill="#1c9a48" d="m664.6 158.9c-1.4 5.1-2.8 10.1-4.2 15.1-8.8 31-17 60.2-18.8 85.6-1.4 19.7 1.4 34.2 9 46.9h33c4.2-51.8-7.2-102.3-19-147.6z"/>
<linearGradient id="SVGID_9_" y2="-185.96" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="812.83" x1="812.83"><stop stop-color="#69A060" offset="0"/><stop stop-color="#639D5C" offset=".039895"/><stop stop-color="#4C944F" offset=".2192"/><stop stop-color="#378E47" offset=".4184"/><stop stop-color="#298B44" offset=".6515"/><stop stop-color="#238A43" offset="1"/></linearGradient><path d="m680.5 0c10.7 55.3-2.5 110.4-15.9 158.9 11.7 45.3 23.2 95.8 18.9 147.6h39.6v-306.5h-42.6z" class="st14" fill="url(#SVGID_9_)"/>
<linearGradient id="SVGID_10_" y2="-185.86" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="652.45" x1="652.45"><stop stop-color="#05B5DC" offset="0"/><stop stop-color="#04B0D7" offset=".2197"/><stop stop-color="#05A4C9" offset=".5371"/><stop stop-color="#0591B4" offset=".9122"/><stop stop-color="#058CAE" offset="1"/></linearGradient><path d="m542.3 267.4c1.5-29.4 7.5-65.6 13.2-100.6 9.8-60.1 19.3-112.8 13.6-166.8h-70.8c-1.4 11.4-2.9 19.2-1.8 41.8 1.5 31.6 7.5 70.5 13.2 108.2 8.4 55.4 16.6 108.8 15.1 156.4h19.2c-1.3-6.2-2.8-17.9-1.7-39z" class="st15" fill="url(#SVGID_10_)"/><polygon points="375.7 153.2 358.1 0 358.1 306.4" fill="#2a3886" class="st16"/>
<linearGradient id="SVGID_11_" y2="77.136" gradientUnits="userSpaceOnUse" y1="-4.3281" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="796.71" x1="751.05"><stop stop-color="#62B16E" offset="0"/><stop stop-color="#87B957" offset="1"/></linearGradient><path d="m641.6 50.6c1.7 27.3 10 58.7 18.8 92 1.4 5.3 2.8 10.7 4.2 16.2 13.5-48.4 26.6-103.5 15.9-158.8h-30c-7.5 13.6-10.3 29.4-8.9 50.6z" class="st17" fill="url(#SVGID_11_)"/>
<linearGradient id="SVGID_12_" y2="-189.28" gradientUnits="userSpaceOnUse" y1="113.71" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="631.59" x1="550.4"><stop stop-color="#069AD4" offset="0"/><stop stop-color="#30A0CE" offset=".3525"/><stop stop-color="#5BB0C0" offset="1"/></linearGradient><path d="m509.8 150c-5.7-37.7-11.7-76.6-13.2-108.2-1.1-22.7 0.4-30.4 1.8-41.8h-41.5c1.5 40.1-1.5 85.3-7 160.8-3.1 43.5-8 110.5-7 145.7h82.1c1.4-47.7-6.8-101.1-15.2-156.5z" class="st18" fill="url(#SVGID_12_)"/>
<linearGradient id="SVGID_13_" y2="-185.86" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="505.33" x1="505.33"><stop stop-color="#1E458E" offset="0"/><stop stop-color="#1F4F96" offset=".2411"/><stop stop-color="#2B6AAB" offset=".7292"/><stop stop-color="#337BB9" offset="1"/></linearGradient><polygon points="358.1 306.4 414.6 306.4 414.6 0 358.1 0 375.7 153.2" fill="url(#SVGID_13_)" class="st19"/>
<linearGradient id="SVGID_14_" y2="120.54" gradientUnits="userSpaceOnUse" y1="-185.86" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="554.92" x1="554.92"><stop stop-color="#3F9AC9" offset="0"/><stop stop-color="#2062A2" offset="1"/></linearGradient><path d="m449.9 160.8c5.5-75.5 8.5-120.6 7-160.8h-42.2l-0.1 306.4h28.3c-1-35.1 3.8-102.1 7-145.6z" class="st20" fill="url(#SVGID_14_)"/></g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -15,3 +15,11 @@ a, a:active, a:focus {
button::-moz-focus-inner { button::-moz-focus-inner {
border: 0; border: 0;
} }
.supergraphic {
background-image: url("assets/imgs/supergraphic.svg");
}
.clickable {
cursor: pointer;
}