-
-
+
+
=
≠
<
@@ -48,13 +55,26 @@
∉
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -67,18 +87,20 @@
@@ -87,8 +109,14 @@
@@ -100,11 +128,13 @@
{{materials[sample.material_id].name}}
{{materials[sample.material_id].supplier}}
{{materials[sample.material_id].group}}
-
{{materials[sample.material_id].properties[key[2]] | exists}}
+
+ {{materials[sample.material_id].properties[key[2]] | exists}}
+
{{sample.type}}
{{sample.color}}
{{sample.batch}}
-
{{sample.notes | object}}
+
{{sample.notes | object: ['_id', 'sample_references']}}
{{sample[key[1]] | exists: key[2]}}
{{sample.added | date:'dd/MM/yy'}}
@@ -118,7 +148,8 @@
-
+
+
of {{pages}} ({{totalSamples}} samples)
diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss
index 55d78fd..7c2cc64 100644
--- a/src/app/samples/samples.component.scss
+++ b/src/app/samples/samples.component.scss
@@ -178,5 +178,5 @@ textarea.linkmodal {
.filter-inputs > * {
display: inline-block;
- max-width: 250px;
+ width: 220px;
}
diff --git a/src/app/samples/samples.component.spec.ts b/src/app/samples/samples.component.spec.ts
index 1964449..d3ec677 100644
--- a/src/app/samples/samples.component.spec.ts
+++ b/src/app/samples/samples.component.spec.ts
@@ -2,7 +2,7 @@
//
// import { SamplesComponent } from './samples.component';
//
-// // TODO
+// // TODO: tests
//
// describe('SamplesComponent', () => {
// let component: SamplesComponent;
diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts
index 0aa5c89..3523e14 100644
--- a/src/app/samples/samples.component.ts
+++ b/src/app/samples/samples.component.ts
@@ -2,6 +2,7 @@ import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core
import {ApiService} from '../services/api.service';
import {AutocompleteService} from '../services/autocomplete.service';
import _ from 'lodash';
+import {SampleModel} from '../models/sample.model';
interface LoadSamplesOptions {
@@ -13,6 +14,7 @@ interface KeyInterface {
id: string;
label: string;
active: boolean;
+ sortable: boolean;
}
@Component({
@@ -21,9 +23,8 @@ interface KeyInterface {
styleUrls: ['./samples.component.scss']
})
-// TODO: manage branches, introduce versioning, only upload ui from master
-// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
+// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
export class SamplesComponent implements OnInit {
@@ -32,7 +33,7 @@ export class SamplesComponent implements OnInit {
downloadCsv = false;
materials = {};
- samples = [];
+ samples: SampleModel[] = [];
totalSamples = 0; // total number of samples
csvUrl = ''; // store url separate so it only has to be generated when clicking the download button
filters = {
@@ -53,7 +54,7 @@ export class SamplesComponent implements OnInit {
{field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']},
{field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']},
{field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']},
- {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]}
+ {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']}
]
};
page = 1;
@@ -61,16 +62,16 @@ export class SamplesComponent implements OnInit {
loadSamplesQueue = []; // arguments of queued up loadSamples() calls
apiKey = '';
keys: KeyInterface[] = [
- {id: 'number', label: 'Number', active: true},
- {id: 'material.numbers', label: 'Material numbers', active: true},
- {id: 'material.name', label: 'Material name', active: true},
- {id: 'material.supplier', label: 'Supplier', active: true},
- {id: 'material.group', label: 'Material', active: false},
- {id: 'type', label: 'Type', active: true},
- {id: 'color', label: 'Color', active: true},
- {id: 'batch', label: 'Batch', active: true},
- {id: 'notes', label: 'Notes', active: false},
- {id: 'added', label: 'Added', active: true}
+ {id: 'number', label: 'Number', active: true, sortable: true},
+ {id: 'material.numbers', label: 'Material numbers', active: true, sortable: false},
+ {id: 'material.name', label: 'Material name', active: true, sortable: true},
+ {id: 'material.supplier', label: 'Supplier', active: true, sortable: true},
+ {id: 'material.group', label: 'Material', active: false, sortable: true},
+ {id: 'type', label: 'Type', active: true, sortable: true},
+ {id: 'color', label: 'Color', active: true, sortable: true},
+ {id: 'batch', label: 'Batch', active: true, sortable: true},
+ {id: 'notes', label: 'Notes', active: false, sortable: false},
+ {id: 'added', label: 'Added', active: true, sortable: true},
];
isActiveKey: {[key: string]: boolean} = {};
activeKeys: KeyInterface[] = [];
@@ -91,7 +92,8 @@ export class SamplesComponent implements OnInit {
this.materials[material._id] = material;
});
this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name);
- this.filters.filters.find(e => e.field === 'color').autocomplete = [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))];
+ this.filters.filters.find(e => e.field === 'color').autocomplete =
+ [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))];
this.loadSamples();
});
this.api.get('/user/key', (data: {key: string}) => {
@@ -112,8 +114,24 @@ export class SamplesComponent implements OnInit {
const templateKeys = [];
data.forEach(item => {
item.parameters.forEach(parameter => {
- templateKeys.push({id: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false});
- this.filters.filters.push({field: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']});
+ const parameterName = encodeURIComponent(parameter.name);
+ // exclude spectrum
+ if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) {
+ templateKeys.push({
+ id: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`,
+ label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`,
+ active: false,
+ sortable: true
+ });
+ this.filters.filters.push({
+ field: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`,
+ label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`,
+ active: false,
+ autocomplete: [],
+ mode: 'eq',
+ values: ['']
+ });
+ }
});
});
this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys);
@@ -152,10 +170,22 @@ export class SamplesComponent implements OnInit {
});
}
- sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples
+ sampleUrl(options: {
+ paging?: boolean,
+ pagingOptions?: {
+ firstPage?: boolean,
+ toPage?: number,
+ event?: Event
+ },
+ csv?: boolean,
+ export?: boolean,
+ host?: boolean
+ }) { // return url to fetch samples
const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false
const query: string[] = [];
- query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')));
+ query.push(
+ 'status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))
+ );
if (options.paging) {
if (this.samples[0]) { // do not include from-id when page size was changed
if (!options.pagingOptions.firstPage) {
@@ -178,11 +208,11 @@ export class SamplesComponent implements OnInit {
query.push('key=' + this.apiKey);
}
this.keys.forEach(key => {
- if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { // do not load material properties for table
+ // do not load material properties for table
+ if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) {
query.push('fields[]=' + key.id);
}
});
- console.log(this.filters.filters);
query.push(..._.cloneDeep(this.filters.filters)
.map(e => {
@@ -195,7 +225,6 @@ export class SamplesComponent implements OnInit {
.filter(e => e.active && e.values.length > 0)
.map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values']))))
);
- console.log(this.filters);
if (!options.export) {
additionalTableKeys.forEach(key => {
if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added
@@ -206,8 +235,9 @@ export class SamplesComponent implements OnInit {
else if (this.downloadCsv) {
query.push('fields[]=measurements.spectrum.dpt');
}
- console.log('/samples?' + query.join('&'));
- return (options.host && isDevMode() ? window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&');
+ return (options.host && isDevMode() ? window.location.host : '') +
+ (options.export ? this.api.hostName : '') +
+ '/samples?' + query.join('&');
}
loadPage(delta) {
@@ -220,17 +250,7 @@ export class SamplesComponent implements OnInit {
updateFilterFields(field) {
const filter = this.filters.filters.find(e => e.field === field);
- if (filter.mode === 'in' || filter.mode === 'nin') {
- if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') {
- filter.values.pop();
- }
- else if (filter.values[filter.values.length - 1] !== '') {
- filter.values.push((filter.field === 'added' ? new Date() : '') as string & Date);
- }
- }
- else {
- filter.values = [filter.values[0] as string & Date];
- }
+ filter.active = true;
if (filter.active) {
this.loadSamples({firstPage: true});
}
@@ -243,10 +263,13 @@ export class SamplesComponent implements OnInit {
updateActiveKeys() { // array with all activeKeys
this.activeKeys = this.keys.filter(e => e.active);
- this.activeTemplateKeys.material = this.keys.filter(e => e.id.indexOf('material.properties.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el)));
- this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el)));
- console.log(this.activeTemplateKeys);
- console.log(this.keys); // TODO: glass fiber filter not working
+ this.activeTemplateKeys.material = this.keys
+ .filter(e => e.id.indexOf('material.properties.') >= 0 && e.active)
+ .map(e => e.id.split('.')
+ .map(el => decodeURIComponent(el)));
+ this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active)
+ .map(e => e.id.split('.')
+ .map(el => decodeURIComponent(el))); // TODO: glass fiber filter not working
}
calcFieldSelectKeys() {
diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts
index 088792a..f4816bc 100644
--- a/src/app/services/login.service.ts
+++ b/src/app/services/login.service.ts
@@ -9,7 +9,20 @@ import {Observable} from 'rxjs';
})
export class LoginService implements CanActivate {
+ private pathPermissions = [
+ {path: 'templates', permission: 'maintain'},
+ {path: 'users', permission: 'admin'}
+ ];
+ readonly levels = [
+ 'read',
+ 'write',
+ 'maintain',
+ 'dev',
+ 'admin'
+ ];
+
private loggedIn;
+ private level;
constructor(
private api: ApiService,
@@ -20,13 +33,30 @@ export class LoginService implements CanActivate {
login(username = '', password = '') {
return new Promise(resolve => {
- if (username !== '') {
- this.storage.set('basicAuth', btoa(username + ':' + password));
+ if (username !== '' || password !== '') { // some credentials given
+ let credentials: string[];
+ const credentialString: string = this.storage.get('basicAuth');
+ if (credentialString) { // found stored credentials
+ credentials = atob(credentialString).split(':');
+ }
+ else {
+ credentials = ['', ''];
+ }
+ if (username !== '' && password !== '') { // all credentials given
+ this.storage.set('basicAuth', btoa(username + ':' + password));
+ }
+ else if (username !== '') { // username given
+ this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
+ }
+ else if (password !== '') { // password given
+ this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
+ }
}
this.api.get('/authorized', (data: any, error) => {
if (!error) {
if (data.status === 'Authorization successful') {
this.loggedIn = true;
+ this.level = data.level;
resolve(true);
} else {
this.loggedIn = false;
@@ -49,14 +79,21 @@ export class LoginService implements CanActivate {
canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable
{
return new Observable(observer => {
- if (this.loggedIn === undefined) {
- this.login().then(res => {
- observer.next(res as any);
+ const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
+ if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path
+ if (this.loggedIn === undefined) {
+ this.login().then(res => {
+ observer.next(res as any);
+ observer.complete();
+ });
+ }
+ else {
+ observer.next(this.loggedIn);
observer.complete();
- });
+ }
}
else {
- observer.next(this.loggedIn);
+ observer.next(false);
observer.complete();
}
});
@@ -66,6 +103,10 @@ export class LoginService implements CanActivate {
return this.loggedIn;
}
+ is(level) {
+ return this.levels.indexOf(this.level) >= this.levels.indexOf(level);
+ }
+
get username() {
return atob(this.storage.get('basicAuth')).split(':')[0];
}
diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts
index 229c967..a58ec72 100644
--- a/src/app/services/validation.service.ts
+++ b/src/app/services/validation.service.ts
@@ -66,10 +66,16 @@ export class ValidationService {
return {ok: true, error: ''};
}
- string(data) {
- const {ignore, error} = Joi.string().max(128).allow('').validate(data);
+ string(data, option = null) {
+ let validator = Joi.string().max(128).allow('');
+ let errorMsg = 'must contain max 128 characters';
+ if (option === 'alphanum') {
+ validator = validator.alphanum();
+ errorMsg = 'must contain max 128 alphanumerical characters';
+ }
+ const {ignore, error} = validator.validate(data);
if (error) {
- return {ok: false, error: 'must contain max 128 characters'};
+ return {ok: false, error: errorMsg};
}
return {ok: true, error: ''};
}
@@ -121,4 +127,60 @@ export class ValidationService {
}
return {ok: true, error: ''};
}
+
+ equal(data, compare) {
+ if (data !== compare) {
+ return {ok: false, error: `must be equal`};
+ }
+ return {ok: true, error: ''};
+ }
+
+ parameterName(data) {
+ const {ignore, error} = Joi.string()
+ .max(128)
+ .invalid('condition_template', 'material_template')
+ .pattern(/^[^.]+$/)
+ .required()
+ .messages({'string.pattern.base': 'name must not contain a dot'})
+ .validate(data);
+ if (error) {
+ return {ok: false, error: error.details[0].message};
+ }
+ return {ok: true, error: ''};
+ }
+
+ parameterRange(data) {
+ if (data) {
+ try {
+ const {ignore, error} = Joi.object({
+ values: Joi.array()
+ .min(1),
+
+ min: Joi.number(),
+
+ max: Joi.number(),
+
+ type: Joi.string()
+ .valid('array')
+ })
+ .oxor('values', 'min')
+ .oxor('values', 'max')
+ .oxor('type', 'values')
+ .oxor('type', 'min')
+ .oxor('type', 'max')
+ .required()
+ .validate(JSON.parse(data));
+ if (error) {
+ return {ok: false, error: error.details[0].message};
+ }
+ }
+ catch (e) {
+ return {ok: false, error: `no valid JSON`};
+ }
+ return {ok: true, error: ''};
+ }
+ else {
+ return {ok: false, error: `no valid value`};
+ }
+ }
}
diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html
new file mode 100644
index 0000000..8e73ba1
--- /dev/null
+++ b/src/app/settings/settings.component.html
@@ -0,0 +1,45 @@
+Settings
+
+
+
+
+Change password
+
+
+
diff --git a/src/app/settings/settings.component.scss b/src/app/settings/settings.component.scss
new file mode 100644
index 0000000..3651625
--- /dev/null
+++ b/src/app/settings/settings.component.scss
@@ -0,0 +1,7 @@
+.pass-heading {
+ margin-top: 40px;
+}
+
+.message {
+ margin-left: 20px;
+}
diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/settings.component.spec.ts
new file mode 100644
index 0000000..91588f3
--- /dev/null
+++ b/src/app/settings/settings.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SettingsComponent } from './settings.component';
+
+describe('SettingsComponent', () => {
+ let component: SettingsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SettingsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SettingsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts
new file mode 100644
index 0000000..313d727
--- /dev/null
+++ b/src/app/settings/settings.component.ts
@@ -0,0 +1,68 @@
+import { Component, OnInit } from '@angular/core';
+import {ApiService} from '../services/api.service';
+import {UserModel} from '../models/user.model';
+import {Router} from '@angular/router';
+import {LoginService} from '../services/login.service';
+
+
+@Component({
+ selector: 'app-settings',
+ templateUrl: './settings.component.html',
+ styleUrls: ['./settings.component.scss']
+})
+export class SettingsComponent implements OnInit {
+
+ user: UserModel = new UserModel();
+ password = '';
+ messageUser = '';
+ messagePass = '';
+
+ constructor(
+ private api: ApiService,
+ private login: LoginService,
+ private router: Router
+ ) { }
+
+ ngOnInit(): void {
+ this.api.get('/user', data => {
+ this.user.deserialize(data);
+ });
+ }
+
+ saveUser() {
+ this.api.put('/user', this.user.sendFormat(), (data, err) => {
+ if (err) {
+ this.messageUser = err.error.status;
+ }
+ else {
+ this.login.login(data.name).then(res => {
+ if (res) {
+ this.router.navigate(['/samples']);
+ }
+ else {
+ this.messageUser = 'request not successful, try again';
+ }
+ });
+ }
+ });
+ }
+
+ savePass() {
+ this.api.put('/user', {pass: this.password}, (ignore, err) => {
+ if (err) {
+ this.messagePass = err.error.status;
+ }
+ else {
+ this.login.login('', this.password).then(res => {
+ if (res) {
+ this.router.navigate(['/samples']);
+ }
+ else {
+ this.messagePass = 'request not successful, try again';
+ }
+ });
+ }
+ });
+ }
+
+}
diff --git a/src/app/templates/templates.component.html b/src/app/templates/templates.component.html
new file mode 100644
index 0000000..6d71ebc
--- /dev/null
+++ b/src/app/templates/templates.component.html
@@ -0,0 +1,67 @@
+Templates
+
+
+ Materials
+ Measurements
+ Conditions
+
+
+
+New template
+
+
+
+
+
+
+
+
+
+
{{group.name}}
+
{{group.version}}
+
+
+
+
+ {{template.name}}
+ {{template.version}}
+ {{template.parameters | parameters}}
+
+
+
+
+
+
+
diff --git a/src/app/templates/templates.component.scss b/src/app/templates/templates.component.scss
new file mode 100644
index 0000000..ede71e8
--- /dev/null
+++ b/src/app/templates/templates.component.scss
@@ -0,0 +1,40 @@
+@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
+
+.list {
+
+ .row {
+ display: grid;
+ grid-template-columns: 1fr 4fr;
+ border-bottom: 1px solid $color-gray-mercury;
+ overflow: hidden;
+
+ & > div {
+ padding: 8px 5px;
+
+ &.header {
+ font-weight: bold;
+ }
+
+ &.details {
+ grid-column: span 2;
+ display: grid;
+ grid-template-columns: 1fr 1fr 3fr;
+ background: $color-gray-alabaster;
+
+ .template-actions {
+ grid-column: span 3;
+ margin-top: 10px;
+
+ .parameters {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ }
+
+ rb-icon-button[icon="save"] {
+ float: right;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/templates/templates.component.spec.ts b/src/app/templates/templates.component.spec.ts
new file mode 100644
index 0000000..b5c63aa
--- /dev/null
+++ b/src/app/templates/templates.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TemplatesComponent } from './templates.component';
+
+describe('TemplatesComponent', () => {
+ let component: TemplatesComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TemplatesComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TemplatesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/templates/templates.component.ts b/src/app/templates/templates.component.ts
new file mode 100644
index 0000000..c86e07c
--- /dev/null
+++ b/src/app/templates/templates.component.ts
@@ -0,0 +1,124 @@
+import { Component, OnInit } from '@angular/core';
+import {ApiService} from '../services/api.service';
+import {TemplateModel} from '../models/template.model';
+import {animate, style, transition, trigger} from '@angular/animations';
+import {ValidationService} from '../services/validation.service';
+import _ from 'lodash';
+
+@Component({
+ selector: 'app-templates',
+ templateUrl: './templates.component.html',
+ styleUrls: ['./templates.component.scss'],
+ animations: [
+ trigger(
+ 'inOut', [
+ transition(':enter', [
+ style({height: 0, opacity: 0}),
+ animate('0.5s ease-out', style({height: '*', opacity: 1}))
+ ]),
+ transition(':leave', [
+ style({height: '*', opacity: 1}),
+ animate('0.5s ease-in', style({height: 0, opacity: 0}))
+ ])
+ ]
+ )
+ ]
+})
+export class TemplatesComponent implements OnInit {
+
+ collection = 'measurement';
+ templates: TemplateModel[] = [];
+ templateGroups: {[first_id: string]: TemplateModel[]} = {}; // templates grouped by first_id
+ templateEdit: {[first_id: string]: TemplateModel} = {}; // latest template of each first_id for editing
+ groupsView: {first_id: string, name: string, version: number, expanded: boolean, edit: boolean, entries: TemplateModel[]}[] = [];
+ arr = ['testA', 'testB', 'testC'];
+
+ constructor(
+ private api: ApiService,
+ private validate: ValidationService
+ ) { }
+
+ ngOnInit(): void {
+ this.loadTemplates();
+ }
+
+ loadTemplates() {
+ this.api.get(`/template/${this.collection}s`, data => {
+ this.templates = data;
+ this.templateFormat();
+ });
+ }
+
+ templateFormat() {
+ this.templateGroups = {};
+ this.templateEdit = {};
+ this.templates.forEach(template => {
+ if (this.templateGroups[template.first_id]) {
+ this.templateGroups[template.first_id].push(template);
+ }
+ else {
+ this.templateGroups[template.first_id] = [template];
+ }
+ });
+ Object.keys(this.templateGroups).forEach(id => {
+ this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version);
+ this.templateEdit[id] = _.cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
+ this.templateEdit[id].parameters = this.templateEdit[id].parameters.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
+ });
+ this.groupsView = Object.values(this.templateGroups)
+ .map(e => ({
+ first_id: e[e.length - 1].first_id,
+ name: e[e.length - 1].name,
+ version: e[e.length - 1].version,
+ expanded: false,
+ edit: false,
+ entries: e
+ }));
+ }
+
+ saveTemplate(first_id) {
+ const template = _.cloneDeep(this.templateEdit[first_id]);
+ template.parameters = template.parameters.filter(e => e.name !== '');
+ let valid = true;
+ valid = valid && this.validate.string(template.name).ok;
+ template.parameters.forEach(parameter => {
+ valid = valid && this.validate.parameterName(parameter.name).ok;
+ valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
+ if (valid) {
+ parameter.range = JSON.parse(parameter.rangeString);
+ }
+ });
+ if (valid) {
+ console.log('valid', template);
+ const sendData = {name: template.name, parameters: template.parameters.map(e => _.omit(e, ['rangeString']))};
+ if (first_id === 'null') {
+ this.api.post(`/template/${this.collection}/new`, sendData, data => {
+ if (data.version > template.version) { // there were actual changes and a new version was created
+ this.templates.push(data);
+ }
+ this.templateFormat();
+ });
+ }
+ else {
+ this.api.put(`/template/${this.collection}/${template.first_id}`, sendData, data => {
+ if (data.version > template.version) { // there were actual changes and a new version was created
+ this.templates.push(data);
+ }
+ this.templateFormat();
+ });
+ }
+ }
+ else {
+ console.log('not valid');
+ }
+ }
+
+ newTemplate() {
+ if (!this.templateEdit.null) {
+ const template = new TemplateModel();
+ template.name = 'new template';
+ this.groupsView.push({first_id: 'null', name: 'new template', version: 0, expanded: true, edit: true, entries: [template]});
+ this.templateEdit.null = new TemplateModel();
+ }
+ }
+}
diff --git a/src/app/users/users.component.html b/src/app/users/users.component.html
new file mode 100644
index 0000000..b176428
--- /dev/null
+++ b/src/app/users/users.component.html
@@ -0,0 +1,94 @@
+Users
+
+New user
+
+
+
+
+
+ Name
+ Email
+ Level
+ Location
+ Device
+
+
+
+
+
+ {{user.name}}
+ {{user.email}}
+ {{user.level}}
+ {{user.location}}
+ {{user.device_name}}
+
+
+
+
+
+ {{nameInput.errors.failure}}
+ Cannot be empty
+
+
+
+
+ Invalid email
+ Cannot be empty
+
+
+
+
+ {{level}}
+
+
+
+
+ {{locationInput.errors.failure}}
+ Cannot be empty
+
+
+
+
+ {{deviceInput.errors.failure}}
+
+
+ Save
+
+
+
diff --git a/src/app/users/users.component.scss b/src/app/users/users.component.scss
new file mode 100644
index 0000000..9ba4fcf
--- /dev/null
+++ b/src/app/users/users.component.scss
@@ -0,0 +1,3 @@
+::ng-deep td .error-messages {
+ position: absolute;
+}
diff --git a/src/app/users/users.component.spec.ts b/src/app/users/users.component.spec.ts
new file mode 100644
index 0000000..909b5ba
--- /dev/null
+++ b/src/app/users/users.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UsersComponent } from './users.component';
+
+describe('UsersComponent', () => {
+ let component: UsersComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ UsersComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/users/users.component.ts b/src/app/users/users.component.ts
new file mode 100644
index 0000000..d80817b
--- /dev/null
+++ b/src/app/users/users.component.ts
@@ -0,0 +1,47 @@
+import { Component, OnInit } from '@angular/core';
+import {ApiService} from '../services/api.service';
+import {UserModel} from '../models/user.model';
+import {LoginService} from '../services/login.service';
+
+
+@Component({
+ selector: 'app-users',
+ templateUrl: './users.component.html',
+ styleUrls: ['./users.component.scss']
+})
+export class UsersComponent implements OnInit {
+
+ users: UserModel[] = [];
+ newUser: UserModel | null = null;
+ newUserPass = '';
+
+ constructor(
+ private api: ApiService,
+ public login: LoginService
+ ) { }
+
+ ngOnInit(): void {
+ this.api.get('/users', data => {
+ this.users = data.map(e => new UserModel().deserialize(e));
+ });
+ }
+
+ saveUser(user: UserModel) {
+ this.api.put('/user/' + user.origName, user.sendFormat('admin'), data => {
+ user.deserialize(data);
+ user.edit = false;
+ });
+ }
+
+ saveNewUser() {
+ this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => {
+ this.newUser = null;
+ this.users.push(new UserModel().deserialize(data));
+ });
+ }
+
+ addNewUser() {
+ this.newUser = this.newUser ? null : new UserModel();
+ }
+
+}
diff --git a/src/assets/imgs/supergraphic.svg b/src/assets/imgs/supergraphic.svg
new file mode 100644
index 0000000..85e56b9
--- /dev/null
+++ b/src/assets/imgs/supergraphic.svg
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/styles.scss b/src/styles.scss
index d08cb34..a0f6fd5 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -15,3 +15,11 @@ a, a:active, a:focus {
button::-moz-focus-inner {
border: 0;
}
+
+.supergraphic {
+ background-image: url("assets/imgs/supergraphic.svg");
+}
+
+.clickable {
+ cursor: pointer;
+}