diff --git a/.vscode/launch.json b/.vscode/launch.json index 5642e427..170691ac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,7 @@ "ASSETS": "projects/aas-server/src/assets", "ENDPOINTS": "[\"file:///endpoints/samples?name=Samples\"]", // "USER_STORAGE": "mongodb://aas-server:bObXJWW6e8Nh78YF@localhost:27017/aasportal-users", - // "TEMPLATE_STORAGE": "http://aas-server:5w0vmrkzrwDIDyZs@localhost:8080/templates", + "TEMPLATE_STORAGE": "http://aas-server:5w0vmrkzrwDIDyZs@localhost:8080/templates", "AAS_INDEX": "mysql://aas-server:60PYRe6Vd8C99u4n@localhost:3306", } }, diff --git a/.vscode/settings.json b/.vscode/settings.json index c571fc5d..fc4ad8e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "opnxml", "owncloud", "preprocessors", + "qrcode", "Qualifiable", "rechtlich", "selbstaendige", diff --git a/package-lock.json b/package-lock.json index 99e9fef9..abaaa4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aas-portal-project", - "version": "3.0.0-development.118", + "version": "3.0.0-development.121", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aas-portal-project", - "version": "3.0.0-development.118", + "version": "3.0.0-development.121", "license": "Apache-2.0", "workspaces": [ "projects/fhg-jest", @@ -53,6 +53,7 @@ "node-opcua": "^2.128.0", "node-opcua-client-crawler": "^2.124.0", "nodemailer": "^6.9.15", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "~7.8.1", "swagger-ui-express": "^5.0.1", @@ -92,6 +93,7 @@ "@types/multer": "^1.4.12", "@types/node": "^20.11.1", "@types/nodemailer": "^6.4.15", + "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^10.0.0", @@ -9035,6 +9037,16 @@ "@types/retry": "*" } }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -11246,7 +11258,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12889,6 +12900,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -13153,6 +13173,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -28073,7 +28099,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -28278,7 +28303,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -29087,6 +29111,165 @@ "node": ">=0.9" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -29532,6 +29715,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -30631,6 +30820,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -34245,6 +34440,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", @@ -34382,7 +34583,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -34477,7 +34677,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -34493,7 +34692,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -34506,21 +34704,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -34530,7 +34725,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index 14fb0ea5..64ff83ab 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "node-opcua": "^2.128.0", "node-opcua-client-crawler": "^2.124.0", "nodemailer": "^6.9.15", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "~7.8.1", "swagger-ui-express": "^5.0.1", @@ -115,6 +116,7 @@ "@types/multer": "^1.4.12", "@types/node": "^20.11.1", "@types/nodemailer": "^6.4.15", + "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^10.0.0", diff --git a/projects/aas-core/src/lib/index.ts b/projects/aas-core/src/lib/index.ts index 5ee0411e..e3fc1617 100644 --- a/projects/aas-core/src/lib/index.ts +++ b/projects/aas-core/src/lib/index.ts @@ -167,6 +167,28 @@ export function isHasSemantics(value: unknown): value is HasSemantics { ); } +/** + * Gets the semantic identifier of the specified AAS element. + * @param value The AAS element. + * @returns The semantic identifier or `undefined`. + */ +export function getSemanticId(value: HasSemantics | Reference): string | undefined { + let semanticId: string | undefined; + if (value) { + if (isReference(value)) { + if (value.keys.length > 0) { + return value.keys[0].value; + } + } else { + if (value.semanticId?.keys != null && value.semanticId.keys.length > 0) { + return value.semanticId.keys[0].value; + } + } + } + + return semanticId; +} + /** * Determines whether the specified value represents a submodel element. * @param value The current value. diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.component.ts b/projects/aas-lib/src/lib/aas-table/aas-table.component.ts index 79385685..82d0c874 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.component.ts +++ b/projects/aas-lib/src/lib/aas-table/aas-table.component.ts @@ -29,6 +29,7 @@ import { ViewMode } from '../types/view-mode'; import { AASTableStore } from './aas-table.store'; import { MaxLengthPipe } from '../max-length.pipe'; import { AASTableFilter } from './aas-table.filter'; +import { encodeBase64Url } from '../convert'; @Component({ selector: 'fhg-aas-table', @@ -111,13 +112,12 @@ export class AASTableComponent implements OnDestroy { } public open(row: AASTableRow): void { - this.clipboard.set('AASDocument', row.element); this.router.navigate(['/aas'], { - skipLocationChange: true, queryParams: { - id: row.id, - endpoint: row.endpoint, + endpoint: encodeBase64Url(row.endpoint), + id: encodeBase64Url(row.id), }, + state: { data: JSON.stringify(row.element) }, }); } diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts index cf1732ce..5490bcce 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts @@ -15,6 +15,7 @@ import { extensionToMimeType, getAbbreviation, getLocaleValue, + getSemanticId, isAnnotatedRelationshipElement, isAssetAdministrationShell, isBlob, @@ -37,10 +38,10 @@ import { toLocale, } from 'aas-core'; -import { resolveSemanticId, supportedSubmodelTemplates } from '../submodel-template/submodel-template'; import { Tree, TreeNode } from '../tree'; import { basename, normalize } from '../convert'; import { signal, WritableSignal } from '@angular/core'; +import { findRoute } from '../views/submodel-template'; export class AASTreeRow extends TreeNode { public constructor( @@ -377,7 +378,7 @@ class TreeInitialize { } if (isSubmodel(referable)) { - const sid = this.getSemanticId(referable); + const sid = getSemanticId(referable); return sid ? `Semantic ID: ${sid}` : '-'; } @@ -434,17 +435,17 @@ class TreeInitialize { return '-'; } - private getSemanticId(hasSematics: aas.HasSemantics): string { - return this.referenceToString(hasSematics?.semanticId); - } - private referenceToString(reference: aas.Reference | undefined): string { return reference?.keys.map(key => key.value).join('.') ?? '-'; } private hasSpecificSemantic(submodel: aas.Submodel): boolean { - const sematicId = resolveSemanticId(submodel); - return sematicId != null && supportedSubmodelTemplates.has(sematicId); + const sematicId = getSemanticId(submodel); + if (sematicId === undefined) { + return false; + } + + return findRoute(sematicId) !== undefined; } } diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts index a76baae1..c1200634 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts @@ -32,6 +32,7 @@ import { isOperation, isSubmodel, equalDocument, + getSemanticId, } from 'aas-core'; import { AASTree, AASTreeRow } from './aas-tree-row'; @@ -41,18 +42,13 @@ import { ShowVideoFormComponent } from '../show-video-form/show-video-form.compo import { OperationCallFormComponent } from '../operation-call-form/operation-call-form.component'; import { AASTreeSearch } from './aas-tree-search'; import { basename, encodeBase64Url } from '../convert'; -import { ViewQuery } from '../types/view-query-params'; import { WindowService } from '../window.service'; import { DocumentService } from '../document.service'; import { DownloadService } from '../download.service'; import { WebSocketFactoryService } from '../web-socket-factory.service'; import { ClipboardService } from '../clipboard.service'; import { LogType, NotifyService } from '../notify/notify.service'; -import { - SubmodelViewDescriptor, - resolveSemanticId, - supportedSubmodelTemplates, -} from '../submodel-template/submodel-template'; +import { findRoute } from '../views/submodel-template'; import { AASTreeApiService } from './aas-tree-api.service'; import { AASTreeStore } from './aas-tree.store'; @@ -284,7 +280,7 @@ export class AASTreeComponent implements OnInit, OnDestroy { } else if (isOperation(node.element)) { this.openOperation(node.element); } else if (isSubmodel(node.element)) { - this.openSubmodel(node.element); + this.openView(node.element); } } @@ -396,29 +392,29 @@ export class AASTreeComponent implements OnInit, OnDestroy { } } - private openSubmodel(submodel: aas.Submodel | undefined): void { - if (!submodel || this.state() === 'online') return; - - const semanticId = resolveSemanticId(submodel); - if (semanticId) { - const document = this.document(); - const template = supportedSubmodelTemplates.get(semanticId); - if (template && document) { - const descriptor: SubmodelViewDescriptor = { - template, - submodels: [ - { - id: document.id, - endpoint: document.endpoint, - idShort: submodel.idShort, - }, - ], - }; - - this.clipboard.set('ViewQuery', { descriptor } as ViewQuery); - this.router.navigateByUrl('/view?format=ViewQuery', { skipLocationChange: true }); - } + private openView(submodel: aas.Submodel | undefined): Promise { + const document = this.document(); + if (submodel === undefined || this.state() === 'online' || document === null) { + return Promise.resolve(false); + } + + const semanticId = getSemanticId(submodel); + if (semanticId === undefined) { + return Promise.resolve(false); } + + const route = findRoute(semanticId); + if (route === undefined) { + return Promise.resolve(false); + } + + return this.router.navigate([`/view/${route.path}`], { + queryParams: { + endpoint: encodeBase64Url(document.endpoint), + id: encodeBase64Url(document.id), + }, + state: { data: JSON.stringify([document]) }, + }); } private async showImageAsync(name: string, src: string): Promise { @@ -504,13 +500,12 @@ export class AASTreeComponent implements OnInit, OnDestroy { } } - private openDocumentByAssetId(assetId: string): void { - if (assetId) { + private openDocumentByAssetId(id: string): void { + if (id) { this.clipboard.clear('AASDocument'); this.router.navigate(['/aas'], { - skipLocationChange: true, onSameUrlNavigation: 'reload', - queryParams: { id: assetId }, + queryParams: { id: encodeBase64Url(id) }, }); } } @@ -518,9 +513,8 @@ export class AASTreeComponent implements OnInit, OnDestroy { private openExternalReference(reference: aas.Reference): void { this.clipboard.clear('AASDocument'); this.router.navigate(['/aas'], { - skipLocationChange: true, onSameUrlNavigation: 'reload', - queryParams: { id: reference.keys[0].value }, + queryParams: { id: encodeBase64Url(reference.keys[0].value) }, }); } @@ -536,9 +530,8 @@ export class AASTreeComponent implements OnInit, OnDestroy { } else if (reference.keys[0].type === 'AssetAdministrationShell') { this.clipboard.clear('AASDocument'); this.router.navigate(['/aas'], { - skipLocationChange: true, onSameUrlNavigation: 'reload', - queryParams: { id: reference.keys[0].value }, + queryParams: { id: encodeBase64Url(reference.keys[0].value) }, }); } } diff --git a/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.html b/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.html index adbe983a..a746454b 100644 --- a/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.html +++ b/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.html @@ -21,7 +21,7 @@
{{name()}}
- @for (item of items(); track item) { + @for (item of items(); track item.name) {
{{item.name}}
diff --git a/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.ts b/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.ts index 8a442919..e7cd93fb 100644 --- a/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.ts +++ b/projects/aas-lib/src/lib/customer-feedback/customer-feedback.component.ts @@ -6,12 +6,11 @@ * *****************************************************************************/ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, effect, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, effect, signal } from '@angular/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { DecimalPipe } from '@angular/common'; +import { DecimalPipe, Location } from '@angular/common'; import { Subscription } from 'rxjs'; -import { aas, getLocaleValue, getPreferredName } from 'aas-core'; -import { DocumentSubmodelPair, SubmodelTemplate } from '../submodel-template/submodel-template'; +import { aas, AASDocument, getLocaleValue, getPreferredName, isReference } from 'aas-core'; import { ScoreComponent } from '../score/score.component'; export interface GeneralItem { @@ -28,6 +27,7 @@ export interface FeedbackItem { subject: string; message: string; } +const CustomerFeedback = 'urn:IOSB:Fraunhofer:de:KIReallabor:CUNACup:SemId:Submodel:CustomerFeedback'; @Component({ selector: 'fhg-customer-feedback', @@ -37,12 +37,16 @@ export interface FeedbackItem { imports: [ScoreComponent, DecimalPipe, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CustomerFeedbackComponent implements SubmodelTemplate, OnInit, OnDestroy { +export class CustomerFeedbackComponent implements OnInit, OnDestroy { private static readonly maxStars = 5; private readonly map = new Map(); private readonly subscription = new Subscription(); + private readonly submodels = signal<[aas.Environment, aas.Submodel][]>([]); - public constructor(private readonly translate: TranslateService) { + public constructor( + private readonly location: Location, + private readonly translate: TranslateService, + ) { effect( () => { this.init(this.submodels()); @@ -51,8 +55,6 @@ export class CustomerFeedbackComponent implements SubmodelTemplate, OnInit, OnDe ); } - public readonly submodels = input(null); - public readonly name = computed(() => { const submodels = this.submodels(); if (!submodels) { @@ -60,9 +62,8 @@ export class CustomerFeedbackComponent implements SubmodelTemplate, OnInit, OnDe } const names = submodels.map( - item => - getLocaleValue(getPreferredName(item.document.content!, item.submodel), this.translate.currentLang) ?? - item.submodel.idShort, + ([env, submodel]) => + getLocaleValue(getPreferredName(env, submodel), this.translate.currentLang) ?? submodel.idShort, ); if (names.length <= 2) { @@ -79,6 +80,12 @@ export class CustomerFeedbackComponent implements SubmodelTemplate, OnInit, OnDe public readonly starClassNames = signal([]); public ngOnInit(): void { + const state = this.location.getState() as Record; + if (state.data) { + const documents: AASDocument[] = JSON.parse(state.data); + this.submodels.set([...this.filterSubmodels(documents)]); + } + this.subscription.add( this.translate.onLangChange.subscribe(() => { this.init(this.submodels()); @@ -90,60 +97,86 @@ export class CustomerFeedbackComponent implements SubmodelTemplate, OnInit, OnDe this.subscription.unsubscribe(); } - private init(submodels: DocumentSubmodelPair[] | null): void { + private init(submodels: [aas.Environment, aas.Submodel][]): void { this.map.clear(); let count = 0; let stars = 0.0; const items: GeneralItem[] = []; const feedbacks: FeedbackItem[] = []; - let starClassNames: string[] | undefined; let sumStars = 0; - if (submodels) { - for (const pair of submodels) { - if (pair.submodel.submodelElements) { - for (const feedback of pair.submodel.submodelElements.filter( - item => item.modelType === 'SubmodelElementCollection', - )) { - const general = (feedback as aas.SubmodelElementCollection).value?.find( - item => item.modelType === 'SubmodelElementCollection' && item.idShort === 'General', - ); - - if (general) { - sumStars += this.getStars(feedback); - this.buildItems(general, items); - ++count; - } - - feedbacks.push({ - stars: this.initStarClassNames(this.getStars(feedback)), - createdAt: this.getCreatedAt(feedback), - subject: pair.submodel.idShort, - message: this.getMessage(feedback), - }); + + for (const [, submodel] of submodels) { + if (submodel.submodelElements) { + for (const feedback of submodel.submodelElements.filter( + item => item.modelType === 'SubmodelElementCollection', + )) { + const general = (feedback as aas.SubmodelElementCollection).value?.find( + item => item.modelType === 'SubmodelElementCollection' && item.idShort === 'General', + ); + + if (general) { + sumStars += this.getStars(feedback); + this.buildItems(general, items); + ++count; } - } - } - if (count > 0) { - stars = sumStars / count; - items.forEach(item => { - item.score = item.sum / item.count; - item.like = item.score >= 0.0; - }); + feedbacks.push({ + stars: this.initStarClassNames(this.getStars(feedback)), + createdAt: this.getCreatedAt(feedback), + subject: submodel.idShort, + message: this.getMessage(feedback), + }); + } } + } - starClassNames = this.initStarClassNames(stars); - } else { - starClassNames = this.initStarClassNames(0); + if (count > 0) { + stars = sumStars / count; + items.forEach(item => { + item.score = item.sum / item.count; + item.like = item.score >= 0.0; + }); } this.stars.set(stars); this.count.set(count); - this.starClassNames.set(starClassNames); + this.starClassNames.set(this.initStarClassNames(stars)); this.items.set(items.filter(item => item.count > 0)); this.feedbacks.set(feedbacks); } + private *filterSubmodels(documents: AASDocument[]): Generator<[aas.Environment, aas.Submodel]> { + for (const document of documents) { + if (!document.content) { + continue; + } + + for (const submodel of document.content.submodels) { + const semanticId = this.getSemanticId(submodel); + if (semanticId === CustomerFeedback) { + yield [document.content, submodel]; + } + } + } + } + + private getSemanticId(value: aas.HasSemantics | aas.Reference): string | undefined { + let semanticId: string | undefined; + if (value) { + if (isReference(value)) { + if (value.keys.length > 0) { + return value.keys[0].value; + } + } else { + if (value.semanticId?.keys != null && value.semanticId.keys.length > 0) { + return value.semanticId.keys[0].value; + } + } + } + + return semanticId; + } + private buildItems(general: aas.SubmodelElementCollection, items: GeneralItem[]): void { if (general.value) { for (const element of general.value.filter(child => child.modelType === 'SubmodelElementCollection')) { diff --git a/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.html b/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.html index 06da3aa0..1b4eeeb7 100644 --- a/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.html +++ b/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.html @@ -6,7 +6,7 @@ ! !----------------------------------------------------------------------------> -@for (nameplate of nameplates(); track nameplate) { +@for (nameplate of nameplates(); track nameplate.id) {
diff --git a/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.ts b/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.ts index b21ba3db..c676212d 100644 --- a/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.ts +++ b/projects/aas-lib/src/lib/digital-nameplate/digital-nameplate.component.ts @@ -6,20 +6,23 @@ * *****************************************************************************/ -import { ChangeDetectionStrategy, Component, OnChanges, SimpleChanges, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, signal } from '@angular/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { aas, + AASDocument, convertToString, getLocaleValue, + getSemanticId, isMultiLanguageProperty, isProperty, isSubmodelElementCollection, } from 'aas-core'; -import { DocumentSubmodelPair, SubmodelTemplate } from '../submodel-template/submodel-template'; +import { Location } from '@angular/common'; -export interface DigitalNameplate { +export type DigitalNameplate = { + id: string; serialNumber: string; productCountryOfOrigin: string; yearOfConstruction: string; @@ -28,7 +31,11 @@ export interface DigitalNameplate { manufacturerName: string; cityTown: string; street: string; -} +}; + +const ZVEINameplate = 'https://admin-shell.io/zvei/nameplate/2/0/Nameplate'; +const FHGNameplate = 'urn:IOSB:Fraunhofer:de:KIReallabor:CUNACup:SemId:Submodel:Nameplate'; +const HSUNameplate = 'https://www.hsu-hh.de/aut/aas/nameplate'; @Component({ selector: 'fhg-digital-nameplate', @@ -38,34 +45,35 @@ export interface DigitalNameplate { imports: [TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DigitalNameplateComponent implements SubmodelTemplate, OnChanges { - public constructor(private readonly translate: TranslateService) {} - - public readonly submodels = input(null); +export class DigitalNameplateComponent implements OnInit { + public constructor( + private readonly location: Location, + private readonly translate: TranslateService, + ) {} public readonly nameplates = signal([]); - public ngOnChanges(changes: SimpleChanges): void { - if (changes['submodels']) { - this.init(); + public ngOnInit(): void { + const state = this.location.getState() as Record; + if (state.data) { + this.init(JSON.parse(state.data)); } } - private init() { + private init(documents: AASDocument[]) { + const submodels = [...this.filterSubmodels(documents)]; this.nameplates.set( - this.submodels()?.map(pair => { - const submodel = pair.submodel; - return { - serialNumber: this.getPropertyValue(submodel, ['SerialNumber']), - productCountryOfOrigin: this.getPropertyValue(submodel, ['ProductCountryOfOrigin']), - yearOfConstruction: this.getPropertyValue(submodel, ['YearOfConstruction']), - manufacturerName: this.getPropertyValue(submodel, ['ManufacturerName']), - countryCode: this.getPropertyValue(submodel, ['PhysicalAddress', 'CountryCode']), - zip: this.getPropertyValue(submodel, ['PhysicalAddress', 'Zip']), - cityTown: this.getPropertyValue(submodel, ['PhysicalAddress', 'CityTown']), - street: this.getPropertyValue(submodel, ['PhysicalAddress', 'Street']), - }; - }) ?? [], + submodels.map(submodel => ({ + id: submodel.id, + serialNumber: this.getPropertyValue(submodel, ['SerialNumber']), + productCountryOfOrigin: this.getPropertyValue(submodel, ['ProductCountryOfOrigin']), + yearOfConstruction: this.getPropertyValue(submodel, ['YearOfConstruction']), + manufacturerName: this.getPropertyValue(submodel, ['ManufacturerName']), + countryCode: this.getPropertyValue(submodel, ['PhysicalAddress', 'CountryCode']), + zip: this.getPropertyValue(submodel, ['PhysicalAddress', 'Zip']), + cityTown: this.getPropertyValue(submodel, ['PhysicalAddress', 'CityTown']), + street: this.getPropertyValue(submodel, ['PhysicalAddress', 'Street']), + })), ); } @@ -88,4 +96,19 @@ export class DigitalNameplateComponent implements SubmodelTemplate, OnChanges { return ''; } + + private *filterSubmodels(documents: AASDocument[]): Generator { + for (const document of documents) { + if (!document.content) { + continue; + } + + for (const submodel of document.content.submodels) { + const semanticId = getSemanticId(submodel); + if (semanticId === ZVEINameplate || semanticId === FHGNameplate || semanticId === HSUNameplate) { + yield submodel; + } + } + } + } } diff --git a/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.html b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.html new file mode 100644 index 00000000..4cfeec01 --- /dev/null +++ b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.html @@ -0,0 +1,34 @@ + + +@if (viewData()) { +
+
+
DigitalPassportPortal.BANNER
+
DigitalPassportPortal.CAPTION
+
{{uriOfTheProduct()}}
+
DigitalPassportPortal.TYPE{{manufacturerProductType()}},  + DigitalPassportPortal.SERIAL_NUMBER{{serialNumber()}} +
+
+ +
+ +
+
+
{{dppHazardStatement()}}
+
+ +
+
+
+} @else { + +} \ No newline at end of file diff --git a/projects/aas-lib/src/lib/types/view-query-params.ts b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.scss similarity index 62% rename from projects/aas-lib/src/lib/types/view-query-params.ts rename to projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.scss index bee6cf24..1b4785f3 100644 --- a/projects/aas-lib/src/lib/types/view-query-params.ts +++ b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.scss @@ -5,13 +5,3 @@ * zur Foerderung der angewandten Forschung e.V. * *****************************************************************************/ - -import { SubmodelViewDescriptor } from '../submodel-template/submodel-template'; - -export interface ViewQueryParams { - format: string; -} - -export interface ViewQuery { - descriptor: SubmodelViewDescriptor; -} diff --git a/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.ts b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.ts new file mode 100644 index 00000000..bba7a232 --- /dev/null +++ b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.component.ts @@ -0,0 +1,157 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Component, computed, effect, ElementRef, OnInit, viewChild } from '@angular/core'; +import { aas, AASDocument, getIdShortPath, getSemanticId, selectSubmodel } from 'aas-core'; +import { Location } from '@angular/common'; +import QRCode from 'qrcode'; +import { CarbonFootprint, ZVEINameplate } from '../views/submodel-template'; +import { TranslateModule } from '@ngx-translate/core'; +import { DigitalProductPassportStore } from './digital-passport-portal.store'; +import { SecuredImageComponent } from '../secured-image/secured-image.component'; +import { basename, decodeBase64Url, encodeBase64Url } from '../convert'; +import { ActivatedRoute } from '@angular/router'; +import { first } from 'rxjs'; +import { DigitalPassportPortalService } from './digital-passport-portal.service'; + +const HandoverDocumentationId = '0173-1#01-AHF578#003'; + +@Component({ + selector: 'fhg-device-passport-portal', + standalone: true, + providers: [DigitalPassportPortalService], + imports: [TranslateModule, SecuredImageComponent], + templateUrl: './digital-passport-portal.component.html', + styleUrl: './digital-passport-portal.component.scss', +}) +export class DigitalPassportPortalComponent implements OnInit { + public constructor( + private readonly route: ActivatedRoute, + private readonly location: Location, + private readonly store: DigitalProductPassportStore, + private readonly api: DigitalPassportPortalService, + ) { + effect(() => { + const qrCode = this.qrCode(); + const uriOfTheProduct = this.uriOfTheProduct(); + if (qrCode) { + QRCode.toCanvas(qrCode!.nativeElement, uriOfTheProduct); + } + }); + } + + public readonly qrCode = viewChild>('qrCode'); + + public readonly viewData = this.store.viewData$.asReadonly(); + + public readonly uriOfTheProduct = computed(() => this.store.getNameplateString(['URIOfTheProduct'])); + + public readonly manufacturerProductType = computed(() => + this.store.getNameplateString(['ManufacturerProductType']), + ); + + public readonly serialNumber = computed(() => this.store.getNameplateString(['SerialNumber'])); + + public readonly dppHazardStatement = computed(() => + this.store.getNameplateString(['AssetSpecificProperties', 'DppHazardStatement_01']), + ); + + public readonly dppHazardSymbol = computed(() => + this.resolveFile(this.store.getNameplateFile(['AssetSpecificProperties', 'DppHazardSymbol'])), + ); + + public readonly thumbnail = computed(() => { + const document = this.store.viewData$()?.document; + if (document === undefined) { + return ''; + } + + return `/api/v1/endpoints/${encodeBase64Url(document.endpoint)}/documents/${encodeBase64Url(document.id)}/thumbnail`; + }); + + public ngOnInit(): void { + const state = this.location.getState() as Record; + if (state.data) { + const documents: AASDocument[] = JSON.parse(state.data); + this.initialize(documents); + } else { + this.route.queryParams.pipe(first()).subscribe(params => { + if (params.endpoint && params.id) { + if (params.id) { + if (params.endpoint) { + this.getDocument(decodeBase64Url(params.id), decodeBase64Url(params.endpoint)); + } else { + this.getDocument(decodeBase64Url(params.id)); + } + } + } + }); + } + } + + private getDocument(id: string, endpoint?: string): void { + this.api.getDocument(id, endpoint).subscribe({ + next: document => this.initialize([document]), + error: error => console.debug(error), + }); + } + + private initialize(documents: AASDocument[]): void { + let nameplate: aas.Submodel | undefined; + let carbonFootprint: aas.Submodel | undefined; + let handoverDocumentation: aas.Submodel | undefined; + for (const document of documents) { + if (!document.content) { + continue; + } + + for (const submodel of document.content.submodels) { + const semanticId = getSemanticId(submodel); + if (semanticId === ZVEINameplate) { + nameplate = submodel; + } else if (semanticId === CarbonFootprint) { + carbonFootprint = submodel; + } else if (semanticId === HandoverDocumentationId) { + handoverDocumentation = submodel; + } + } + + if (nameplate && carbonFootprint && handoverDocumentation) { + this.store.viewData$.set({ document, nameplate, carbonFootprint, handoverDocumentation }); + break; + } + + nameplate = carbonFootprint = handoverDocumentation = undefined; + } + } + + private resolveFile(file: aas.File | undefined): { url: string; name: string } { + const value: { url: string; name: string } = { url: '', name: '' }; + if (file === undefined || file.value === undefined) { + return value; + } + + const document = this.store.viewData$()?.document; + if (!document?.content) { + return value; + } + + const submodel = selectSubmodel(document.content, file); + if (submodel === undefined) { + return value; + } + + const smId = encodeBase64Url(submodel.id); + const path = getIdShortPath(file); + value.name = basename(file.value); + const name = encodeBase64Url(document.endpoint); + const id = encodeBase64Url(document.id); + value.url = `/api/v1/containers/${name}/documents/${id}/submodels/${smId}/submodel-elements/${path}/value`; + return value; + } +} diff --git a/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.service.ts b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.service.ts new file mode 100644 index 00000000..73a5135a --- /dev/null +++ b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.service.ts @@ -0,0 +1,47 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { AASDocument, aas } from 'aas-core'; +import { Observable } from 'rxjs'; +import { encodeBase64Url } from '../convert'; + +/** The client side AAS provider service. */ +@Injectable() +export class DigitalPassportPortalService { + public constructor(private readonly http: HttpClient) {} + + /** + * Gets the AAS document with the specified identifier. + * @param id The AAS identifier. + * @param endpoint The endpoint name. + * @returns The requested AAS document. + */ + public getDocument(id: string, endpoint?: string): Observable { + if (endpoint) { + return this.http.get( + `/api/v1/containers/${encodeBase64Url(endpoint)}/documents/${encodeBase64Url(id)}`, + ); + } + + return this.http.get(`/api/v1/documents/${encodeBase64Url(id)}`); + } + + /** + * Loads the element structure of the specified document. + * @param endpoint The endpoint name. + * @param id The identification of the AAS document. + * @returns The root of the element structure. + */ + public getContent(id: string, endpoint: string): Observable { + return this.http.get( + `/api/v1/containers/${encodeBase64Url(endpoint)}/documents/${encodeBase64Url(id)}/content`, + ); + } +} diff --git a/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.store.ts b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.store.ts new file mode 100644 index 00000000..c8a4b99c --- /dev/null +++ b/projects/aas-lib/src/lib/digital-passport-portal/digital-passport-portal.store.ts @@ -0,0 +1,71 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Injectable, signal } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { aas, AASDocument, getChildren, getLocaleValue, isFile, isMultiLanguageProperty, isProperty } from 'aas-core'; + +type ViewData = { + document: AASDocument; + nameplate: aas.Submodel; + carbonFootprint: aas.Submodel; + handoverDocumentation: aas.Submodel; +}; + +@Injectable({ providedIn: 'root' }) +export class DigitalProductPassportStore { + public constructor(private readonly translate: TranslateService) {} + + public readonly viewData$ = signal(undefined); + + public readonly thumbnail$ = signal(''); + + public getNameplateString(idShortPath: string[]): string { + const submodel = this.viewData$()?.nameplate; + if (submodel === undefined || submodel.submodelElements === undefined || idShortPath.length === 0) { + return ''; + } + + const referable = this.getReferable(submodel, idShortPath); + if (isProperty(referable)) { + return referable.value || ''; + } + + if (isMultiLanguageProperty(referable)) { + return getLocaleValue(referable.value, this.translate.currentLang) || ''; + } + + throw new Error(`Invalid path ${idShortPath.join('.')}.`); + } + + public getNameplateFile(idShortPath: string[]): aas.File | undefined { + const submodel = this.viewData$()?.nameplate; + if (submodel === undefined || submodel.submodelElements === undefined || idShortPath.length === 0) { + return undefined; + } + const referable = this.getReferable(submodel, idShortPath); + if (isFile(referable)) { + return referable; + } + + return undefined; + } + + private getReferable(submodel: aas.Submodel, idShortPath: string[]): aas.Referable | undefined { + let referable: aas.Referable | undefined = submodel; + for (const idShort of idShortPath) { + const children = getChildren(referable); + referable = children.find(child => child.idShort === idShort); + if (referable === undefined) { + return undefined; + } + } + + return referable; + } +} diff --git a/projects/aas-lib/src/lib/index-change.service.ts b/projects/aas-lib/src/lib/index-change.service.ts index 27652889..e564b339 100644 --- a/projects/aas-lib/src/lib/index-change.service.ts +++ b/projects/aas-lib/src/lib/index-change.service.ts @@ -7,18 +7,18 @@ *****************************************************************************/ import { computed, EventEmitter, Injectable, signal } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { WebSocketSubject } from 'rxjs/webSocket'; import { WebSocketData, AASServerMessage } from 'aas-core'; import { WebSocketFactoryService } from './web-socket-factory.service'; import { HttpClient } from '@angular/common/http'; -import { map, Observable, zip } from 'rxjs'; +import { first, map, mergeMap, Observable, zip } from 'rxjs'; +import { AuthService } from '../public-api'; -interface State { +type State = { documentCount: number; endpointCount: number; changedDocuments: number; -} +}; @Injectable({ providedIn: 'root', @@ -34,34 +34,27 @@ export class IndexChangeService { public constructor( private readonly http: HttpClient, private readonly webSocketFactory: WebSocketFactoryService, - private readonly translate: TranslateService, + private readonly auth: AuthService, ) { this.subscribeIndexChanged(); - zip( - this.http.get<{ count: number }>('/api/v1/endpoints/count'), - this.http.get<{ count: number }>('/api/v1/documents/count'), - ) + this.auth.ready .pipe( - map(([endpointCount, documentCount]) => [endpointCount.count, documentCount.count]), - map(([endpointCount, documentCount]) => - this.state.update(state => ({ ...state, endpointCount, documentCount })), + first(ready => ready), + mergeMap(() => + zip( + this.http.get<{ count: number }>('/api/v1/endpoints/count'), + this.http.get<{ count: number }>('/api/v1/documents/count'), + ).pipe(map(([endpointCount, documentCount]) => [endpointCount.count, documentCount.count])), ), ) - .subscribe(); + .subscribe(([endpointCount, documentCount]) => { + this.state.update(state => ({ ...state, endpointCount, documentCount })); + }); } public readonly reset = new EventEmitter(); - public readonly summary = computed(() => { - const state = this.state(); - if (state.changedDocuments === 0) { - return `${state.documentCount} ${this.translate.instant('IndexChangeService.SHELLS')} / ${state.endpointCount} ${this.translate.instant('IndexChangeService.ENDPOINTS')}`; - } - - return `${state.documentCount} ${this.translate.instant('IndexChangeService.SHELLS')} (${state.changedDocuments}) / ${state.endpointCount} ${this.translate.instant('IndexChangeService.ENDPOINTS')}`; - }); - public readonly documentCount = computed(() => this.state().documentCount); public readonly endpointCount = computed(() => this.state().endpointCount); diff --git a/projects/aas-lib/src/lib/message-table/message-table.component.html b/projects/aas-lib/src/lib/message-table/message-table.component.html index 473f2d50..af99bb3a 100644 --- a/projects/aas-lib/src/lib/message-table/message-table.component.html +++ b/projects/aas-lib/src/lib/message-table/message-table.component.html @@ -27,7 +27,7 @@ - @for (message of messages(); track message) { + @for (message of messages(); track message.text) { @if (message.type === 'Info') { diff --git a/projects/aas-lib/src/lib/notify/notify.component.html b/projects/aas-lib/src/lib/notify/notify.component.html index dbbbfb70..9088dee0 100644 --- a/projects/aas-lib/src/lib/notify/notify.component.html +++ b/projects/aas-lib/src/lib/notify/notify.component.html @@ -6,7 +6,7 @@ ! !----------------------------------------------------------------------------> -@for (message of messages(); track message) { +@for (message of messages(); track message.text) { {{message.text}} } \ No newline at end of file diff --git a/projects/aas-lib/src/lib/secured-image/secured-image.component.html b/projects/aas-lib/src/lib/secured-image/secured-image.component.html index 6ee7e251..4b7dbf9d 100644 --- a/projects/aas-lib/src/lib/secured-image/secured-image.component.html +++ b/projects/aas-lib/src/lib/secured-image/secured-image.component.html @@ -6,5 +6,15 @@ ! !----------------------------------------------------------------------------> - \ No newline at end of file +@if (width() !== undefined && height() !== undefined) { + +} @else if (width() !== undefined) { + +} @else if (height() !== undefined) { + +} @else { + +} \ No newline at end of file diff --git a/projects/aas-lib/src/lib/secured-image/secured-image.component.ts b/projects/aas-lib/src/lib/secured-image/secured-image.component.ts index 65927971..48e1172e 100644 --- a/projects/aas-lib/src/lib/secured-image/secured-image.component.ts +++ b/projects/aas-lib/src/lib/secured-image/secured-image.component.ts @@ -16,6 +16,7 @@ import { DomSanitizer } from '@angular/platform-browser'; selector: 'fhg-img', templateUrl: './secured-image.component.html', styleUrls: ['./secured-image.component.scss'], + host: {}, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -35,7 +36,7 @@ export class SecuredImageComponent { public readonly alt = input(); - public readonly classname = input(); + public readonly class = input(); public readonly width = input(); diff --git a/projects/aas-lib/src/lib/show-image-form/show-image-form.component.html b/projects/aas-lib/src/lib/show-image-form/show-image-form.component.html index 321c41bf..94bf3edb 100644 --- a/projects/aas-lib/src/lib/show-image-form/show-image-form.component.html +++ b/projects/aas-lib/src/lib/show-image-form/show-image-form.component.html @@ -14,7 +14,7 @@
@@ -85,7 +85,7 @@
@@ -93,10 +93,10 @@
diff --git a/projects/aas-portal/src/app/start/start.component.ts b/projects/aas-portal/src/app/start/start.component.ts index 5418b092..eef7d701 100644 --- a/projects/aas-portal/src/app/start/start.component.ts +++ b/projects/aas-portal/src/app/start/start.component.ts @@ -6,27 +6,33 @@ * *****************************************************************************/ -import { Router } from '@angular/router'; +import { Route, Router } from '@angular/router'; import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnDestroy, TemplateRef, computed, effect, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + TemplateRef, + computed, + effect, + signal, + viewChild, +} from '@angular/core'; + import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { aas, AASDocument, AASEndpoint, QueryParser, stringFormat } from 'aas-core'; +import { AASEndpoint, QueryParser, stringFormat } from 'aas-core'; import { catchError, EMPTY, first, from, map, mergeMap, Observable, of } from 'rxjs'; import { AASTableComponent, AuthService, - ClipboardService, - CustomerFeedback, DownloadService, NotifyService, - SubmodelViewDescriptor, ViewMode, - ViewQuery, WindowService, - ZVEINameplate, - resolveSemanticId, - supportedSubmodelTemplates, + encodeBase64Url, + viewRoutes, } from 'aas-lib'; import { AddEndpointFormComponent } from './add-endpoint-form/add-endpoint-form.component'; @@ -39,7 +45,6 @@ import { FavoritesFormComponent } from './favorites-form/favorites-form.componen import { StartStore } from './start.store'; import { UpdateEndpointFormComponent } from './update-endpoint-form/update-endpoint-form.component'; import { ExtrasEndpointFormComponent } from './extras-endpoint-form/extras-endpoint-form.component'; -import { FormsModule } from '@angular/forms'; import { StartService } from './start.service'; @Component({ @@ -64,42 +69,31 @@ export class StartComponent implements OnDestroy { private readonly toolbar: ToolbarService, private readonly auth: AuthService, private readonly download: DownloadService, - private readonly clipboard: ClipboardService, private readonly favorites: FavoritesService, ) { if (this.store.viewMode === ViewMode.Undefined) { - this.auth.ready.pipe(first(ready => ready)).subscribe({ - next: () => this.viewMode.set(ViewMode.List), - error: error => this.notify.error(error), - }); + this.auth.ready.pipe(first(ready => ready)).subscribe(() => this.viewMode.set(ViewMode.List)); + } else { + this.service.restore(); } effect( () => { - const name = this.activeFavorites(); - if (this.viewMode() !== ViewMode.List) { - this.viewMode.set(ViewMode.List); - } else { - this.service.setActiveFavorites(name); - } + this.service.vieModeChange(this.store.viewMode$()); }, { allowSignalWrites: true }, ); effect( () => { - this.limit(); - if (!this.store.activeFavorites) { - this.service.refreshPage(); - } + this.service.activeFavoritesChange(this.activeFavorites()); }, { allowSignalWrites: true }, ); effect( () => { - const viewMode = this.viewMode(); - this.service.setViewMode(viewMode); + this.service.limitChange(this.limit()); }, { allowSignalWrites: true }, ); @@ -136,21 +130,13 @@ export class StartComponent implements OnDestroy { public readonly isLastPage = computed(() => this.store.next$() === null); - public readonly documents = this.store.documents$; + public readonly documents = this.store.documents$.asReadonly(); public readonly selected = this.store.selected$; - public readonly canDownloadDocument = computed(() => (this.store.selected$().length > 0 ? true : false)); - - public readonly canDeleteDocument = computed(() => this.store.selected$().length > 0); + public readonly someSelected = computed(() => this.store.selected$().length > 0); - public readonly canViewUserFeedback = computed(() => - this.store.selected$().some(document => this.selectSubmodels(document, CustomerFeedback).length === 1), - ); - - public readonly canViewNameplate = computed(() => - this.store.selected$().some(document => this.selectSubmodels(document, ZVEINameplate).length === 1), - ); + public readonly views = signal(viewRoutes).asReadonly(); public ngOnDestroy(): void { this.toolbar.clear(); @@ -288,50 +274,21 @@ export class StartComponent implements OnDestroy { ); } - public viewUserFeedback(): void { - const descriptor: SubmodelViewDescriptor = { - template: supportedSubmodelTemplates.get(CustomerFeedback), - submodels: [], - }; - - for (const document of this.store.selected) { - const submodels = this.selectSubmodels(document, CustomerFeedback); - if (submodels.length === 1) { - descriptor.submodels.push({ - id: document.id, - endpoint: document.endpoint, - idShort: submodels[0].idShort, - }); - } - } - - if (descriptor.submodels.length > 0) { - this.clipboard.set('ViewQuery', { descriptor } as ViewQuery); - this.router.navigateByUrl('/view?format=ViewQuery', { skipLocationChange: true }); - } - } - - public viewNameplate(): void { - const descriptor: SubmodelViewDescriptor = { - template: supportedSubmodelTemplates.get(ZVEINameplate), - submodels: [], - }; - - for (const document of this.store.selected) { - const submodels = this.selectSubmodels(document, ZVEINameplate); - if (submodels.length === 1) { - descriptor.submodels.push({ - id: document.id, - endpoint: document.endpoint, - idShort: submodels[0].idShort, - }); - } + public openView(view: Route): Promise { + const documents = this.store.selected; + if (documents.length === 1) { + return this.router.navigate([`/view/${view.path}`], { + queryParams: { + endpoint: encodeBase64Url(documents[0].endpoint), + id: encodeBase64Url(documents[0].id), + }, + state: { data: JSON.stringify(this.store.selected) }, + }); } - if (descriptor.submodels.length > 0) { - this.clipboard.set('ViewQuery', { descriptor } as ViewQuery); - this.router.navigateByUrl('/view?format=ViewQuery', { skipLocationChange: true }); - } + return this.router.navigate([`/view/${view.path}`], { + state: { data: JSON.stringify(documents) }, + }); } public setFilter(filter: string): void { @@ -380,8 +337,4 @@ export class StartComponent implements OnDestroy { }), ); } - - private selectSubmodels(document: AASDocument, semanticId: string): aas.Submodel[] { - return document.content?.submodels.filter(submodel => resolveSemanticId(submodel) === semanticId) ?? []; - } } diff --git a/projects/aas-portal/src/app/start/start.service.ts b/projects/aas-portal/src/app/start/start.service.ts index 812dec80..ed6bdf01 100644 --- a/projects/aas-portal/src/app/start/start.service.ts +++ b/projects/aas-portal/src/app/start/start.service.ts @@ -17,6 +17,10 @@ import { FavoritesService } from './favorites.service'; @Injectable() export class StartService { + private ignoreViewModeChange = false; + private ignoreActiveFavoritesChange = false; + private ignoreLimitChange = false; + public constructor( private readonly store: StartStore, private readonly api: StartApiService, @@ -24,37 +28,36 @@ export class StartService { private readonly translate: TranslateService, ) {} - public setActiveFavorites(name: string) { - this.store.activeFavorites$.set(name); - this.store.selected$.set([]); + public restore(): void { + this.ignoreViewModeChange = this.ignoreActiveFavoritesChange = this.ignoreLimitChange = true; + } - const viewMode = this.store.viewMode; - if (viewMode === ViewMode.List) { - const name = this.store.activeFavorites; - const favorites = this.favorites.get(name); - if (favorites) { - this.getFavorites(favorites.name, favorites.documents); - } else { - this.getFirstPage(); - } - } else if (viewMode === ViewMode.Tree) { - this.getTreeView(this.store.selected); + public vieModeChange(viewMode: ViewMode): void { + if (this.ignoreViewModeChange) { + this.ignoreViewModeChange = false; + return; } + + this.initialize(viewMode, this.store.activeFavorites); } - public setViewMode(viewMode: ViewMode) { - this.store.documents$.set([]); - this.store.viewMode$.set(viewMode); - if (viewMode === ViewMode.List) { - const name = this.store.activeFavorites; - const favorites = this.favorites.get(name); - if (favorites) { - this.getFavorites(favorites.name, favorites.documents); - } else { - this.getFirstPage(); - } - } else if (viewMode === ViewMode.Tree) { - this.getTreeView(this.store.selected); + public activeFavoritesChange(name: string): void { + if (this.ignoreActiveFavoritesChange) { + this.ignoreActiveFavoritesChange = false; + return; + } + + this.initialize(this.store.viewMode, name); + } + + public limitChange(limit: number): void { + if (this.ignoreLimitChange) { + this.ignoreLimitChange = false; + return; + } + + if (!this.store.activeFavorites) { + this.refreshPage(limit); } } @@ -142,7 +145,22 @@ export class StartService { .subscribe(); } - public refreshPage(): void { + private initialize(viewMode: ViewMode, name: string): void { + if (viewMode === ViewMode.List) { + this.store.selected$.set([]); + const favorites = this.favorites.get(name); + if (favorites) { + this.getFavorites(favorites.name, favorites.documents); + } else { + this.getFirstPage(); + } + } else if (viewMode === ViewMode.Tree) { + this.store.documents$.set([]); + this.getTreeView(this.store.selected); + } + } + + private refreshPage(limit: number): void { if (this.store.documents.length === 0) { return; } @@ -151,7 +169,7 @@ export class StartService { .getPage( { next: this.getId(this.store.documents[0]), - limit: this.store.limit, + limit, }, this.store.filterText, this.translate.currentLang, diff --git a/projects/aas-portal/src/app/start/start.store.ts b/projects/aas-portal/src/app/start/start.store.ts index 5f844bfd..1814b602 100644 --- a/projects/aas-portal/src/app/start/start.store.ts +++ b/projects/aas-portal/src/app/start/start.store.ts @@ -8,7 +8,7 @@ import { Injectable, signal, untracked } from '@angular/core'; import { first, mergeMap, Observable } from 'rxjs'; -import { AASDocument, AASDocumentId, equalArray } from 'aas-core'; +import { AASDocument, AASDocumentId } from 'aas-core'; import { AuthService, ViewMode } from 'aas-lib'; type StartState = { @@ -63,7 +63,7 @@ export class StartStore { public readonly viewMode$ = signal(initialState.viewMode); - public readonly documents$ = signal(initialState.documents, { equal: (a, b) => equalArray(a, b) }); + public readonly documents$ = signal(initialState.documents); public readonly activeFavorites$ = signal(initialState.activeFavorites); @@ -71,7 +71,7 @@ export class StartStore { public readonly filterText$ = signal(initialState.filterText); - public readonly selected$ = signal(initialState.selected, { equal: (a, b) => equalArray(a, b) }); + public readonly selected$ = signal(initialState.selected); public readonly previous$ = signal(initialState.previous); diff --git a/projects/aas-portal/src/app/view/view-api.service.ts b/projects/aas-portal/src/app/view/view-api.service.ts deleted file mode 100644 index afaf1533..00000000 --- a/projects/aas-portal/src/app/view/view-api.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, - * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft - * zur Foerderung der angewandten Forschung e.V. - * - *****************************************************************************/ - -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { AASDocument } from 'aas-core'; -import { Observable } from 'rxjs'; -import { encodeBase64Url } from 'aas-lib'; - -/** The client side AAS provider service. */ -@Injectable({ - providedIn: 'root', -}) -export class ViewApiService { - public constructor(private readonly http: HttpClient) {} - - /** - * Gets the referenced AAS document. - * @param endpoint The endpoint name. - * @param id The AAS identification. - * @returns The AAS document. - */ - public getDocument(endpoint: string, id: string): Observable { - return this.http.get( - `/api/v1/containers/${encodeBase64Url(endpoint)}/documents/${encodeBase64Url(id)}`, - ); - } -} diff --git a/projects/aas-portal/src/app/view/view.component.html b/projects/aas-portal/src/app/view/view.component.html index a30052e8..aab0956d 100644 --- a/projects/aas-portal/src/app/view/view.component.html +++ b/projects/aas-portal/src/app/view/view.component.html @@ -7,17 +7,7 @@ !---------------------------------------------------------------------------->
- @switch (template()) { - @case('Nameplate'){ - - } - @case ('CustomerFeedback') { - - } - @default { - - } - } +
\ No newline at end of file diff --git a/projects/aas-portal/src/app/view/view.component.ts b/projects/aas-portal/src/app/view/view.component.ts index 7b5e2635..c2f95161 100644 --- a/projects/aas-portal/src/app/view/view.component.ts +++ b/projects/aas-portal/src/app/view/view.component.ts @@ -6,97 +6,35 @@ * *****************************************************************************/ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - OnDestroy, - OnInit, - TemplateRef, - ViewChild, -} from '@angular/core'; - -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { EMPTY, from, mergeMap, of, Subscription, toArray, zip } from 'rxjs'; +import { ChangeDetectionStrategy, Component, effect, OnDestroy, TemplateRef, viewChild } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; import { ToolbarService } from '../toolbar.service'; -import { ViewApiService } from './view-api.service'; -import { - ClipboardService, - CustomerFeedbackComponent, - DigitalNameplateComponent, - DocumentSubmodelPair, - SubmodelViewDescriptor, - ViewQuery, - ViewQueryParams, -} from 'aas-lib'; -import { ViewStore } from './view.store'; @Component({ selector: 'fhg-view', templateUrl: './view.component.html', styleUrls: ['./view.component.scss'], standalone: true, - imports: [DigitalNameplateComponent, CustomerFeedbackComponent, TranslateModule], + imports: [RouterOutlet], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ViewComponent implements OnInit, AfterViewInit, OnDestroy { - private readonly subscription = new Subscription(); - - public constructor( - private readonly route: ActivatedRoute, - private readonly api: ViewApiService, - private readonly store: ViewStore, - private readonly clipboard: ClipboardService, - private readonly toolbar: ToolbarService, - ) {} - - @ViewChild('viewToolbar', { read: TemplateRef }) - public viewToolbar: TemplateRef | null = null; - - public readonly template = this.store.template$.asReadonly(); - - public readonly submodels = this.store.submodels$.asReadonly(); - - public ngOnInit(): void { - let query: ViewQuery | undefined; - const params = this.route.snapshot.queryParams as ViewQueryParams; - if (params.format) { - query = this.clipboard.get(params.format); - } - - if (query?.descriptor) { - const descriptor: SubmodelViewDescriptor = query.descriptor; - zip( - of(descriptor.template), - from(descriptor.submodels).pipe( - mergeMap(item => zip(this.api.getDocument(item.endpoint, item.id), of(item.idShort))), - mergeMap(tuple => { - const submodel = tuple[0].content?.submodels.find(item => item.idShort === tuple[1]); - if (submodel?.modelType === 'Submodel') { - return of({ document: tuple[0], submodel } as DocumentSubmodelPair); - } - - return EMPTY; - }), - toArray(), - ), - ).subscribe(tuple => { - this.store.submodels$.set(tuple[1]); - this.store.template$.set(tuple[0]); - }); - } +export class ViewComponent implements OnDestroy { + public constructor(private readonly toolbar: ToolbarService) { + effect( + () => { + const viewToolbar = this.viewToolbar(); + if (viewToolbar) { + this.toolbar.set(viewToolbar); + } + }, + { allowSignalWrites: true }, + ); } - public ngAfterViewInit(): void { - if (this.viewToolbar) { - this.toolbar.set(this.viewToolbar); - } - } + public readonly viewToolbar = viewChild>('viewToolbar'); public ngOnDestroy(): void { - this.subscription.unsubscribe(); this.toolbar.clear(); } } diff --git a/projects/aas-portal/src/app/view/view.store.ts b/projects/aas-portal/src/app/view/view.store.ts deleted file mode 100644 index f7385903..00000000 --- a/projects/aas-portal/src/app/view/view.store.ts +++ /dev/null @@ -1,37 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, - * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft - * zur Foerderung der angewandten Forschung e.V. - * - *****************************************************************************/ - -import { Injectable, signal, untracked } from '@angular/core'; -import { DocumentSubmodelPair } from 'aas-lib'; - -type ViewState = { - template: string | undefined; - submodels: DocumentSubmodelPair[]; -}; - -const initialState: ViewState = { - template: undefined, - submodels: [], -}; - -@Injectable({ - providedIn: 'root', -}) -export class ViewStore { - public template$ = signal(initialState.template); - - public submodels$ = signal(initialState.submodels); - - public get template(): string | undefined { - return untracked(this.template$); - } - - public get submodels(): DocumentSubmodelPair[] { - return untracked(this.submodels$); - } -} diff --git a/projects/aas-portal/src/assets/i18n/de-de.json b/projects/aas-portal/src/assets/i18n/de-de.json index adbe6767..941bedf4 100644 --- a/projects/aas-portal/src/assets/i18n/de-de.json +++ b/projects/aas-portal/src/assets/i18n/de-de.json @@ -44,8 +44,6 @@ "CMD_TOGGLE_MESSAGES": "Meldungen", "CMD_TOGGLE_PACKAGES": "Packages", "CMD_DELETE_USER": "Benutzerkonto löschen...", - "CMD_VIEW_USER_FEEDBACK": "Nutzer-Feedback", - "CMD_VIEW_NAMEPLATE": "Typenschild", "CMD_SHOW_LICENSE_TEXT": "Lizenztext anzeigen", "CMD_HIDE_LICENSE_TEXT": "Lizenztext verbergen", "COLUMN_VARIANT": "Variante", @@ -168,7 +166,10 @@ "UPDATE_ENDPOINT": "Bearbeiten...", "REMOVE_ENDPOINT": "Entfernen...", "EXTRAS": "Extras...", - "PLACEHOLDER_FILTER": "Filter" + "PLACEHOLDER_FILTER": "Filter", + "DigitalPassportPortal": "DPPPortal", + "CustomerFeedback": "Nutzer-Feedback", + "Nameplate": "Typenschild" }, "AAS": { "PLACEHOLDER_SEARCH": "Suche" @@ -262,7 +263,7 @@ "TERM_EXPECTED": "Term erwartet an Position {0}.", "UNEXPECTED_CLOSING_BRACKET": "Unerwartete schließende Klammer an Position ${0}." }, - "IndexChangeService": { + "Main": { "ENDPOINTS": "Endpunkte", "SHELLS": "Verwaltungsschalen" }, @@ -272,5 +273,12 @@ "COUNT": "{0} Favorit(en)", "ERROR_FAVORITES_LIST_NAME_USED_SEVERAL_TIMES": "'{0}' ist mehrfach verwendet.", "ERROR_INVALID_EMPTY_FAVORITES_LIST_NAME": "Ungültiger leerer Name." - } + }, + "DigitalPassportPortal": { + "EMPTY": "Ups...", + "BANNER": "Fraunhofer IOSB-INA DPPPortal", + "CAPTION": "Product Passport", + "TYPE": "Type", + "SERIAL_NUMBER": "Seriennummer" + } } \ No newline at end of file diff --git a/projects/aas-portal/src/assets/i18n/en-us.json b/projects/aas-portal/src/assets/i18n/en-us.json index 07168dba..fcfae95c 100644 --- a/projects/aas-portal/src/assets/i18n/en-us.json +++ b/projects/aas-portal/src/assets/i18n/en-us.json @@ -44,8 +44,6 @@ "CMD_TOGGLE_MESSAGES": "Messages", "CMD_TOGGLE_PACKAGES": "Packages", "CMD_DELETE_USER": "Delete user account...", - "CMD_VIEW_USER_FEEDBACK": "User feedback", - "CMD_VIEW_NAMEPLATE": "Nameplate", "CMD_SHOW_LICENSE_TEXT": "Show license text", "CMD_HIDE_LICENSE_TEXT": "Hide license text", "COLUMN_NAME": "Name", @@ -169,7 +167,10 @@ "UPDATE_ENDPOINT": "Edit...", "REMOVE_ENDPOINT": "Remove...", "EXTRAS": "Extras...", - "PLACEHOLDER_FILTER": "Filter" + "PLACEHOLDER_FILTER": "Filter", + "DigitalPassportPortal": "DPPPortal", + "CustomerFeedback": "User feedback", + "Nameplate": "Nameplate" }, "AAS": { "PLACEHOLDER_SEARCH": "Search" @@ -263,7 +264,7 @@ "TERM_EXPECTED": "Term expected at position {0}.", "UNEXPECTED_CLOSING_BRACKET": "Unexpected closing bracket at position ${0}." }, - "IndexChangeService": { + "Main": { "ENDPOINTS": "Endpoints", "SHELLS": "Shells" }, @@ -273,6 +274,12 @@ "COUNT": "{0} favorite(s)", "ERROR_FAVORITES_LIST_NAME_USED_SEVERAL_TIMES": "'{0}' is used several times.", "ERROR_INVALID_EMPTY_FAVORITES_LIST_NAME": "Invalid empty name." - } - + }, + "DigitalPassportPortal": { + "EMPTY": "Ups...", + "BANNER": "Fraunhofer IOSB-INA DPPPortal", + "CAPTION": "Product Passport", + "TYPE": "Type", + "SERIAL_NUMBER": "Serial No." + } } \ No newline at end of file diff --git a/projects/aas-portal/src/test/main/main-api.service.spec.ts b/projects/aas-portal/src/test/main/main-api.service.spec.ts deleted file mode 100644 index a888c592..00000000 --- a/projects/aas-portal/src/test/main/main-api.service.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, - * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft - * zur Foerderung der angewandten Forschung e.V. - * - *****************************************************************************/ - -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { createDocument } from '../assets/test-document'; -import { MainApiService } from '../../app/main/main-api.service'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('MainApiService', function () { - let service: MainApiService; - let httpTestingController: HttpTestingController; - - beforeEach(function () { - - TestBed.configureTestingModule({ - declarations: [], - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); - - service = TestBed.inject(MainApiService); - httpTestingController = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpTestingController.verify(); - }); - - it('should created', function () { - expect(service).toBeTruthy(); - }); - - describe('getDocument', () => { - it('/api/v1/documents/:id', function () { - const document = createDocument('document1'); - - service.getDocument('document1').subscribe(value => { - expect(value).toEqual(document); - }); - - const req = httpTestingController.expectOne('/api/v1/documents/ZG9jdW1lbnQx'); - expect(req.request.method).toEqual('GET'); - req.flush(document); - }); - }); -}); \ No newline at end of file diff --git a/projects/aas-portal/src/test/main/main.component.spec.ts b/projects/aas-portal/src/test/main/main.component.spec.ts index 4dfff6e7..5e2f6aa2 100644 --- a/projects/aas-portal/src/test/main/main.component.spec.ts +++ b/projects/aas-portal/src/test/main/main.component.spec.ts @@ -10,12 +10,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, input, Signal, signal } from '@angular/core'; import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { provideRouter } from '@angular/router'; -import { Subject } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { AASDocument } from 'aas-core'; -import { AuthComponent, IndexChangeService, LocalizeComponent, NotifyComponent, WindowService } from 'aas-lib'; +import { + AuthComponent, + AuthService, + IndexChangeService, + LocalizeComponent, + NotifyComponent, + WindowService, +} from 'aas-lib'; import { MainComponent } from '../../app/main/main.component'; -import { MainApiService } from '../../app/main/main-api.service'; import { ToolbarService } from '../../app/toolbar.service'; @Component({ @@ -45,28 +51,27 @@ describe('MainComponent', () => { let component: MainComponent; let fixture: ComponentFixture; let documentSubject: Subject; - let api: jasmine.SpyObj; let window: jasmine.SpyObj; let toolbar: jasmine.SpyObj; let indexChange: jasmine.SpyObj; + let auth: jasmine.SpyObj; beforeEach(() => { documentSubject = new Subject(); documentSubject.next(null); - api = jasmine.createSpyObj('ProjectService', ['getDocument']); window = jasmine.createSpyObj(['getQueryParams']); window.getQueryParams.and.returnValue(new URLSearchParams()); toolbar = jasmine.createSpyObj(['set', 'clear'], { toolbarTemplate: signal(null) }); indexChange = jasmine.createSpyObj(['clear'], { - summary: (() => 'Hallo') as Signal, + documentCount: (() => 42) as Signal, + endpointCount: (() => 1) as Signal, + changedDocuments: (() => 0) as Signal, }); + auth = jasmine.createSpyObj({}, { ready: of(true) }); + TestBed.configureTestingModule({ providers: [ - { - provide: MainApiService, - useValue: api, - }, { provide: WindowService, useValue: window, @@ -79,6 +84,10 @@ describe('MainComponent', () => { provide: IndexChangeService, useValue: indexChange, }, + { + provide: AuthService, + useValue: auth, + }, provideRouter([]), ], imports: [ diff --git a/projects/aas-portal/src/test/view/view-api.service.spec.ts b/projects/aas-portal/src/test/view/view-api.service.spec.ts deleted file mode 100644 index b7359f72..00000000 --- a/projects/aas-portal/src/test/view/view-api.service.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, - * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft - * zur Foerderung der angewandten Forschung e.V. - * - *****************************************************************************/ - -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { createDocument } from '../assets/test-document'; -import { ViewApiService } from '../../app/view/view-api.service'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('ViewApiService', function () { - let service: ViewApiService; - let httpTestingController: HttpTestingController; - - beforeEach(function () { - - TestBed.configureTestingModule({ - declarations: [], - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); - - service = TestBed.inject(ViewApiService); - httpTestingController = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpTestingController.verify(); - }); - - it('should created', function () { - expect(service).toBeTruthy(); - }); - - describe('getDocument', () => { - it('/api/v1/documents/:id', function () { - const document = createDocument('document1'); - - service.getDocument('Samples', 'document1').subscribe(value => { - expect(value).toEqual(document); - }); - - const req = httpTestingController.expectOne('/api/v1/containers/U2FtcGxlcw/documents/ZG9jdW1lbnQx'); - expect(req.request.method).toEqual('GET'); - req.flush(document); - }); - }); -}); \ No newline at end of file diff --git a/projects/aas-portal/src/test/view/view.component.spec.ts b/projects/aas-portal/src/test/view/view.component.spec.ts index c488c3e8..befa214e 100644 --- a/projects/aas-portal/src/test/view/view.component.spec.ts +++ b/projects/aas-portal/src/test/view/view.component.spec.ts @@ -7,64 +7,16 @@ *****************************************************************************/ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { of } from 'rxjs'; -import { ClipboardService, SubmodelViewDescriptor, ViewQuery } from 'aas-lib'; -import { sampleDocument } from '../../test/assets/sample-document'; import { ViewComponent } from '../../app/view/view.component'; -import { ViewApiService } from '../../app/view/view-api.service'; describe('ViewComponent', () => { let component: ViewComponent; let fixture: ComponentFixture; - let api: jasmine.SpyObj; - let route: jasmine.SpyObj; - let clipboard: ClipboardService; beforeEach(() => { - api = jasmine.createSpyObj('ProjectService', ['getDocument']); - api.getDocument.and.returnValue(of(sampleDocument)); - - const descriptor: SubmodelViewDescriptor = { - template: 'Nameplate', - submodels: [ - { - id: 'http://customer.com/aas/9175_7013_7091_9168', - endpoint: 'C:\\Git\\AASPortal\\data\\endpoints\\samples', - idShort: 'Identification', - }, - ], - }; - - route = jasmine.createSpyObj( - 'ActivatedRoute', - {}, - { - snapshot: jasmine.createSpyObj( - 'ActivatedRouteSnapshot', - {}, - { - queryParams: { - format: 'ViewQuery', - }, - }, - ), - }, - ); - TestBed.configureTestingModule({ - providers: [ - { - provide: ViewApiService, - useValue: api, - }, - { - provide: ActivatedRoute, - useValue: route, - }, - ], imports: [ TranslateModule.forRoot({ loader: { @@ -75,8 +27,6 @@ describe('ViewComponent', () => { ], }); - clipboard = TestBed.inject(ClipboardService); - clipboard.set('ViewQuery', { descriptor } as ViewQuery); fixture = TestBed.createComponent(ViewComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -85,15 +35,4 @@ describe('ViewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - // it('provides a view for a specific template view', function (done: DoneFn) { - // store - // .select(state => state.view) - // .pipe(first()) - // .subscribe(state => { - // expect(state.submodels[0].submodel.idShort).toEqual('Identification'); - // expect(component.submodels[0].submodel.idShort).toEqual('Identification'); - // done(); - // }); - // }); }); diff --git a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts index fd22c6cb..21d25c8d 100644 --- a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts +++ b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts @@ -591,7 +591,7 @@ export class MySqlIndex extends AASIndex { timestamp: Number(result.timestamp), content: null, onlineReady: true, - readonly: true, + readonly: false, }; if (result.assetId) { diff --git a/projects/aas-server/src/app/aas-provider/aas-provider.ts b/projects/aas-server/src/app/aas-provider/aas-provider.ts index 8d30ff96..2a23c44e 100644 --- a/projects/aas-server/src/app/aas-provider/aas-provider.ts +++ b/projects/aas-server/src/app/aas-provider/aas-provider.ts @@ -664,12 +664,12 @@ export class AASProvider { } private async onRemoved(result: ScanEndpointResult): Promise { - const document = result.document; - const endpoint = await this.index.findEndpoint(document.endpoint); + const endpoint = await this.index.findEndpoint(result.endpoint.name); if (endpoint === undefined || endpoint.schedule?.type === 'disabled') { return; } + const document = result.document; await this.index.remove(result.endpoint.name, document.id); this.cache.remove(document.endpoint, document.id); this.logger.info(`Removed: AAS ${document.idShort} [${document.id}] in ${result.endpoint.url}`); diff --git a/projects/aas-server/src/app/aas-provider/aas-resource-scan.ts b/projects/aas-server/src/app/aas-provider/aas-resource-scan.ts index fe92ac4d..84298cbb 100644 --- a/projects/aas-server/src/app/aas-provider/aas-resource-scan.ts +++ b/projects/aas-server/src/app/aas-provider/aas-resource-scan.ts @@ -76,7 +76,7 @@ export abstract class AASResourceScan extends EventEmitter { this.emit('add', value.document); } else if (endOfEndpoint && value.reference) { keys.push(value.reference.id); - this.emit('remove', value.document); + this.emit('remove', value.reference); } } @@ -89,7 +89,7 @@ export abstract class AASResourceScan extends EventEmitter { } else if (value.document) { this.emit('add', value.document); } else if (value.reference) { - this.emit('remove', value.document); + this.emit('remove', value.reference); } } } finally { diff --git a/projects/aas-server/src/app/controller/containers-controller.ts b/projects/aas-server/src/app/controller/containers-controller.ts index 3b83784b..80bef6a0 100644 --- a/projects/aas-server/src/app/controller/containers-controller.ts +++ b/projects/aas-server/src/app/controller/containers-controller.ts @@ -136,27 +136,6 @@ export class ContainersController extends AASController { } } - /** - * @summary Gets the thumbnail of the specified AAS document. - * @param endpoint The endpoint name (Base64Url encoded). - * @param id The AAS identifier (Base64Url encoded). - * @returns The thumbnail of the current AAS document. - */ - @Get('{endpoint}/documents/{id}/thumbnail') - @Security('bearerAuth', ['guest']) - @OperationId('getDocumentThumbnail') - public async getDocumentThumbnail( - @Path() endpoint: string, - @Path() id: string, - ): Promise { - try { - this.logger.start('getDocumentThumbnail'); - return await this.aasProvider.getThumbnailAsync(decodeBase64Url(endpoint), decodeBase64Url(id)); - } finally { - this.logger.stop(); - } - } - /** * @summary Downloads the value of DataElement. * @param endpoint The URL of the AAS container (Base64Url encoded). diff --git a/projects/aas-server/src/app/controller/endpoints-controller.ts b/projects/aas-server/src/app/controller/endpoints-controller.ts index 07dd73c2..b5c96b0c 100644 --- a/projects/aas-server/src/app/controller/endpoints-controller.ts +++ b/projects/aas-server/src/app/controller/endpoints-controller.ts @@ -167,4 +167,25 @@ export class EndpointsController extends AASController { this.logger.stop(); } } + + /** + * @summary Gets the thumbnail of the specified AAS document. + * @param endpoint The endpoint name (Base64Url encoded). + * @param id The AAS identifier (Base64Url encoded). + * @returns The thumbnail of the current AAS document. + */ + @Get('{endpoint}/documents/{id}/thumbnail') + @Security('bearerAuth', ['guest']) + @OperationId('getThumbnail') + public async getThumbnail( + @Path() endpoint: string, + @Path() id: string, + ): Promise { + try { + this.logger.start('getThumbnail'); + return await this.aasProvider.getThumbnailAsync(decodeBase64Url(endpoint), decodeBase64Url(id)); + } finally { + this.logger.stop(); + } + } } diff --git a/projects/aas-server/src/app/packages/aas-resource.ts b/projects/aas-server/src/app/packages/aas-resource.ts index 5474fd0c..4f34b21e 100644 --- a/projects/aas-server/src/app/packages/aas-resource.ts +++ b/projects/aas-server/src/app/packages/aas-resource.ts @@ -67,10 +67,10 @@ export abstract class AASResource { /** * Delete an aasx package from the current source. - * @param aasIdentifier The AAS identifier. + * @param aasId The AAS identifier. * @param name The name of the package in the source. */ - public abstract deletePackageAsync(aasIdentifier: string, name: string): Promise; + public abstract deletePackageAsync(aasId: string, name: string): Promise; /** * Invokes the specified operation synchronously. diff --git a/projects/aas-server/src/app/packages/aas-server/aas-api-client-v3.ts b/projects/aas-server/src/app/packages/aas-server/aas-api-client-v3.ts index 96f287dc..a816a814 100644 --- a/projects/aas-server/src/app/packages/aas-server/aas-api-client-v3.ts +++ b/projects/aas-server/src/app/packages/aas-server/aas-api-client-v3.ts @@ -8,7 +8,8 @@ import FormData from 'form-data'; import cloneDeep from 'lodash-es/cloneDeep.js'; -import { createReadStream } from 'fs'; +import fs from 'fs'; +import path from 'path'; import { aas, AASEndpoint, @@ -198,17 +199,22 @@ export class AASApiClientV3 extends AASApiClient { return await this.http.getResponse(this.resolve(`packages/${packageId}`), this.endpoint.headers); } - public postPackageAsync(file: Express.Multer.File): Promise { + public async postPackageAsync(file: Express.Multer.File): Promise { const formData = new FormData(); - formData.append('file', createReadStream(file.path)); - formData.append('fileName', file.filename); - return this.http.post(this.resolve(`packages`), formData, this.endpoint.headers); + const aasxFile = path.join(path.dirname(file.path), file.originalname); + if (fs.existsSync(aasxFile)) { + await fs.promises.unlink(aasxFile); + } + + await fs.promises.rename(file.path, path.join(path.dirname(file.path), file.originalname)); + formData.append('file', fs.createReadStream(aasxFile)); + const result = await this.http.post(this.resolve(`packages`), formData, this.endpoint.headers); + return result; } - public async deletePackageAsync(aasIdentifier: string): Promise { - const aasId = encodeBase64Url(aasIdentifier); + public async deletePackageAsync(aasId: string): Promise { const descriptors: PackageDescriptor[] = await this.http.get( - this.resolve(`packages?aasId=${aasId}`), + this.resolve(`packages?aasId=${encodeBase64Url(aasId)}`), this.endpoint.headers, );