diff --git a/.env.local b/.env.local
index 3991d06..4269600 100644
--- a/.env.local
+++ b/.env.local
@@ -2,6 +2,7 @@ APPLICATION_ID_URI: 'api://mnestix-test-web-api/'
LOCK_TIMESERIES_PERIOD_FEATURE_FLAG: true
COMPARISON_FEATURE_FLAG: true
AAS_LIST_FEATURE_FLAG: true
+AAS_LIST_V2_FEATURE_FLAG: false
TRANSFER_FEATURE_FLAG: false
AAS_REPO_API_URL: 'http://localhost:5064/repo'
SUBMODEL_REPO_API_URL: 'http://localhost:5064/repo'
@@ -23,4 +24,7 @@ KEYCLOAK_CLIENT_ID: "mnestix-browser-client-demo"
KEYCLOAK_LOCAL_URL: ""
KEYCLOAK_REALM: "BaSyx"
KEYCLOAK_ISSUER: "http://localhost:8080"
-NEXTAUTH_URL: http://localhost:3000
\ No newline at end of file
+NEXTAUTH_URL: http://localhost:3000
+
+IMPRINT_URL:
+DATA_PRIVACY_URL:
diff --git a/.github/workflows/autoAddReviewer.yaml b/.github/workflows/autoAddReviewer.yaml
index 70f009c..fba26c8 100644
--- a/.github/workflows/autoAddReviewer.yaml
+++ b/.github/workflows/autoAddReviewer.yaml
@@ -1,15 +1,17 @@
name: Auto Add Reviewer
-on:
+on:
pull_request:
- types: [opened]
-
+ types: [ opened ]
+
jobs:
build:
+ permissions:
+ pull-requests: write
runs-on: ubuntu-latest
steps:
- - name: Add Reviewers
- uses: madrapps/add-reviewers@v1
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- reviewers: GailMelanie,hofermo,JonathanXITASO,pawel-baran-se,XAlinaGS,Xmilofranke,NilsXitaso,FranzXitaso,XAndreJentzsch,fleanegan
+ - name: Add Reviewers
+ uses: madrapps/add-reviewers@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ reviewers: GailMelanie,hofermo,JonathanXITASO,pawel-baran-se,XAlinaGS,Xmilofranke,NilsXitaso
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
index 1653207..4c98fad 100644
--- a/.github/workflows/docker-build.yml
+++ b/.github/workflows/docker-build.yml
@@ -12,13 +12,9 @@ env:
IMAGE_NAME: mnestix-browser
IMAGE_TAG: latest
# Update the version manually
- IMAGE_TAG_VERSION: 1.3.0
- AD_CLIENT_ID: ${{ secrets.AD_CLIENT_ID }}
- AD_TENANT_ID: ${{ secrets.AD_TENANT_ID }}
- REGISTRY_USER: ${{ vars.REGISTRY_USER }}
- REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
- AZURE_REGISTRY_PASS: ${{ secrets.AZURE_REGISTRY_PASS }}
- AZURE_REGISTRY_USER: ${{ vars.AZURE_REGISTRY_USER }}
+ IMAGE_TAG_VERSION: 1.3.1
+ REGISTRY_USER: ${{ secrets.DOCKER_USERNAME }}
+ REGISTRY_PASS: ${{ secrets.DOCKER_API_TOKEN }}
jobs:
default:
@@ -79,6 +75,5 @@ jobs:
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/staging'
env:
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
- run: docker tag mnestix/$IMAGE_NAME mnestixcr.azurecr.io/$IMAGE_NAME:$BRANCH_NAME &&
- docker login -u $AZURE_REGISTRY_USER -p $AZURE_REGISTRY_PASS mnestixcr.azurecr.io &&
- docker push mnestixcr.azurecr.io/$IMAGE_NAME:$BRANCH_NAME
+ run: docker tag mnestix/$IMAGE_NAME mnestix/$IMAGE_NAME:$BRANCH_NAME &&
+ docker push mnestix/$IMAGE_NAME:$BRANCH_NAME
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 72cc7b7..de50ceb 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -32,7 +32,7 @@
-
+
@@ -42,7 +42,7 @@
-
+
diff --git a/README.md b/README.md
index d19a07e..27f8ee8 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,18 @@
-
-
-
+
+
+
+
+
Mnestix
[![Made by XITASO](https://img.shields.io/badge/Made_by_XITASO-005962?style=flat-square)](https://xitaso.com/)
[![MIT License](https://img.shields.io/badge/License-MIT-005962.svg?style=flat-square)](https://choosealicense.com/licenses/mit/)
[![Yarn License](https://img.shields.io/badge/YARN-V1.22.22-005962?style=flat-square)]()
-[![Join our Community](https://img.shields.io/badge/Join_our_Community-005962?style=flat-square)](https://mnestix.io/)
+[![Join our Community](https://img.shields.io/badge/Join_our_Community-005962?style=flat-square)](https://xitaso.com/kompetenzen/mnestix/#support)
### Welcome to the Mnestix Community!
@@ -22,7 +27,7 @@ implementation of standardized digital twins. It opens the way for use cases suc
You can find a demo [here](https://mnestix-prod.azurewebsites.net/).
Some screenshots can be found in the [screenshots folder](screenshots).
-### **If you need support feel free to contact us through our website [Mnestix.io](https://mnestix.io/).**
+### **If you need support feel free to contact us through our website [here](https://xitaso.com/kompetenzen/mnestix/#support).**
## Quickstart
@@ -276,6 +281,7 @@ Mnestix provides the following configuration options. You can adapt the values i
| `SUBMODEL_REPO_API_URL` | | Default Submodel Repository to display when Submodel Id is not in Submodel Registry | required |
| `MNESTIX_BACKEND_API_URL` | | Mnestix Backend with a lot of business comfort features like the Repository-Proxy or the Template builder | optional |
| `AAS_LIST_FEATURE_FLAG` | false | Enables or disables the AasList in the frontend. This only works in combination with `Features__AllowRetrievingAllShellsAndSubmodels` being set to `true` (Needs the Mnestix Backend to work) | optional |
+| `AAS_LIST_V2_FEATURE_FLAG` | false | The functionality controlled by this flag is under active development and may change without notice. Please see [details](#aas-list-v2-feature-details) | optional |
| `TRANSFER_FEATURE_FLAG` | false | Enables or disables the Transfer Feature in the frontend. If enabled, it is possible to import a viewed AAS to a configured repository. This feature is currently being developed. | optional |
| `AUTHENTICATION_FEATURE_FLAG` | false | Enable or disable the authentication in the frontend. (Needs the Mnestix Backend to work) | optional |
| `COMPARISON_FEATURE_FLAG` | false | Enables or disables the comparison feature. | optional |
@@ -289,6 +295,8 @@ Mnestix provides the following configuration options. You can adapt the values i
| `KEYCLOAK_ISSUER` | | Configuration variable that specifies the URL of the Keycloak servers issuer endpoint. This endpoint provides the base URL for the Keycloak server that issues tokens and handles authentication requests | optional |
| `KEYCLOAK_LOCAL_URL` | | Optional configuration variable specifically used for development environments within Docker. This allows your application to connect to a Keycloak instance running in a Docker container | optional |
| `KEYCLOAK_REALM` | BaSyx | Configuration variable that specifies the name of the Keycloak realm your application will use for authentication and authorization. | optional |
+| `IMPRINT_URL` | | Address that will be used in the imprint link. Will only show the link, if a value has been set. | optional |
+| `DATA_PRIVACY_URL` | | Address that will be used in the data privacy link. Will only show the link, if a value has been set. | optional |
### How to set a custom logo
@@ -543,6 +551,18 @@ backend environment variable `Features__AllowRetrievingAllShellsAndSubmodels: fa
Remember that this also means that the functionality to list all AAS won't work anymore in the Mnestix Browser, so
disable this functionality with the environment variable `AAS_LIST_FEATURE_FLAG: false`.
+#### AAS List V2 Feature Details
+
+The `AAS_LIST_V2_FEATURE_FLAG` is a feature flag introduced as part of a preview release.
+It enables access to an updated list implementation that operates independently of the Mnestix API.
+
+This flag is currently disabled by default (false) and is available only in preview.
+It is not yet recommended for production environments.
+
+To enable the feature for testing or preview purposes, set the flag to true in your configuration.
+
+#### AzureAD Service
+
One can also add an AzureAD Service to give people deeper access, this enables the "Login" Button in the Mnestix
Browser.
After logging in, users have access to even more functionality.
@@ -707,7 +727,7 @@ Right now, we are building a community around Mnestix.
If you are looking for a way to support us, you can start contributing to Mnestix right away.
For this purpose, issues which are particularly suitable for a first contribution are labeled with the tag
`good first issue`.
-If this is your first time contributing to a github project, we recommend having a look at this
-guide: [Contributing to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project).
-We would be more than happy to have you onboard. If there is anything you want to know, feel free to contact
-us [mnestix@xitaso.com](mailto:mnestix@xitaso.com).
+If this is your first time contributing to an eclipse project, we recommend having a look at this
+guide: [Contributing to a Eclipse project](https://www.eclipse.org/contribute/).
+We would be more than happy to have you on board. If there is anything you want to know, feel free to contact
+us [mnestix@xitaso.com](mailto:mnestix@xitaso.com) or through our website [here](https://xitaso.com/kompetenzen/mnestix/#support).
diff --git a/__mocks__/svg.tsx b/__mocks__/svg.tsx
new file mode 100644
index 0000000..87ff5a0
--- /dev/null
+++ b/__mocks__/svg.tsx
@@ -0,0 +1,7 @@
+import { forwardRef, SVGProps } from 'react';
+
+// eslint-disable-next-line react/display-name
+const SvgrMock = forwardRef>((props, ref) => );
+
+export const ReactComponent = SvgrMock;
+export default SvgrMock;
diff --git a/compose.yml b/compose.yml
index 7a8734b..b31adf2 100644
--- a/compose.yml
+++ b/compose.yml
@@ -24,11 +24,14 @@ services:
SUBMODEL_REPO_API_URL: 'http://mnestix-api:5064/repo'
MNESTIX_BACKEND_API_URL: 'http://mnestix-api:5064'
AAS_LIST_FEATURE_FLAG: "true"
+ AAS_LIST_V2_FEATURE_FLAG: "false"
TRANSFER_FEATURE_FLAG: "false"
COMPARISON_FEATURE_FLAG: "true"
AUTHENTICATION_FEATURE_FLAG: "false"
LOCK_TIMESERIES_PERIOD_FEATURE_FLAG: "true"
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-verySecureNextAuthSecret}
+ IMPRINT_URL: ""
+ DATA_PRIVACY_URL: ""
depends_on:
aas-environment:
condition: service_healthy # only after the healthcheck in aas is successful, the mnestix container is being created
@@ -38,7 +41,7 @@ services:
- mnestix-database:/app/prisma/database
mnestix-api:
- image: mnestix/mnestix-api:1.2.3
+ image: mnestix/mnestix-api:1.2.4
container_name: mnestix-api
profiles: [ "", "backend", "tests" ]
ports:
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 0000000..6ef4371
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,2 @@
+import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom/jest-globals'
\ No newline at end of file
diff --git a/infrastructure/mainTemplate.json b/infrastructure/mainTemplate.json
deleted file mode 100644
index 0c0b329..0000000
--- a/infrastructure/mainTemplate.json
+++ /dev/null
@@ -1,790 +0,0 @@
-{
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "parameters": {
- "location": {
- "type": "string",
- "defaultValue": "WestEurope"
- },
- "randomId": {
- "type": "string",
- "defaultValue": "[take(guid(resourceGroup().id), 8)]"
- },
- "apiKey": {
- "type": "securestring",
- "defaultValue": "verySecureApiKey"
- },
- "tenantId": {
- "type": "securestring",
- "defaultValue": "-"
- },
- "frontendClientId": {
- "type": "securestring",
- "defaultValue": "-"
- },
- "applicationIdUri": {
- "type": "securestring",
- "defaultValue": "-"
- },
- "backendClientId": {
- "type": "securestring",
- "defaultValue": "-"
- },
- "assetAdministrationShellDatabaseName": {
- // This parameter exists only for validation purposes and should not be passed explicitly
- "type": "string",
- "defaultValue": "basyxdb"
- },
- "mnestixApiServiceName": {
- "defaultValue": "[concat('mnestix-api-', parameters('randomId'))]",
- "type": "string"
- },
- "mnestixBrowserServiceName": {
- "defaultValue": "[concat('mnestix-browser-', parameters('randomId'))]",
- "type": "string"
- }
- },
- "variables": {
- "virtualNetworkName": "[concat('mnestix-network-', parameters('randomId'))]",
- "databasePrivateEndpointName": "[concat('mnestix-database-private-endpoint-', parameters('randomId'))]",
- "repoPrivateEndpointName": "[concat('mnestix-basyx-private-endpoint-', parameters('randomId'))]",
- "discoveryPrivateEndpointName": "[concat('mnestix-discovery-basyx-private-endpoint-', parameters('randomId'))]",
- "databaseAccountName": "[concat('mnestix-cosmos-', parameters('randomId'))]",
- "serverFarmsName": "[concat('mnestix-app-service-plan-', parameters('randomId'))]",
- "repoAppServiceName": "[concat('mnestix-basyx-repo-', parameters('randomId'))]",
- "repoAppServiceUrl": "[uri(concat('https://', variables('repoAppServiceName'), '.azurewebsites.net'), '')]",
- "mnestixApiServiceName": "[parameters('mnestixApiServiceName')]",
- "mnestixApiName": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net'), '')]",
- "mnestixApiServiceUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net'), '')]",
- "mnestixApiProxyUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net', '/repo'), '')]",
- "mnestixApiDiscoveryProxyUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net', '/discovery'), '')]",
- "mnestixBrowserServiceName": "[parameters('mnestixBrowserServiceName')]",
- "mnestixBrowserUrl": "[uri(concat('https://', variables('mnestixBrowserServiceName'), '.azurewebsites.net'), '')]",
- "discoveryAppServiceName": "[concat('mnestix-basyx-discovery-', parameters('randomId'))]",
- "discoveryAppServiceUrl": "[uri(concat('https://', variables('discoveryAppServiceName'), '.azurewebsites.net'), '')]",
- "submodelRegistryAppServiceName": "[concat('mnestix-basyx-submodel-registry-', parameters('randomId'))]",
- "submodelRegistryAppServiceUrl": "[uri(concat('https://', variables('submodelRegistryAppServiceName'), '.azurewebsites.net'), '')]",
- "aasRegistryAppServiceName": "[concat('mnestix-basyx-aas-registry-', parameters('randomId'))]",
- "aasRegistryAppServiceUrl": "[uri(concat('https://', variables('aasRegistryAppServiceName'), '.azurewebsites.net'), '')]"
- },
- "resources": [
- {
- "type": "Microsoft.Network/virtualNetworks",
- "name": "[variables('virtualNetworkName')]",
- "apiVersion": "2022-05-01",
- "location": "[parameters('location')]",
- "tags": {},
- "properties": {
- "addressSpace": {
- "addressPrefixes": ["10.0.0.0/16"]
- },
- "subnets": [
- {
- "name": "private-endpoints",
- "properties": {
- "addressPrefix": "10.0.0.0/24"
- },
- "type": "Microsoft.Network/virtualNetworks/subnets"
- },
- {
- "name": "private-endpoint-consumers",
- "type": "Microsoft.Network/virtualNetworks/subnets",
- "properties": {
- "addressPrefix": "10.0.1.0/24",
- "delegations": [
- {
- "name": "delegation",
- "type": "Microsoft.Network/virtualNetworks/subnets/delegations",
- "properties": {
- "serviceName": "Microsoft.Web/serverfarms",
- "actions": ["Microsoft.Network/virtualNetworks/subnets/action"]
- }
- }
- ]
- }
- }
- ]
- },
- "dependsOn": ["[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]"]
- },
- {
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2018-09-01",
- "name": "privatelink.azurewebsites.net",
- "dependsOn": ["[resourceId('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"],
- "location": "global",
- "properties": {},
- "resources": [
- {
- "name": "vnet-link",
- "type": "virtualNetworkLinks",
- "apiVersion": "2018-09-01",
- "location": "global",
- "dependsOn": [
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
- ],
- "properties": {
- "virtualNetwork": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- },
- "registrationEnabled": false
- }
- }
- ]
- },
- {
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2018-09-01",
- "name": "privatelink.mongo.cosmos.azure.com",
- "dependsOn": ["[resourceId('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"],
- "location": "global",
- "properties": {},
- "resources": [
- {
- "name": "vnet-link",
- "type": "virtualNetworkLinks",
- "apiVersion": "2018-09-01",
- "location": "global",
- "dependsOn": [
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.mongo.cosmos.azure.com')]"
- ],
- "properties": {
- "virtualNetwork": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- },
- "registrationEnabled": false
- }
- }
- ]
- },
- {
- "type": "Microsoft.DocumentDB/databaseAccounts",
- "apiVersion": "2021-10-15",
- "name": "[variables('databaseAccountName')]",
- "location": "[parameters('location')]",
- "tags": {
- "defaultExperience": "Azure Cosmos DB for MongoDB API"
- },
- "kind": "MongoDB",
- "identity": {
- "type": "None"
- },
- "properties": {
- "publicNetworkAccess": "Disabled",
- "enableAutomaticFailover": false,
- "enableMultipleWriteLocations": false,
- "isVirtualNetworkFilterEnabled": false,
- "virtualNetworkRules": [],
- "disableKeyBasedMetadataWriteAccess": false,
- "enableFreeTier": false,
- "enableAnalyticalStorage": false,
- "analyticalStorageConfiguration": {
- "schemaType": "FullFidelity"
- },
- "databaseAccountOfferType": "Standard",
- "defaultIdentity": "FirstPartyIdentity",
- "networkAclBypass": "None",
- "disableLocalAuth": false,
- "consistencyPolicy": {
- "defaultConsistencyLevel": "Session",
- "maxIntervalInSeconds": 5,
- "maxStalenessPrefix": 100
- },
- "apiProperties": {
- "serverVersion": "4.0"
- },
- "locations": [
- {
- "locationName": "[parameters('location')]",
- "provisioningState": "Succeeded",
- "failoverPriority": 0,
- "isZoneRedundant": false
- }
- ],
- "cors": [],
- "capabilities": [
- {
- "name": "EnableMongo"
- },
- {
- "name": "DisableRateLimitingResponses"
- },
- {
- "name": "EnableServerless"
- }
- ],
- "ipRules": [],
- "backupPolicy": {
- "type": "Periodic",
- "periodicModeProperties": {
- "backupIntervalInMinutes": 240,
- "backupRetentionIntervalInHours": 168,
- "backupStorageRedundancy": "Geo"
- }
- },
- "networkAclBypassResourceIds": []
- },
- "dependsOn": ["[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"]
- },
- {
- "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases",
- "apiVersion": "2021-10-15",
- "name": "[concat(variables('databaseAccountName'), '/basyxdb')]",
- "properties": {
- "resource": {
- "id": "[parameters('assetAdministrationShellDatabaseName')]"
- }
- },
- "dependsOn": ["[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName'))]"]
- },
- {
- "name": "[variables('databasePrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2020-11-01",
- "location": "[parameters('location')]",
- "tags": {},
- "properties": {
- "privateLinkServiceConnections": [
- {
- "name": "[variables('databasePrivateEndpointName')]",
- "properties": {
- "privateLinkServiceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName'))]",
- "groupIds": ["MongoDB"],
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "description": "",
- "actionsRequired": "None"
- }
- },
- "type": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections"
- }
- ],
- "manualPrivateLinkServiceConnections": [],
- "subnet": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoints')]"
- }
- },
- "resources": [
- {
- "name": "default",
- "type": "privateDnsZoneGroups",
- "apiVersion": "2020-03-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "dnsConfig",
- "properties": {
- "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.mongo.cosmos.azure.com')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.mongo.cosmos.azure.com')]",
- "[resourceId('Microsoft.Network/privateEndpoints', variables('databasePrivateEndpointName'))]"
- ]
- }
- ],
- "dependsOn": [
- "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/serverfarms",
- "apiVersion": "2021-03-01",
- "name": "[variables('serverFarmsName')]",
- "location": "[parameters('location')]",
- "sku": {
- "name": "B2",
- "tier": "Basic",
- "size": "B2",
- "family": "B",
- "capacity": 1
- },
- "kind": "linux",
- "properties": {
- "name": "[variables('serverFarmsName')]",
- "reserved": true,
- "zoneRedundant": false,
- "targetWorkerCount": 1,
- "targetWorkerSizeId": 3
- }
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('repoAppServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('repoAppServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "BASYX_BACKEND",
- "value": "MongoDB"
- },
- {
- "name": "SPRING_DATA_MONGODB_URI",
- "value": "[first(listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName')), '2021-10-15').connectionStrings).connectionString]"
- },
- {
- "name": "SPRING_DATA_MONGODB_DATABASE",
- "value": "basyxdb"
- },
- {
- "name": "SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE",
- "value": "100000KB"
- },
- {
- "name": "SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE",
- "value": "100000KB"
- }
- ],
- "linuxFxVersion": "DOCKER|eclipsebasyx/aas-environment:2.0.0-SNAPSHOT",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('repoAppServiceUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "name": "[variables('repoPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2020-11-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateLinkServiceConnections": [
- {
- "name": "[variables('repoPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections",
- "properties": {
- "privateLinkServiceId": "[resourceId('Microsoft.Web/sites', variables('repoAppServiceName'))]",
- "groupIds": ["sites"],
- "requestMessage": "",
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "description": "",
- "actionsRequired": "None"
- }
- }
- }
- ],
- "manualPrivateLinkServiceConnections": [],
- "subnet": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoints')]"
- }
- },
- "resources": [
- {
- "name": "default",
- "type": "privateDnsZoneGroups",
- "apiVersion": "2020-03-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "dnsConfig",
- "properties": {
- "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]",
- "[resourceId('Microsoft.Network/privateEndpoints', variables('repoPrivateEndpointName'))]"
- ]
- }
- ],
- "dependsOn": [
- "[resourceId('Microsoft.Web/sites', variables('repoAppServiceName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('mnestixApiServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('mnestixApiServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "Features__AasRegistryMiddleware",
- "value": "true"
- },
- {
- "name": "Features__UseMongoDbBasedAasIdStorage",
- "value": "true"
- },
- {
- "name": "Features__AllowRetrievingAllShellsAndSubmodels",
- "value": "true"
- },
- {
- "name": "Features__UseAuthentication",
- "value": "true"
- },
- {
- "name": "ReverseProxy__Clusters__aasRepoCluster__Destinations__destination1__Address",
- "value": "[variables('repoAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__submodelRepoCluster__Destinations__destination1__Address",
- "value": "[variables('repoAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__discoveryCluster__Destinations__destination1__Address",
- "value": "[variables('discoveryAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__influxCluster__Destinations__destination1__Address",
- "value": "placeholder_for_influx_db_connection"
- },
- {
- "name": "ReverseProxy__Routes__InfluxRoute__Transforms__1__Set",
- "value": "Token"
- },
- {
- "name": "CustomerEndpointsSecurity__ApiKey",
- "value": "[parameters('apiKey')]"
- },
- {
- "name": "AzureAd__ClientId",
- "value": "[parameters('backendClientId')]"
- },
- {
- "name": "AzureAd__TenantId",
- "value": "[parameters('tenantId')]"
- },
- {
- "name": "ASPNETCORE_URLS",
- "value": "http://+:5064"
- },
- {
- "name": "PORT",
- "value": "5064"
- },
- {
- "name": "BasyxDbConnectionConfiguration__MongoConnectionString",
- "value": "[first(listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName')), '2021-10-15').connectionStrings).connectionString]"
- }
- ],
- "linuxFxVersion": "DOCKER|mnestix/mnestix-api:latest",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('mnestixApiName')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
- "[resourceId('Microsoft.Web/sites', variables('repoAppServiceName'))]",
- "[resourceId('Microsoft.Web/sites', variables('discoveryAppServiceName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('mnestixBrowserServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('mnestixBrowserServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "AAS_LIST_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "COMPARISON_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "AUTHENTICATION_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "LOCK_TIMESERIES_PERIOD_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "FRONTEND_URL",
- "value": "[variables('mnestixBrowserUrl')]"
- },
- {
- "name": "AAS_REPO_API_URL",
- "value": "[variables('mnestixApiProxyUrl')]"
- },
- {
- "name": "SUBMODEL_REPO_API_URL",
- "value": "[variables('mnestixApiProxyUrl')]"
- },
- {
- "name": "DISCOVERY_API_URL",
- "value": "[variables('mnestixApiDiscoveryProxyUrl')]"
- },
- {
- "name": "MNESTIX_BACKEND_API_URL",
- "value": "[variables('mnestixApiServiceUrl')]"
- },
- {
- "name": "AD_CLIENT_ID",
- "value": "[parameters('frontendClientId')]"
- },
- {
- "name": "AD_TENANT_ID",
- "value": "[parameters('tenantId')]"
- },
- {
- "name": "APPLICATION_ID_URI",
- "value": "[parameters('applicationIdUri')]"
- },
- {
- "name": "REGISTRY_API_URL",
- "value": "[variables('aasRegistryAppServiceUrl')]"
- },
- {
- "name": "SUBMODEL_REGISTRY_API_URL",
- "value": "[variables('submodelRegistryAppServiceUrl')]"
- }
- ],
- "linuxFxVersion": "DOCKER|mnestix/mnestix-browser:latest",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('mnestixBrowserUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('discoveryAppServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('discoveryAppServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "BASYX_BACKEND",
- "value": "MongoDB"
- },
- {
- "name": "SPRING_DATA_MONGODB_URI",
- "value": "[first(listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName')), '2021-10-15').connectionStrings).connectionString]"
- },
- {
- "name": "SPRING_DATA_MONGODB_DATABASE",
- "value": "basyxdb"
- }
- ],
- "linuxFxVersion": "DOCKER|eclipsebasyx/aas-discovery:2.0.0-SNAPSHOT",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('discoveryAppServiceUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "name": "[variables('discoveryPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2020-11-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateLinkServiceConnections": [
- {
- "name": "[variables('discoveryPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections",
- "properties": {
- "privateLinkServiceId": "[resourceId('Microsoft.Web/sites', variables('discoveryAppServiceName'))]",
- "groupIds": ["sites"],
- "requestMessage": "",
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "description": "",
- "actionsRequired": "None"
- }
- }
- }
- ],
- "manualPrivateLinkServiceConnections": [],
- "subnet": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoints')]"
- }
- },
- "resources": [
- {
- "name": "default",
- "type": "privateDnsZoneGroups",
- "apiVersion": "2020-03-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "dnsConfig",
- "properties": {
- "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]",
- "[resourceId('Microsoft.Network/privateEndpoints', variables('discoveryPrivateEndpointName'))]"
- ]
- }
- ],
- "dependsOn": [
- "[resourceId('Microsoft.Web/sites', variables('discoveryAppServiceName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('submodelRegistryAppServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('submodelRegistryAppServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "SPRING_DATA_MONGODB_URI",
- "value": "[first(listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName')), '2021-10-15').connectionStrings).connectionString]"
- }
- ],
- "linuxFxVersion": "DOCKER|eclipsebasyx/submodel-registry-log-mongodb:2.0.0-SNAPSHOT",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('submodelRegistryAppServiceUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('aasRegistryAppServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('aasRegistryAppServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "SPRING_DATA_MONGODB_URI",
- "value": "[first(listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('databaseAccountName')), '2021-10-15').connectionStrings).connectionString]"
- }
- ],
- "linuxFxVersion": "DOCKER|eclipsebasyx/aas-registry-log-mongodb:2.0.0-SNAPSHOT",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('aasRegistryAppServiceUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), 'private-endpoint-consumers')]"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/serverfarms', variables('serverFarmsName'))]",
- "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
- ]
- }
- ]
-}
diff --git a/infrastructure/migration/mainTemplate.json b/infrastructure/migration/mainTemplate.json
deleted file mode 100644
index 0e323c9..0000000
--- a/infrastructure/migration/mainTemplate.json
+++ /dev/null
@@ -1,318 +0,0 @@
-{
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "parameters": {
- "location": {
- "type": "string",
- "defaultValue": "WestEurope"
- },
- "randomId": {
- "type": "string",
- "defaultValue": "[take(guid(resourceGroup().id), 8)]"
- },
- "apiKey": {
- "type": "securestring"
- },
- "tenantId": {
- "type": "securestring"
- },
- "frontendClientId": {
- "type": "securestring"
- },
- "applicationIdUri": {
- "type": "securestring"
- },
- "backendClientId": {
- "type": "securestring"
- },
- "mnestixApiServiceName": {
- "defaultValue": "[concat('mnestix-api-', parameters('randomId'))]",
- "type": "string"
- },
- "mnestixBrowserServiceName": {
- "defaultValue": "[concat('mnestix-browser-', parameters('randomId'))]",
- "type": "string"
- },
- "serverFarmsName": {
- "type": "string"
- },
- "virtualNetworkName": {
- "type": "string"
- },
- "basyxAppServiceUrl": {
- "type": "string"
- },
- "mongoConnectionString": {
- "type": "securestring"
- }
- },
- "variables": {
- "mnestixApiServiceName": "[parameters('mnestixApiServiceName')]",
- "mnestixApiName": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net'), '')]",
- "mnestixApiServiceUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net'), '')]",
- "mnestixApiProxyUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net', '/repo'), '')]",
- "mnestixApiDiscoveryProxyUrl": "[uri(concat('https://', variables('mnestixApiServiceName'), '.azurewebsites.net', '/discovery'), '')]",
- "mnestixBrowserServiceName": "[parameters('mnestixBrowserServiceName')]",
- "mnestixBrowserUrl": "[uri(concat('https://', variables('mnestixBrowserServiceName'), '.azurewebsites.net'), '')]",
- "discoveryAppServiceName": "[concat('mnestix-basyx-discovery-', parameters('randomId'))]",
- "discoveryAppServiceUrl": "[uri(concat('https://', variables('discoveryAppServiceName'), '.azurewebsites.net'), '')]",
- "discoveryPrivateEndpointName": "[concat('mnestix-discovery-basyx-private-endpoint-', parameters('randomId'))]"
- },
- "resources": [
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('mnestixApiServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('mnestixApiServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "Features__AasRegistryMiddleware",
- "value": "true"
- },
- {
- "name": "Features__UseMongoDbBasedAasIdStorage",
- "value": "true"
- },
- {
- "name": "Features__AllowRetrievingAllShellsAndSubmodels",
- "value": "false"
- },
- {
- "name": "Features__UseAuthentication",
- "value": "true"
- },
- {
- "name": "ReverseProxy__Clusters__aasRepoCluster__Destinations__destination1__Address",
- "value": "[parameters('basyxAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__submodelRepoCluster__Destinations__destination1__Address",
- "value": "[parameters('basyxAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__discoveryCluster__Destinations__destination1__Address",
- "value": "[variables('discoveryAppServiceUrl')]"
- },
- {
- "name": "ReverseProxy__Clusters__influxCluster__Destinations__destination1__Address",
- "value": "placeholder_for_influx_db_connection"
- },
- {
- "name": "ReverseProxy__Routes__InfluxRoute__Transforms__1__Set",
- "value": "Token"
- },
- {
- "name": "CustomerEndpointsSecurity__ApiKey",
- "value": "[parameters('apiKey')]"
- },
- {
- "name": "AzureAd__ClientId",
- "value": "[parameters('backendClientId')]"
- },
- {
- "name": "AzureAd__TenantId",
- "value": "[parameters('tenantId')]"
- },
- {
- "name": "ASPNETCORE_URLS",
- "value": "http://+:5064"
- },
- {
- "name": "PORT",
- "value": "5064"
- },
- {
- "name": "BasyxDbConnectionConfiguration__MongoConnectionString",
- "value": "[parameters('mongoConnectionString')]"
- }
- ],
- "linuxFxVersion": "DOCKER|mnestix/mnestix-api:latest",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('mnestixApiName')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'private-endpoint-consumers')]"
- }
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('mnestixBrowserServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('mnestixBrowserServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "AAS_LIST_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "COMPARISON_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "AUTHENTICATION_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "LOCK_TIMESERIES_PERIOD_FEATURE_FLAG",
- "value": "true"
- },
- {
- "name": "FRONTEND_URL",
- "value": "[variables('mnestixBrowserUrl')]"
- },
- {
- "name": "AAS_REPO_API_URL",
- "value": "[variables('mnestixApiProxyUrl')]"
- },
- {
- "name": "DISCOVERY_API_URL",
- "value": "[variables('mnestixApiDiscoveryProxyUrl')]"
- },
- {
- "name": "MNESTIX_BACKEND_API_URL",
- "value": "[variables('mnestixApiServiceUrl')]"
- },
- {
- "name": "AD_CLIENT_ID",
- "value": "[parameters('frontendClientId')]"
- },
- {
- "name": "AD_TENANT_ID",
- "value": "[parameters('tenantId')]"
- },
- {
- "name": "APPLICATION_ID_URI",
- "value": "[parameters('applicationIdUri')]"
- }
- ],
- "linuxFxVersion": "DOCKER|mnestix/mnestix-browser:latest",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('mnestixBrowserUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'private-endpoint-consumers')]"
- }
- },
- {
- "type": "Microsoft.Web/sites",
- "apiVersion": "2021-03-01",
- "name": "[variables('discoveryAppServiceName')]",
- "location": "[parameters('location')]",
- "kind": "app,linux,container",
- "properties": {
- "name": "[variables('discoveryAppServiceName')]",
- "siteConfig": {
- "appSettings": [
- {
- "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
- "value": "false"
- },
- {
- "name": "BASYX_BACKEND",
- "value": "MongoDB"
- },
- {
- "name": "SPRING_DATA_MONGODB_URI",
- "value": "[parameters('mongoConnectionString')]"
- },
- {
- "name": "SPRING_DATA_MONGODB_DATABASE",
- "value": "basyxdb"
- }
- ],
- "linuxFxVersion": "DOCKER|eclipsebasyx/aas-discovery:2.0.0-SNAPSHOT",
- "alwaysOn": true
- },
- "hostNameSslStates": [
- {
- "name": "[variables('discoveryAppServiceUrl')]",
- "sslState": "Disabled",
- "hostType": "Standard"
- }
- ],
- "vnetRouteAllEnabled": false,
- "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverFarmsName'))]",
- "clientAffinityEnabled": false,
- "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'private-endpoint-consumers')]"
- }
- },
- {
- "name": "[variables('discoveryPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2020-11-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateLinkServiceConnections": [
- {
- "name": "[variables('discoveryPrivateEndpointName')]",
- "type": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections",
- "properties": {
- "privateLinkServiceId": "[resourceId('Microsoft.Web/sites', variables('discoveryAppServiceName'))]",
- "groupIds": ["sites"],
- "requestMessage": "",
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "description": "",
- "actionsRequired": "None"
- }
- }
- }
- ],
- "manualPrivateLinkServiceConnections": [],
- "subnet": {
- "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'private-endpoints')]"
- }
- },
- "resources": [
- {
- "name": "default",
- "type": "privateDnsZoneGroups",
- "apiVersion": "2020-03-01",
- "location": "[parameters('location')]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "dnsConfig",
- "properties": {
- "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateEndpoints', variables('discoveryPrivateEndpointName'))]",
- "[resourceId('Microsoft.Web/sites', variables('discoveryAppServiceName'))]"
- ]
- }
- ]
- }
- ]
-}
diff --git a/jest.backend.config.ts b/jest.backend.config.ts
new file mode 100644
index 0000000..6126554
--- /dev/null
+++ b/jest.backend.config.ts
@@ -0,0 +1,11 @@
+export {};
+module.exports = {
+ moduleDirectories: ['node_modules', 'src'],
+ transform: {
+ '^.+\\.(t|j)sx?$': '@swc/jest',
+ },
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
+ modulePathIgnorePatterns: ['cypress']
+};
diff --git a/jest.config.ts b/jest.config.ts
index 6126554..ddc73ea 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,11 +1,5 @@
+/**
+ * Unit tests for nextjs frontend and backend need different configurations.
+ */
export {};
-module.exports = {
- moduleDirectories: ['node_modules', 'src'],
- transform: {
- '^.+\\.(t|j)sx?$': '@swc/jest',
- },
- preset: 'ts-jest',
- testEnvironment: 'node',
- testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
- modulePathIgnorePatterns: ['cypress']
-};
+module.exports = { projects: ['/jest.frontend.config.ts', '/jest.backend.config.ts'] };
\ No newline at end of file
diff --git a/jest.frontend.config.ts b/jest.frontend.config.ts
new file mode 100644
index 0000000..903bf3e
--- /dev/null
+++ b/jest.frontend.config.ts
@@ -0,0 +1,47 @@
+import nextJest from 'next/jest';
+import { Config } from 'jest';
+import { TextDecoder, TextEncoder } from 'util';
+
+export {};
+const createJestConfig = nextJest({
+ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
+ dir: './',
+});
+
+const config: Config = {
+ setupFilesAfterEnv: ['/jest.setup.ts'],
+ testEnvironment: 'jsdom',
+ modulePathIgnorePatterns: ['cypress'],
+ globals: {
+ 'ts-jest': {
+ tsConfigFile: 'tsconfig.json',
+ },
+ TextEncoder: TextEncoder,
+ TextDecoder: TextDecoder,
+ },
+ // mock all svg files
+ moduleNameMapper: {
+ '^.+\\.(svg)$': '/__mocks__/svg.tsx',
+ },
+ testMatch: ['**/__tests__/**/*.tsx', '**/?(*.)+(spec|test).tsx'],
+ moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
+};
+
+// @ts-expect-error We don't know the type
+const jestConfigWithOverrides = async (...args) => {
+ const fn = createJestConfig(config);
+ // @ts-expect-error We don't know the type
+ const res = await fn(...args);
+
+ // Don't ignore specific node_modules during transformation. This is needed if a node_module doesn't return valid JavaScript files.
+ res.transformIgnorePatterns = res.transformIgnorePatterns!.map((pattern) => {
+ if (pattern === '/node_modules/') {
+ return '/node_modules/(?!.+)';
+ }
+ return pattern;
+ });
+
+ return res;
+};
+
+module.exports = jestConfigWithOverrides;
diff --git a/package.json b/package.json
index f49927b..349b4e8 100644
--- a/package.json
+++ b/package.json
@@ -1,123 +1,125 @@
{
- "name": "mnestix-browser",
- "version": "1.3.0",
- "private": true,
- "license": "MIT",
- "devDependencies": {
- "@eslint/js": "^9.2.0",
- "@next/eslint-plugin-next": "^14.2.3",
- "@swc/core": "^1.7.23",
- "@swc/jest": "^0.2.36",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.5.0",
- "@testing-library/react": "^16.0.1",
- "@testing-library/user-event": "^14.5.2",
- "@types/flat": "^5.0.5",
- "@types/jest": "^29.5.12",
- "@types/lodash": "^4.17.0",
- "@types/node": "^22.5.4",
- "@types/react": "^18.2.79",
- "@types/react-dom": "^18.2.25",
- "@typescript-eslint/eslint-plugin": "^7.7.1",
- "@typescript-eslint/parser": "^7.7.1",
- "cypress": "^13.8.0",
- "cypress-junit-reporter": "^1.3.1",
- "cypress-msal-login": "^2.0.1",
- "dotenv": "^16.4.5",
- "eslint-config-next": "14.2.3",
- "eslint-plugin-cypress": "^3.0.0",
- "eslint-plugin-formatjs": "^4.13.0",
- "eslint-plugin-react": "^7.34.1",
- "eslint-plugin-react-hooks": "^4.6.0",
- "globals": "^15.1.0",
- "jest": "^29.7.0",
- "jest-environment-jsdom": "^29.7.0",
- "lodash": "^4.17.21",
- "prettier": "^3.2.5",
- "prisma": "^5.18.0",
- "ts-jest": "^29.2.5",
- "ts-mockito": "^2.6.1",
- "ts-node": "^10.9.2",
- "typescript-eslint": "^7.8.0"
- },
- "dependencies": {
- "@aas-core-works/aas-core3.0-typescript": "^1.0.3",
- "@azure/msal-browser": "^3.13.0",
- "@azure/msal-react": "^2.0.15",
- "@date-io/moment": "^3.0.0",
- "@emotion/cache": "^11.11.0",
- "@emotion/react": "^11.11.4",
- "@emotion/styled": "^11.11.5",
- "@fontsource/noto-sans": "^5.0.22",
- "@fontsource/saira": "^5.0.28",
- "@influxdata/influxdb-client-browser": "^1.33.2",
- "@mui/icons-material": "^5.15.15",
- "@mui/lab": "^5.0.0-alpha.170",
- "@mui/material": "^5.15.15",
- "@mui/material-nextjs": "^5.15.11",
- "@mui/styled-engine": "latest",
- "@mui/x-date-pickers": "^7.2.0",
- "@mui/x-tree-view": "^7.3.0",
- "@prisma/client": "^5.18.0",
- "@svgr/webpack": "^8.1.0",
- "buffer": "^6.0.3",
- "date-fns": "^2.28.0",
- "eslint": "8.57",
- "flat": "^6.0.1",
- "isomorphic-fetch": "^3.0.0",
- "moment": "^2.30.1",
- "next": "^14.2.3",
- "next-auth": "^4.24.7",
- "next-intl": "^3.14.0",
- "ol": "^9.1.0",
- "qr-scanner": "^1.4.2",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-hook-form": "^7.51.3",
- "react-intl": "^6.6.5",
- "recharts": "^2.12.6",
- "sass": "^1.75.0",
- "sqlite3": "^5.1.7",
- "typescript": "^5.4.5",
- "url": "^0.11.3",
- "web-vitals": "^3.5.2"
- },
- "resolutions": {
- "string-width": "4.2.3"
- },
- "scripts": {
- "dev": "yarn prisma migrate deploy && yarn prisma generate && next dev",
- "turbo": "yarn prisma migrate deploy && yarn prisma generate && next dev --turbo",
- "build": "next build",
- "start": "yarn prisma migrate deploy && yarn prisma generate && next start",
- "prettier": "prettier --check ./",
- "format": "prettier --write ./",
- "lint": "next lint",
- "test": "cypress open",
- "test:headless": "cypress run",
- "docker:basyx": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx up",
- "docker:dev": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx --profile backend --profile frontend up",
- "docker:backend": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx --profile backend up",
- "docker:prod": "docker compose up",
- "docker:test": "docker compose -f compose.yml -f docker-compose/compose.test.yml --profile tests up -d --build && docker compose -f compose.yml -f docker-compose/compose.test.yml attach cypress-test",
- "docker:stop": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml kill",
- "docker:down": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml down",
- "docker:prune": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml down -v && docker network prune && docker network rm mnestix-network",
- "docker:keycloak": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.keycloak.yml up -d",
- "docker:azure": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.azure_ad.yml up -d"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- },
- "proxy": "https://localhost:7064",
- "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
+ "name": "mnestix-browser",
+ "version": "1.3.1",
+ "private": true,
+ "license": "MIT",
+ "devDependencies": {
+ "@eslint/js": "^9.2.0",
+ "@jest/globals": "^29.7.0",
+ "@next/eslint-plugin-next": "^14.2.3",
+ "@swc/core": "^1.7.23",
+ "@swc/jest": "^0.2.36",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.1",
+ "@testing-library/user-event": "^14.5.2",
+ "@types/flat": "^5.0.5",
+ "@types/jest": "^29.5.12",
+ "@types/lodash": "^4.17.0",
+ "@types/node": "^22.5.4",
+ "@types/react": "^18.2.79",
+ "@types/react-dom": "^18.2.25",
+ "@typescript-eslint/eslint-plugin": "^7.7.1",
+ "@typescript-eslint/parser": "^7.7.1",
+ "cypress": "^13.8.0",
+ "cypress-junit-reporter": "^1.3.1",
+ "cypress-msal-login": "^2.0.1",
+ "dotenv": "^16.4.5",
+ "eslint-config-next": "14.2.3",
+ "eslint-plugin-cypress": "^3.0.0",
+ "eslint-plugin-formatjs": "^4.13.0",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "globals": "^15.1.0",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "lodash": "^4.17.21",
+ "prettier": "^3.2.5",
+ "prisma": "^5.18.0",
+ "ts-jest": "^29.2.5",
+ "ts-mockito": "^2.6.1",
+ "ts-node": "^10.9.2",
+ "typescript-eslint": "^7.8.0"
+ },
+ "dependencies": {
+ "@aas-core-works/aas-core3.0-typescript": "^1.0.3",
+ "@azure/msal-browser": "^3.13.0",
+ "@azure/msal-react": "^2.0.15",
+ "@date-io/moment": "^3.0.0",
+ "@emotion/cache": "^11.11.0",
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@fontsource/noto-sans": "^5.0.22",
+ "@fontsource/saira": "^5.0.28",
+ "@influxdata/influxdb-client-browser": "^1.33.2",
+ "@mui/icons-material": "^5.15.15",
+ "@mui/lab": "^5.0.0-alpha.170",
+ "@mui/material": "^5.15.15",
+ "@mui/material-nextjs": "^5.15.11",
+ "@mui/styled-engine": "latest",
+ "@mui/x-date-pickers": "^7.2.0",
+ "@mui/x-tree-view": "^7.3.0",
+ "@prisma/client": "^5.18.0",
+ "@svgr/webpack": "^8.1.0",
+ "buffer": "^6.0.3",
+ "date-fns": "^2.28.0",
+ "eslint": "8.57",
+ "flat": "^6.0.1",
+ "isomorphic-fetch": "^3.0.0",
+ "moment": "^2.30.1",
+ "next": "^14.2.3",
+ "next-auth": "^4.24.7",
+ "next-intl": "^3.14.0",
+ "ol": "^9.1.0",
+ "qr-scanner": "^1.4.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.51.3",
+ "react-intl": "^6.6.5",
+ "recharts": "^2.12.6",
+ "sass": "^1.75.0",
+ "sqlite3": "^5.1.7",
+ "typescript": "^5.4.5",
+ "url": "^0.11.3",
+ "web-vitals": "^3.5.2"
+ },
+ "resolutions": {
+ "string-width": "4.2.3"
+ },
+ "scripts": {
+ "dev": "yarn prisma migrate deploy && yarn prisma generate && next dev --turbo",
+ "debug": "yarn prisma migrate deploy && yarn prisma generate && next dev",
+ "build": "next build",
+ "start": "yarn prisma migrate deploy && yarn prisma generate && next start",
+ "prettier": "prettier --check ./",
+ "format": "prettier --write ./",
+ "lint": "next lint",
+ "test": "cypress open",
+ "test:headless": "cypress run",
+ "test:unit": "jest",
+ "docker:basyx": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx up",
+ "docker:dev": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx --profile backend --profile frontend up",
+ "docker:backend": "docker compose -f compose.yml -f docker-compose/compose.dev.yml --profile basyx --profile backend up",
+ "docker:prod": "docker compose up",
+ "docker:test": "docker compose -f compose.yml -f docker-compose/compose.test.yml --profile tests up -d --build && docker compose -f compose.yml -f docker-compose/compose.test.yml attach cypress-test",
+ "docker:stop": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml kill",
+ "docker:down": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml down",
+ "docker:prune": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.test.yml down -v && docker network prune && docker network rm mnestix-network",
+ "docker:keycloak": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.keycloak.yml up -d",
+ "docker:azure": "docker compose -f compose.yml -f docker-compose/compose.dev.yml -f docker-compose/compose.azure_ad.yml up -d"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "proxy": "https://localhost:7064",
+ "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
diff --git a/src/app/[locale]/list/_components-deprecated/AasList.tsx b/src/app/[locale]/list/_components-deprecated/AasList.tsx
new file mode 100644
index 0000000..f027934
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/AasList.tsx
@@ -0,0 +1,95 @@
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Tooltip,
+ Typography,
+ useTheme,
+} from '@mui/material';
+import { FormattedMessage } from 'react-intl';
+import { messages } from 'lib/i18n/localization';
+import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
+import { AasListEntry } from 'lib/api/generated-api/clients.g';
+import { AasListTableRow } from 'app/[locale]/list/_components-deprecated/AasListTableRow';
+
+type AasListProps = {
+ shells: AasListEntry[];
+ tableHeaders: { label: string }[];
+ comparisonFeatureFlag?: boolean;
+ selectedAasList: string[] | undefined;
+ updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void;
+};
+
+export default function AasList(props: AasListProps) {
+ const { shells, tableHeaders, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = props;
+ const theme = useTheme();
+ const MAX_SELECTED_ITEMS = 3;
+
+ /**
+ * Decides if the current checkbox should be disabled or not.
+ */
+ const checkBoxDisabled = (aasId: string | undefined) => {
+ if (!aasId) return false;
+ return selectedAasList && selectedAasList.length >= MAX_SELECTED_ITEMS && !selectedAasList.includes(aasId);
+ };
+
+ return (
+
+
+
+
+ {comparisonFeatureFlag && (
+
+ }
+ arrow
+ >
+
+
+
+ )}
+ {!!tableHeaders &&
+ tableHeaders.map((header: { label: string }, index) => (
+
+ {header.label}
+
+ ))}
+
+
+
+ {shells?.map((aasListEntry) => (
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/app/[locale]/list/_components-deprecated/AasListComparisonHeader.tsx b/src/app/[locale]/list/_components-deprecated/AasListComparisonHeader.tsx
new file mode 100644
index 0000000..1f78188
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/AasListComparisonHeader.tsx
@@ -0,0 +1,50 @@
+import { Box, Button, IconButton, Typography } from '@mui/material';
+import { FormattedMessage } from 'react-intl';
+import { messages } from 'lib/i18n/localization';
+import CloseIcon from '@mui/icons-material/Close';
+import { useRouter } from 'next/navigation';
+import { tooltipText } from 'lib/util/ToolTipText';
+
+type CompareAasListBarType = {
+ selectedAasList: string[] | undefined;
+ updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void;
+};
+
+export const AasListComparisonHeader = (props: CompareAasListBarType) => {
+ const { selectedAasList, updateSelectedAasList } = props;
+
+ const navigate = useRouter();
+ const navigateToCompare = () => {
+ const encodedAasList = selectedAasList?.map((aasId) => {
+ return encodeURIComponent(aasId);
+ });
+ const searchString = encodedAasList?.join('&aasId=');
+ navigate.push(`/compare?aasId=${searchString}`);
+ };
+
+ return (
+ <>
+
+
+
+
+ {selectedAasList?.map((selectedAas) => (
+
+ {tooltipText(selectedAas, 15)}
+ updateSelectedAasList(false, selectedAas)}>
+
+
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/src/app/[locale]/list/_components-deprecated/AasListHeader.tsx b/src/app/[locale]/list/_components-deprecated/AasListHeader.tsx
new file mode 100644
index 0000000..40de5e5
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/AasListHeader.tsx
@@ -0,0 +1,12 @@
+import { getTranslations } from 'next-intl/server';
+import { Typography } from '@mui/material';
+
+export default async function AasListHeader() {
+ const t = await getTranslations('aas-list');
+
+ return (
+
+ {t('header')}
+
+ );
+}
diff --git a/src/app/[locale]/list/_components-deprecated/AasListTableRow.tsx b/src/app/[locale]/list/_components-deprecated/AasListTableRow.tsx
new file mode 100644
index 0000000..1f02a5a
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/AasListTableRow.tsx
@@ -0,0 +1,145 @@
+import { Box, Checkbox, Chip, TableCell, Typography } from '@mui/material';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { messages } from 'lib/i18n/localization';
+import { getProductClassId } from 'lib/util/ProductClassResolverUtil';
+import LabelOffIcon from '@mui/icons-material/LabelOff';
+import { AasListEntry } from 'lib/api/generated-api/clients.g';
+import { encodeBase64 } from 'lib/util/Base64Util';
+import { useRouter } from 'next/navigation';
+import { useAasOriginSourceState, useAasState } from 'components/contexts/CurrentAasContext';
+import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
+import { ProductClassChip } from 'app/[locale]/list/_components-deprecated/ProductClassChip';
+import { tooltipText } from 'lib/util/ToolTipText';
+import PictureTableCell from 'components/basics/listBasics/PictureTableCell';
+import { ArrowForward } from '@mui/icons-material';
+import { RoundedIconButton } from 'components/basics/Buttons';
+import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
+import { getThumbnailFromShell } from 'lib/services/repository-access/repositorySearchActions';
+import { isValidUrl } from 'lib/util/UrlUtil';
+import { useState } from 'react';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { useEnv } from 'app/env/provider';
+import { ImageWithFallback } from 'components/basics/StyledImageWithFallBack';
+
+type AasTableRowProps = {
+ aasListEntry: AasListEntry;
+ comparisonFeatureFlag: boolean | undefined;
+ checkBoxDisabled: (aasId: string | undefined) => boolean | undefined;
+ selectedAasList: string[] | undefined;
+ updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void;
+};
+
+const tableBodyText = {
+ lineHeight: '150%',
+ fontSize: '16px',
+ color: 'text.primary',
+};
+export const AasListTableRow = (props: AasTableRowProps) => {
+ const { aasListEntry, comparisonFeatureFlag, checkBoxDisabled, selectedAasList, updateSelectedAasList } = props;
+ const navigate = useRouter();
+ const intl = useIntl();
+ const [, setAas] = useAasState();
+ const [, setAasOriginUrl] = useAasOriginSourceState();
+ const notificationSpawner = useNotificationSpawner();
+ const [thumbnailUrl, setThumbnailUrl] = useState('');
+ const navigateToAas = (listEntry: AasListEntry) => {
+ setAas(null);
+ setAasOriginUrl(null);
+ if (listEntry.aasId) navigate.push(`/viewer/${encodeBase64(listEntry.aasId)}`);
+ };
+ const env = useEnv();
+
+ const translateListText = (property: { [key: string]: string } | undefined) => {
+ if (!property) return '';
+ return property[intl.locale] ?? Object.values(property)[0] ?? '';
+ };
+
+ useAsyncEffect(async () => {
+ if (isValidUrl(aasListEntry.thumbnailUrl ?? '')) {
+ setThumbnailUrl(aasListEntry.thumbnailUrl ?? '');
+ } else if (aasListEntry.aasId && env.AAS_REPO_API_URL) {
+ const response = await getThumbnailFromShell(aasListEntry.aasId, env.AAS_REPO_API_URL);
+ if (response.isSuccess) {
+ const blob = mapFileDtoToBlob(response.result);
+ const blobUrl = URL.createObjectURL(blob);
+ setThumbnailUrl(blobUrl);
+ }
+ }
+ }, [aasListEntry.thumbnailUrl]);
+
+ const showMaxElementsNotification = () => {
+ notificationSpawner.spawn({
+ message: (
+
+
+
+ ),
+ severity: 'warning',
+ });
+ };
+
+ return (
+ <>
+ {comparisonFeatureFlag && (
+
+ {
+ if (checkBoxDisabled(aasListEntry.aasId)) showMaxElementsNotification();
+ }}
+ >
+ el == aasListEntry.aasId))}
+ disabled={checkBoxDisabled(aasListEntry.aasId)}
+ onChange={(evt) => updateSelectedAasList(evt.target.checked, aasListEntry.aasId)}
+ data-testid="list-checkbox"
+ title={intl.formatMessage(messages.mnestix.aasList.titleComparisonAddButton)}
+ />
+
+
+ )}
+
+
+
+
+ {translateListText(aasListEntry.manufacturerName)}
+
+
+ {tooltipText(translateListText(aasListEntry.manufacturerProductDesignation), 80)}
+
+
+
+
+
+ {tooltipText(aasListEntry.assetId, 80)}
+
+
+
+ {tooltipText(aasListEntry.aasId, 80)}
+
+
+ {aasListEntry.productGroup ? (
+
+ ) : (
+ }
+ variant="outlined"
+ icon={}
+ data-testid="product-class-chip"
+ title={intl.formatMessage(messages.mnestix.aasList.titleProductChipNotAvailable)}
+ />
+ )}
+
+
+ }
+ onClick={() => navigateToAas(aasListEntry)}
+ title={intl.formatMessage(messages.mnestix.aasList.titleViewAASButton)}
+ data-testid="list-to-detailview-button"
+ />
+
+ >
+ );
+};
diff --git a/src/app/[locale]/list/_components/AasListView.tsx b/src/app/[locale]/list/_components-deprecated/AasListViewDeprecated.tsx
similarity index 92%
rename from src/app/[locale]/list/_components/AasListView.tsx
rename to src/app/[locale]/list/_components-deprecated/AasListViewDeprecated.tsx
index df74a8f..23b0069 100644
--- a/src/app/[locale]/list/_components/AasListView.tsx
+++ b/src/app/[locale]/list/_components-deprecated/AasListViewDeprecated.tsx
@@ -8,14 +8,14 @@ import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
import { Box } from '@mui/material';
import { showError } from 'lib/util/ErrorHandlerUtil';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
-import { SelectProductType } from 'app/[locale]/list/_components/SelectProductType';
-import { AasListComparisonHeader } from 'app/[locale]/list/_components/AasListComparisonHeader';
-import AasList from 'app/[locale]/list/_components/AasList';
+import { SelectProductType } from 'app/[locale]/list/_components-deprecated/SelectProductType';
+import { AasListComparisonHeader } from 'app/[locale]/list/_components-deprecated/AasListComparisonHeader';
+import AasList from 'app/[locale]/list/_components-deprecated/AasList';
import { useIntl } from 'react-intl';
import { messages } from 'lib/i18n/localization';
-import { getAasListEntries } from 'lib/services/aasListApiActions';
+import { getAasListEntries } from 'lib/services/list-service/aasListApiActions';
-export const AasListView = () => {
+export const AasListViewDeprecated = () => {
const [isLoadingList, setIsLoadingList] = useState(false);
const [aasList, setAasList] = useState();
const [aasListFiltered, setAasListFiltered] = useState();
diff --git a/src/app/[locale]/list/_components-deprecated/GetProductClassIcon.tsx b/src/app/[locale]/list/_components-deprecated/GetProductClassIcon.tsx
new file mode 100644
index 0000000..02899dd
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/GetProductClassIcon.tsx
@@ -0,0 +1,21 @@
+import dynamic from 'next/dynamic';
+
+const TireRepairIcon = dynamic(() => import('@mui/icons-material/TireRepair'));
+const FireHydrantAltIcon = dynamic(() => import('@mui/icons-material/FireHydrantAlt'));
+const LabelIcon = dynamic(() => import('@mui/icons-material/Label'));
+
+/**
+ * Returns an icon component based on the provided product class type.
+ * @param props
+ */
+export const GetProductClassIcon = (props: { productClassType: string }) => {
+ const { productClassType } = props;
+ switch (productClassType) {
+ case 'pneumatics':
+ return ;
+ case 'hydraulics':
+ return ;
+ default:
+ return ;
+ }
+};
diff --git a/src/app/[locale]/list/_components-deprecated/ProductClassChip.tsx b/src/app/[locale]/list/_components-deprecated/ProductClassChip.tsx
new file mode 100644
index 0000000..5543888
--- /dev/null
+++ b/src/app/[locale]/list/_components-deprecated/ProductClassChip.tsx
@@ -0,0 +1,43 @@
+import { Chip } from '@mui/material';
+import { parseProductClassFromString } from 'lib/util/ProductClassResolverUtil';
+import { useIntl } from 'react-intl';
+import { messages } from 'lib/i18n/localization';
+import { tooltipText } from 'lib/util/ToolTipText';
+import { GetProductClassIcon } from 'app/[locale]/list/_components-deprecated/GetProductClassIcon';
+
+type ProductClassChipProps = {
+ productClassId: string | null;
+ maxChars: number;
+};
+
+/**
+ * Returns a chip component adjusted for product class element
+ */
+export const ProductClassChip = (props: ProductClassChipProps) => {
+ const { productClassId, maxChars } = props;
+ const intl = useIntl();
+ if (!productClassId) return '';
+ let productClass;
+ try {
+ productClass = parseProductClassFromString(
+ productClassId,
+ intl.formatMessage(messages.mnestix.aasList.productClasses[productClassId]),
+ );
+ } catch (e) {
+ console.warn('Invalid product type', e);
+ }
+ if (!productClass) {
+ productClass = parseProductClassFromString(productClassId, productClassId);
+ }
+ return (
+ }
+ data-testid="product-class-chip"
+ title={intl.formatMessage(messages.mnestix.aasList.productClassHeading) + ' ' + productClass.description}
+ />
+ );
+};
diff --git a/src/app/[locale]/list/_components/SelectProductType.tsx b/src/app/[locale]/list/_components-deprecated/SelectProductType.tsx
similarity index 100%
rename from src/app/[locale]/list/_components/SelectProductType.tsx
rename to src/app/[locale]/list/_components-deprecated/SelectProductType.tsx
diff --git a/src/app/[locale]/list/_components/AasList.tsx b/src/app/[locale]/list/_components/AasList.tsx
index 1314043..d866a95 100644
--- a/src/app/[locale]/list/_components/AasList.tsx
+++ b/src/app/[locale]/list/_components/AasList.tsx
@@ -12,22 +12,33 @@
import { FormattedMessage } from 'react-intl';
import { messages } from 'lib/i18n/localization';
import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
-import { AasListEntry } from 'lib/api/generated-api/clients.g';
import { AasListTableRow } from 'app/[locale]/list/_components/AasListTableRow';
+import { AasListDto } from 'lib/services/list-service/ListService';
+import { useTranslations } from 'next-intl';
type AasListProps = {
- shells: AasListEntry[];
- tableHeaders: { label: string }[];
+ repositoryUrl: string;
+ shells: AasListDto | undefined;
comparisonFeatureFlag?: boolean;
selectedAasList: string[] | undefined;
updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void;
};
export default function AasList(props: AasListProps) {
- const { shells, tableHeaders, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = props;
+ const { repositoryUrl, shells, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = props;
const theme = useTheme();
+ const t = useTranslations('aas-list');
const MAX_SELECTED_ITEMS = 3;
+ const tableHeaders = [
+ { label: t('picture') },
+ { label: t('manufacturerHeading') },
+ { label: t('productDesignationHeading') },
+ { label: t('assetIdHeading') },
+ { label: t('aasIdHeading') },
+ { label: t('productClassHeading') },
+ ];
+
/**
* Decides if the current checkbox should be disabled or not.
*/
@@ -37,59 +48,63 @@ export default function AasList(props: AasListProps) {
};
return (
-
-
-
-
- {comparisonFeatureFlag && (
-
- }
- arrow
- >
-
-
-
- )}
- {!!tableHeaders &&
- tableHeaders.map((header: { label: string }, index) => (
-
- {header.label}
-
- ))}
-
-
-
- {shells?.map((aasListEntry) => (
+ <>
+
+
+
-
+ {comparisonFeatureFlag && (
+
+ }
+ arrow
+ >
+
+
+
+ )}
+ {!!tableHeaders &&
+ tableHeaders.map((header: { label: string }, index) => (
+
+ {header.label}
+
+ ))}
- ))}
-
-
-
+
+
+ {shells &&
+ shells.entities?.map((aasListEntry) => (
+
+
+
+ ))}
+
+
+
+ >
);
}
diff --git a/src/app/[locale]/list/_components/AasListComparisonHeader.tsx b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx
index 1f78188..a004075 100644
--- a/src/app/[locale]/list/_components/AasListComparisonHeader.tsx
+++ b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx
@@ -24,10 +24,7 @@ export const AasListComparisonHeader = (props: CompareAasListBarType) => {
return (
<>
-
-
-
-
+
{selectedAasList?.map((selectedAas) => (
{tooltipText(selectedAas, 15)}
diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx
new file mode 100644
index 0000000..e236bfa
--- /dev/null
+++ b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx
@@ -0,0 +1,132 @@
+import { CustomRender } from 'test-utils/CustomRender';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import AasListDataWrapper from 'app/[locale]/list/_components/AasListDataWrapper';
+import * as serverActions from 'lib/services/list-service/aasListApiActions';
+import * as connectionServerActions from 'lib/services/database/connectionServerActions';
+import { ListEntityDto } from 'lib/services/list-service/ListService';
+import { CurrentAasContextProvider } from 'components/contexts/CurrentAasContext';
+import { Internationalization } from 'lib/i18n/Internationalization';
+
+jest.mock('./../../../../lib/services/list-service/aasListApiActions');
+jest.mock('./../../../../lib/services/database/connectionServerActions');
+jest.mock('./../../../../lib/services/repository-access/repositorySearchActions', () => ({
+ getThumbnailFromShell: jest.fn(() => Promise.resolve({ success: true, result: { fileType: '', fileContent: '' } })),
+}));
+jest.mock('next/navigation', () => ({
+ useRouter() {
+ return {
+ prefetch: () => null,
+ };
+ },
+}));
+jest.mock('next-auth', jest.fn());
+
+const REPOSITORY_URL = 'https://test-repository.de';
+const FIRST_PAGE_CURSOR = '123123';
+
+const createTestListEntries = (from = 0, to = 10): ListEntityDto[] => {
+ const objects: ListEntityDto[] = [];
+
+ for (let i = from; i < to; i++) {
+ const obj: ListEntityDto = {
+ aasId: `aasid${i}`,
+ assetId: `assetId${i}`,
+ thumbnail: '',
+ };
+
+ objects.push(obj);
+ }
+
+ return objects;
+};
+
+const mockActionFirstPage = jest.fn(() => {
+ return {
+ success: true,
+ entities: createTestListEntries(0, 10),
+ cursor: FIRST_PAGE_CURSOR,
+ error: {},
+ };
+});
+
+const mockActionSecondPage = jest.fn(() => {
+ return {
+ success: true,
+ entities: createTestListEntries(10, 12),
+ cursor: undefined,
+ error: {},
+ };
+});
+
+describe('AASListDataWrapper', () => {
+ beforeEach(async () => {
+ (serverActions.getAasListEntities as jest.Mock).mockImplementation(mockActionFirstPage);
+
+ const mockDB = jest.fn(() => {
+ return [REPOSITORY_URL];
+ });
+ (connectionServerActions.getConnectionDataByTypeAction as jest.Mock).mockImplementation(mockDB);
+
+ CustomRender(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => screen.getByTestId('repository-select'));
+ });
+
+ it('Shows the info text if no repository is selected.', async () => {
+ expect(screen.queryByTestId('aas-list')).toBeNull();
+ expect(screen.getByTestId('select-repository-text')).toBeInTheDocument();
+ });
+
+ describe('Pagination', () => {
+ beforeEach(async () => {
+ // Choose a repository
+ await waitFor(() => screen.getByTestId('repository-select'));
+ const select = screen.getAllByRole('combobox')[0];
+ fireEvent.mouseDown(select);
+ const firstElement = screen.getAllByRole('option')[0];
+ fireEvent.click(firstElement);
+
+ await waitFor(() => screen.getByTestId('list-next-button'));
+ });
+
+ it('Should disable the back button on the first page', async () => {
+ const backButton = await waitFor(() => screen.getByTestId('list-back-button'));
+ expect(screen.getByText('assetId1', { exact: false })).toBeInTheDocument();
+ expect(backButton).toBeDisabled();
+ });
+
+ it('Loads the next page with the provided cursor', async () => {
+ // go to second page
+ (serverActions.getAasListEntities as jest.Mock).mockImplementation(mockActionSecondPage);
+ const nextButton = await waitFor(() => screen.getByTestId('list-next-button'));
+ await waitFor(async () => nextButton.click());
+
+ expect(screen.getByText('assetId10', { exact: false })).toBeInTheDocument();
+ expect(screen.getByText('Page 2', { exact: false })).toBeInTheDocument();
+ expect(screen.getByTestId('list-next-button')).toBeDisabled();
+ expect(mockActionSecondPage).toBeCalledWith(REPOSITORY_URL, 10, FIRST_PAGE_CURSOR);
+ });
+
+ it('Navigates one page back when clicking on the back button', async () => {
+ // go to second page
+ (serverActions.getAasListEntities as jest.Mock).mockImplementation(mockActionSecondPage);
+ const nextButton = await waitFor(() => screen.getByTestId('list-next-button'));
+ await waitFor(async () => nextButton.click());
+
+ // back to first page
+ (serverActions.getAasListEntities as jest.Mock).mockImplementation(mockActionFirstPage);
+ const backButton = await waitFor(() => screen.getByTestId('list-back-button'));
+ await waitFor(async () => backButton.click());
+
+ expect(screen.getByText('assetId3', { exact: false })).toBeInTheDocument();
+ expect(screen.getByText('Page 1', { exact: false })).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.tsx
new file mode 100644
index 0000000..814c415
--- /dev/null
+++ b/src/app/[locale]/list/_components/AasListDataWrapper.tsx
@@ -0,0 +1,158 @@
+import { AasListDto, ListEntityDto } from 'lib/services/list-service/ListService';
+import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
+import { getAasListEntities } from 'lib/services/list-service/aasListApiActions';
+import { showError } from 'lib/util/ErrorHandlerUtil';
+import { useState } from 'react';
+import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
+import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner';
+import AasList from './AasList';
+import { useEnv } from 'app/env/provider';
+import { SelectProductType } from './filter/SelectProductType';
+import { AasListComparisonHeader } from './AasListComparisonHeader';
+import { Box, IconButton, Typography } from '@mui/material';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
+import { SelectRepository } from './filter/SelectRepository';
+import { useTranslations } from 'next-intl';
+
+export default function AasListDataWrapper() {
+ const [isLoadingList, setIsLoadingList] = useState(false);
+ const [aasList, setAasList] = useState();
+ const [, setAasListFiltered] = useState();
+ const [selectedAasList, setSelectedAasList] = useState();
+ const notificationSpawner = useNotificationSpawner();
+ const [selectedRepository, setSelectedRepository] = useState();
+ const env = useEnv();
+ const t = useTranslations('aas-list');
+
+ //Pagination
+ const [currentCursor, setCurrentCursor] = useState();
+ const [cursorHistory, setCursorHistory] = useState<(string | undefined)[]>([]);
+ const [currentPage, setCurrentPage] = useState(0);
+
+ useAsyncEffect(async () => {
+ resetPagination();
+ await fetchListData();
+ }, [selectedRepository]);
+
+ const fetchListData = async (newCursor?: string | undefined, isNext = true) => {
+ if (!selectedRepository) return;
+
+ setIsLoadingList(true);
+ const response = await getAasListEntities(selectedRepository!, 10, newCursor);
+
+ if (response.success) {
+ setAasList(response);
+ setAasListFiltered(response.entities);
+ setCurrentCursor(response.cursor);
+
+ if (isNext) {
+ setCursorHistory((prevHistory) => [...prevHistory, newCursor]);
+ }
+ } else {
+ showError(response.error, notificationSpawner);
+ }
+ setIsLoadingList(false);
+ };
+
+ const handleNextPage = async () => {
+ await fetchListData(currentCursor, true);
+ setCurrentPage((prevPage) => prevPage + 1);
+ };
+
+ /**
+ * Handle a click on the back button.
+ * To load the page one step back, we need to use the cursor from two pages back.
+ */
+ const handleGoBack = async () => {
+ const previousCursor = cursorHistory[currentPage - 2] ?? undefined;
+ await fetchListData(previousCursor, false);
+ setCurrentPage((prevPage) => prevPage - 1);
+ };
+
+ const resetPagination = () => {
+ setCursorHistory([]);
+ setCurrentPage(0);
+ };
+
+ /**
+ * Update the list of currently selected aas
+ */
+ const updateSelectedAasList = (isChecked: boolean, aasId: string | undefined) => {
+ if (!aasId) return;
+ let selected: string[] = [];
+
+ if (isChecked) {
+ selected = selected.concat(selectedAasList ? selectedAasList : [], [aasId]);
+ selected = [...new Set(selected)];
+ } else if (!isChecked && selectedAasList) {
+ selected = selectedAasList.filter((aas) => {
+ return aas !== aasId;
+ });
+ } else {
+ return;
+ }
+
+ setSelectedAasList(selected);
+ };
+
+ return (
+ <>
+
+ {t('subHeader')}
+
+
+
+
+
+
+
+ {env.COMPARISON_FEATURE_FLAG && (
+
+ )}
+
+ {isLoadingList ? (
+
+ ) : (
+ <>
+ {selectedRepository ? (
+ <>
+
+
+
+
+
+ {t('page') + ' ' + (currentPage + 1)}
+
+
+
+
+ >
+ ) : (
+
+ {t('select-repository')}
+
+ )}
+ >
+ )}
+ >
+ );
+}
diff --git a/src/app/[locale]/list/_components/AasListHeader.tsx b/src/app/[locale]/list/_components/AasListHeader.tsx
index 40de5e5..f826fdc 100644
--- a/src/app/[locale]/list/_components/AasListHeader.tsx
+++ b/src/app/[locale]/list/_components/AasListHeader.tsx
@@ -5,8 +5,10 @@ export default async function AasListHeader() {
const t = await getTranslations('aas-list');
return (
-
- {t('header')}
-
+ <>
+
+ {t('header')}
+
+ >
);
}
diff --git a/src/app/[locale]/list/_components/AasListTableRow.tsx b/src/app/[locale]/list/_components/AasListTableRow.tsx
index 0227218..1266b8f 100644
--- a/src/app/[locale]/list/_components/AasListTableRow.tsx
+++ b/src/app/[locale]/list/_components/AasListTableRow.tsx
@@ -1,15 +1,11 @@
-import { Box, Checkbox, Chip, TableCell, Typography } from '@mui/material';
+import { Box, Checkbox, TableCell, Typography } from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import { messages } from 'lib/i18n/localization';
-import { getProductClassId } from 'lib/util/ProductClassResolverUtil';
-import LabelOffIcon from '@mui/icons-material/LabelOff';
-import { AasListEntry } from 'lib/api/generated-api/clients.g';
-import { base64ToBlob, encodeBase64 } from 'lib/util/Base64Util';
+import { encodeBase64 } from 'lib/util/Base64Util';
import { useRouter } from 'next/navigation';
import { useAasOriginSourceState, useAasState } from 'components/contexts/CurrentAasContext';
import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
-import { ImageWithFallback } from './StyledImageWithFallBack';
-import { ProductClassChip } from 'app/[locale]/list/_components/ProductClassChip';
+import { ImageWithFallback } from 'components/basics/StyledImageWithFallBack';
import { tooltipText } from 'lib/util/ToolTipText';
import PictureTableCell from 'components/basics/listBasics/PictureTableCell';
import { ArrowForward } from '@mui/icons-material';
@@ -18,10 +14,13 @@ import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
import { getThumbnailFromShell } from 'lib/services/repository-access/repositorySearchActions';
import { isValidUrl } from 'lib/util/UrlUtil';
import { useState } from 'react';
-import { isSuccessWithFile } from 'lib/util/apiResponseWrapper/apiResponseWrapperUtil';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { ListEntityDto } from 'lib/services/list-service/ListService';
+import { useTranslations } from 'next-intl';
type AasTableRowProps = {
- aasListEntry: AasListEntry;
+ repositoryUrl: string;
+ aasListEntry: ListEntityDto;
comparisonFeatureFlag: boolean | undefined;
checkBoxDisabled: (aasId: string | undefined) => boolean | undefined;
selectedAasList: string[] | undefined;
@@ -34,35 +33,49 @@ const tableBodyText = {
color: 'text.primary',
};
export const AasListTableRow = (props: AasTableRowProps) => {
- const { aasListEntry, comparisonFeatureFlag, checkBoxDisabled, selectedAasList, updateSelectedAasList } = props;
+ const {
+ repositoryUrl,
+ aasListEntry,
+ comparisonFeatureFlag,
+ checkBoxDisabled,
+ selectedAasList,
+ updateSelectedAasList,
+ } = props;
const navigate = useRouter();
const intl = useIntl();
const [, setAas] = useAasState();
const [, setAasOriginUrl] = useAasOriginSourceState();
const notificationSpawner = useNotificationSpawner();
const [thumbnailUrl, setThumbnailUrl] = useState('');
- const navigateToAas = (listEntry: AasListEntry) => {
+ const t = useTranslations('aas-list');
+
+ const navigateToAas = (listEntry: ListEntityDto) => {
setAas(null);
setAasOriginUrl(null);
if (listEntry.aasId) navigate.push(`/viewer/${encodeBase64(listEntry.aasId)}`);
};
- const translateListText = (property: { [key: string]: string } | undefined) => {
- if (!property) return '';
- return property[intl.locale] ?? Object.values(property)[0] ?? '';
- };
+ /* const translateListText = (property: { [key: string]: string } | undefined) => {
+ if (!property) return '';
+ return property[intl.locale] ?? Object.values(property)[0] ?? '';
+ };*/
useAsyncEffect(async () => {
- if (isValidUrl(aasListEntry.thumbnailUrl ?? '')) {
- setThumbnailUrl(aasListEntry.thumbnailUrl ?? '');
- } else if (aasListEntry.aasId) {
- const response = await getThumbnailFromShell(aasListEntry.aasId);
- if (isSuccessWithFile(response)) {
- const blobUrl = URL.createObjectURL(base64ToBlob(response.result, response.fileType));
+ if (!aasListEntry.thumbnail) {
+ return;
+ }
+
+ if (isValidUrl(aasListEntry.thumbnail)) {
+ setThumbnailUrl(aasListEntry.thumbnail);
+ } else if (aasListEntry.aasId && repositoryUrl) {
+ const response = await getThumbnailFromShell(aasListEntry.aasId, repositoryUrl);
+ if (response.isSuccess) {
+ const blob = mapFileDtoToBlob(response.result);
+ const blobUrl = URL.createObjectURL(blob);
setThumbnailUrl(blobUrl);
}
}
- }, [aasListEntry.thumbnailUrl]);
+ }, [aasListEntry.thumbnail]);
const showMaxElementsNotification = () => {
notificationSpawner.spawn({
@@ -99,23 +112,23 @@ export const AasListTableRow = (props: AasTableRowProps) => {
- {translateListText(aasListEntry.manufacturerName)}
+ {/*{translateListText(aasListEntry.manufacturerName)}*/}
- {tooltipText(translateListText(aasListEntry.manufacturerProductDesignation), 80)}
+ {/*{tooltipText(translateListText(aasListEntry.manufacturerProductDesignation), 80)}*/}
-
-
+
+ {tooltipText(aasListEntry.assetId, 35)}
- {tooltipText(aasListEntry.assetId, 80)}
-
-
+
+
+
+ {tooltipText(aasListEntry.aasId, 35)}
- {tooltipText(aasListEntry.aasId, 80)}
- {aasListEntry.productGroup ? (
+ {/* {aasListEntry.productGroup ? (
) : (
{
data-testid="product-class-chip"
title={intl.formatMessage(messages.mnestix.aasList.titleProductChipNotAvailable)}
/>
- )}
+ )}*/}
>;
+};
+
+export const SelectProductType = (props: SelectProductTypeProps) => {
+ const { aasList, setAasListFiltered } = props;
+ const [productClassFilterValue, setProductClassFilterValue] = useState('');
+ const [productClass, setProductClass] = useState([]);
+ const [filteredAasListCount,] = useState(0);
+ const intl = useIntl();
+ /**
+ * Creates the ProductClass Filter values.
+ */
+ useEffect(() => {
+ const productClasses: ProductClass[] = [];
+ if (aasList) {
+ /* aasList.forEach((aas) => {
+ if (!aas.productGroup) return;
+ const productClassId = getProductClassId(aas.productGroup);
+ const productClassString = translateProductClassId(productClassId, intl);
+ const productClass = parseProductClassFromString(productClassId, productClassString);
+ if (!productClasses.find((element) => element.id === productClass.id)) {
+ productClasses.push(productClass);
+ }
+ });*/
+ }
+ setProductClass(productClasses);
+ }, [aasList]);
+
+ const translateProductClassId = (id: string, intl: IntlShape) => {
+ let productClassString = id;
+ try {
+ productClassString = intl.formatMessage(messages.mnestix.aasList.productClasses[id]);
+ } catch (e) {
+ console.warn('Invalid product type', e);
+ }
+ return productClassString;
+ };
+ /**
+ * Applies product filter change to the list.
+ * @param event
+ */
+ const handleFilterChange = (event: SelectChangeEvent) => {
+ setProductClassFilterValue(event.target.value);
+ if (!aasList) return;
+ if (event.target.value === '') {
+ setAasListFiltered(aasList);
+ } else {
+ /* const filteredList = aasList.filter((aas) => {
+ return aas.productGroup && aas.productGroup.startsWith(event.target.value);
+ });
+ setAasListFiltered(filteredList);
+ setfilteredAasListCount(filteredList.length); */
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ }
+ data-testid="product-class-select"
+ onChange={handleFilterChange}
+ variant="standard">
+
+ {productClass.map((productType) => {
+ return (
+
+ );
+ })}
+
+
+ {productClassFilterValue != '' && (
+
+ {' '}
+ {filteredAasListCount} {intl.formatMessage(messages.mnestix.aasList.productClassHint)}:{' '}
+ {translateProductClassId(productClassFilterValue, intl)}
+
+ )}
+ >
+ );
+};
diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx b/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx
new file mode 100644
index 0000000..8491410
--- /dev/null
+++ b/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx
@@ -0,0 +1,33 @@
+import { expect } from '@jest/globals';
+import { CustomRender } from 'test-utils/CustomRender';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { SelectRepository } from 'app/[locale]/list/_components/filter/SelectRepository';
+import * as connectionServerActions from 'lib/services/database/connectionServerActions';
+
+jest.mock('./../../../../../lib/services/database/connectionServerActions');
+
+describe('SelectRepository', () => {
+ it('Fires repository changed event when changing the select value', async () => {
+ const mockDB = jest.fn(() => {
+ return ['https://test-repository.de'];
+ });
+ const repositoryChanged = jest.fn();
+ (connectionServerActions.getConnectionDataByTypeAction as jest.Mock).mockImplementation(mockDB);
+ CustomRender(
+ {
+ repositoryChanged();
+ }}
+ />,
+ );
+
+ await waitFor(() => screen.getByTestId('repository-select'));
+ const select = screen.getByRole('combobox');
+ fireEvent.mouseDown(select);
+
+ const firstElement = screen.getAllByRole('option')[0];
+ fireEvent.click(firstElement);
+
+ expect(repositoryChanged).toHaveBeenCalled();
+ });
+});
diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.tsx b/src/app/[locale]/list/_components/filter/SelectRepository.tsx
new file mode 100644
index 0000000..519f490
--- /dev/null
+++ b/src/app/[locale]/list/_components/filter/SelectRepository.tsx
@@ -0,0 +1,73 @@
+import { Dispatch, SetStateAction, useState } from 'react';
+import { Box, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Skeleton } from '@mui/material';
+import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
+import { getConnectionDataByTypeAction } from 'lib/services/database/connectionServerActions';
+import { ConnectionTypeEnum, getTypeAction } from 'lib/services/database/ConnectionTypeEnum';
+import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
+import { useEnv } from 'app/env/provider';
+import { useTranslations } from 'next-intl';
+
+export function SelectRepository(props: { onSelectedRepositoryChanged: Dispatch> }) {
+ const [aasRepositories, setAasRepositories] = useState([]);
+ const [selectedRepository, setSelectedRepository] = useState('');
+ const notificationSpawner = useNotificationSpawner();
+ const [isLoading, setIsLoading] = useState(false);
+ const t = useTranslations('aas-list');
+ const env = useEnv();
+
+ useAsyncEffect(async () => {
+ try {
+ setIsLoading(true);
+ const aasRepositories = await getConnectionDataByTypeAction(
+ getTypeAction(ConnectionTypeEnum.AAS_REPOSITORY),
+ );
+ if (env.AAS_REPO_API_URL) {
+ aasRepositories.push(env.AAS_REPO_API_URL);
+ setSelectedRepository(env.AAS_REPO_API_URL);
+ props.onSelectedRepositoryChanged(env.AAS_REPO_API_URL);
+ }
+ setAasRepositories(aasRepositories);
+ } catch (error) {
+ notificationSpawner.spawn({
+ message: error,
+ severity: 'error',
+ });
+ return;
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const onRepositoryChanged = (event: SelectChangeEvent) => {
+ setSelectedRepository(event.target.value);
+ props.onSelectedRepositoryChanged(event.target.value);
+ };
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ {t('repository-dropdown')}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/[locale]/list/page.tsx b/src/app/[locale]/list/page.tsx
index 9564e0d..24252c9 100644
--- a/src/app/[locale]/list/page.tsx
+++ b/src/app/[locale]/list/page.tsx
@@ -1,13 +1,24 @@
+'use client'
+
import { Box } from '@mui/material';
-import { AasListView } from 'app/[locale]/list/_components/AasListView';
+import { AasListViewDeprecated } from 'app/[locale]/list/_components-deprecated/AasListViewDeprecated';
import ListHeader from 'components/basics/ListHeader';
+import { useEnv } from 'app/env/provider';
+import AasListDataWrapper from './_components/AasListDataWrapper';
export default function Page() {
+ const env = useEnv();
+
+ /**
+ * Once the new list implementation is done:
+ * we can delete the "_component-deprecated" folder
+ * we can remove the "AAS_LIST_V2_FEATURE_FLAG" feature flag and the corresponding render condition.
+ */
return (
-
-
+
+ {env.AAS_LIST_V2_FEATURE_FLAG ? : }
);
diff --git a/src/app/[locale]/viewer/[base64AasId]/page.tsx b/src/app/[locale]/viewer/[base64AasId]/page.tsx
index 9481a4d..a9bada5 100644
--- a/src/app/[locale]/viewer/[base64AasId]/page.tsx
+++ b/src/app/[locale]/viewer/[base64AasId]/page.tsx
@@ -5,7 +5,7 @@ import { Box, Button, Skeleton, Typography } from '@mui/material';
import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
import { FormattedMessage, useIntl } from 'react-intl';
import { messages } from 'lib/i18n/localization';
-import { decodeBase64, safeBase64Decode } from 'lib/util/Base64Util';
+import { safeBase64Decode } from 'lib/util/Base64Util';
import { ArrowForward } from '@mui/icons-material';
import { showError } from 'lib/util/ErrorHandlerUtil';
import { LangStringNameType, Reference } from '@aas-core-works/aas-core3.0-typescript/types';
@@ -48,7 +48,7 @@ export default function Page() {
const [aasFromContext, setAasFromContext] = useAasState();
const [submodels, setSubmodels] = useSubmodelState();
const [isSubmodelsLoading, setIsSubmodelsLoading] = useState(true);
- const [registryAasData] = useRegistryAasState();
+ const [registryAasData, setRegistryAasData] = useRegistryAasState();
useAsyncEffect(async () => {
await fetchSubmodels();
@@ -84,6 +84,7 @@ export default function Page() {
showError(new LocalizedError(messages.mnestix.aasUrlNotFound), notificationSpawner);
} else if (result.aas) {
setAasOriginUrl(result.aasData?.aasRepositoryOrigin ?? null);
+ setRegistryAasData(result.aasData);
setAasFromContext(result.aas);
} else {
navigate.push(result.redirectUrl);
@@ -214,7 +215,7 @@ export default function Page() {
} href="/">
diff --git a/src/app/[locale]/viewer/_components/AASOverviewCard.tsx b/src/app/[locale]/viewer/_components/AASOverviewCard.tsx
index d8ad4f4..7d09c73 100644
--- a/src/app/[locale]/viewer/_components/AASOverviewCard.tsx
+++ b/src/app/[locale]/viewer/_components/AASOverviewCard.tsx
@@ -18,13 +18,13 @@ import { IconCircleWrapper } from 'components/basics/IconCircleWrapper';
import { AssetIcon } from 'components/custom-icons/AssetIcon';
import { ShellIcon } from 'components/custom-icons/ShellIcon';
import { isValidUrl } from 'lib/util/UrlUtil';
-import { base64ToBlob, encodeBase64 } from 'lib/util/Base64Util';
+import { encodeBase64 } from 'lib/util/Base64Util';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
import { useRouter } from 'next/navigation';
-import { useAasOriginSourceState, useAasState } from 'components/contexts/CurrentAasContext';
-import { ImageWithFallback } from 'app/[locale]/list/_components/StyledImageWithFallBack';
+import { useAasState } from 'components/contexts/CurrentAasContext';
+import { ImageWithFallback } from 'components/basics/StyledImageWithFallBack';
import { getThumbnailFromShell } from 'lib/services/repository-access/repositorySearchActions';
-import { isSuccessWithFile } from 'lib/util/apiResponseWrapper/apiResponseWrapperUtil';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
type AASOverviewCardProps = {
readonly aas: AssetAdministrationShell | null;
@@ -60,21 +60,24 @@ export function AASOverviewCard(props: AASOverviewCardProps) {
const isAccordion = props.isAccordion;
const specificAssetIds = props.aas?.assetInformation?.specificAssetIds as SpecificAssetId[];
const navigate = useRouter();
- const [productImageUrl, setProductImageUrl] = useState('');
+ const [productImageUrl, setProductImageUrl] = useState('');
const [, setAasState] = useAasState();
- const [aasOriginUrl] = useAasOriginSourceState();
async function createAndSetUrlForImageFile() {
if (!props.aas) return;
- let image: Blob;
- const response = await getThumbnailFromShell(props.aas.id, aasOriginUrl);
- if (isSuccessWithFile(response)) image = base64ToBlob(response.result, response.fileType);
- else {
+ if (!props.repositoryURL) {
+ setProductImageUrl('');
+ return;
+ }
+
+ const response = await getThumbnailFromShell(props.aas.id, props.repositoryURL);
+ if (!response.isSuccess) {
console.error('Image not found');
return;
}
- setProductImageUrl(URL.createObjectURL(image));
+ const blob = mapFileDtoToBlob(response.result);
+ setProductImageUrl(URL.createObjectURL(blob));
}
useAsyncEffect(async () => {
diff --git a/src/app/[locale]/viewer/_components/submodel-elements/document-component/DocumentComponent.tsx b/src/app/[locale]/viewer/_components/submodel-elements/document-component/DocumentComponent.tsx
index 3b5dae8..9b08c82 100644
--- a/src/app/[locale]/viewer/_components/submodel-elements/document-component/DocumentComponent.tsx
+++ b/src/app/[locale]/viewer/_components/submodel-elements/document-component/DocumentComponent.tsx
@@ -15,11 +15,11 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { getTranslationText, hasSemanticId } from 'lib/util/SubmodelResolverUtil';
import { DocumentDetailsDialog } from './DocumentDetailsDialog';
import { isValidUrl } from 'lib/util/UrlUtil';
-import { base64ToBlob, encodeBase64 } from 'lib/util/Base64Util';
+import { encodeBase64 } from 'lib/util/Base64Util';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
import { getAttachmentFromSubmodelElement } from 'lib/services/repository-access/repositorySearchActions';
-import { isSuccessWithFile } from 'lib/util/apiResponseWrapper/apiResponseWrapperUtil';
import { useAasOriginSourceState } from 'components/contexts/CurrentAasContext';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
enum DocumentSpecificSemanticId {
DocumentVersion = 'https://admin-shell.io/vdi/2770/1/0/DocumentVersion',
@@ -177,15 +177,13 @@ export function DocumentComponent(props: MarkingsComponentProps) {
const imageResponse = await getAttachmentFromSubmodelElement(
props.submodelId,
submodelElementPath,
- aasOriginUrl,
+ aasOriginUrl ?? undefined,
);
if (!imageResponse.isSuccess) {
console.error('Image not found' + imageResponse.message);
- } else if (isSuccessWithFile(imageResponse)) {
- digitalFile.digitalFileUrl = URL.createObjectURL(
- base64ToBlob(imageResponse.result, imageResponse.fileType),
- );
- digitalFile.mimeType = (versionSubmodelEl as File).contentType;
+ } else {
+ const image = mapFileDtoToBlob(imageResponse.result);
+ digitalFile.digitalFileUrl = URL.createObjectURL(image);
}
}
@@ -215,12 +213,13 @@ export function DocumentComponent(props: MarkingsComponentProps) {
const imageResponse = await getAttachmentFromSubmodelElement(
props.submodelId,
submodelElementPath,
- aasOriginUrl,
+ aasOriginUrl ?? undefined,
);
if (!imageResponse.isSuccess) {
console.error('Image not found' + imageResponse.message);
- } else if (isSuccessWithFile(imageResponse)) {
- previewImgUrl = URL.createObjectURL(base64ToBlob(imageResponse.result, imageResponse.fileType));
+ } else {
+ const image = mapFileDtoToBlob(imageResponse.result);
+ previewImgUrl = URL.createObjectURL(image);
}
}
diff --git a/src/app/[locale]/viewer/_components/submodel-elements/generic-elements/FileComponent.tsx b/src/app/[locale]/viewer/_components/submodel-elements/generic-elements/FileComponent.tsx
index 09c4e7b..5065216 100644
--- a/src/app/[locale]/viewer/_components/submodel-elements/generic-elements/FileComponent.tsx
+++ b/src/app/[locale]/viewer/_components/submodel-elements/generic-elements/FileComponent.tsx
@@ -7,9 +7,8 @@ import { getSanitizedHref } from 'lib/util/HrefUtil';
import { isValidUrl } from 'lib/util/UrlUtil';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
import { getAttachmentFromSubmodelElement } from 'lib/services/repository-access/repositorySearchActions';
-import { base64ToBlob } from 'lib/util/Base64Util';
-import { isSuccessWithFile } from 'lib/util/apiResponseWrapper/apiResponseWrapperUtil';
import { useAasOriginSourceState } from 'components/contexts/CurrentAasContext';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
const StyledFileImg = styled('img')(() => ({
objectFit: 'contain',
@@ -37,15 +36,13 @@ export function FileComponent(props: FileComponentProps) {
const imageResponse = await getAttachmentFromSubmodelElement(
props.submodelId,
props.submodelElementPath,
- aasOriginUrl,
+ aasOriginUrl ?? undefined,
);
if (!imageResponse.isSuccess) {
console.error('Image not found' + imageResponse.message);
- } else if (isSuccessWithFile(imageResponse)) {
- const imageObjectURL = URL.createObjectURL(
- base64ToBlob(imageResponse.result, imageResponse.fileType),
- );
- setImage(imageObjectURL);
+ } else {
+ const image = mapFileDtoToBlob(imageResponse.result);
+ setImage(URL.createObjectURL(image));
}
}
}
diff --git a/src/app/[locale]/viewer/_components/submodel-elements/marking-components/SingleMarkingsComponent.tsx b/src/app/[locale]/viewer/_components/submodel-elements/marking-components/SingleMarkingsComponent.tsx
index a7ce7fd..922d93d 100644
--- a/src/app/[locale]/viewer/_components/submodel-elements/marking-components/SingleMarkingsComponent.tsx
+++ b/src/app/[locale]/viewer/_components/submodel-elements/marking-components/SingleMarkingsComponent.tsx
@@ -4,9 +4,8 @@ import { useState } from 'react';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
import { isValidUrl } from 'lib/util/UrlUtil';
import { getAttachmentFromSubmodelElement } from 'lib/services/repository-access/repositorySearchActions';
-import { base64ToBlob } from 'lib/util/Base64Util';
-import { isSuccessWithFile } from 'lib/util/apiResponseWrapper/apiResponseWrapperUtil';
import { useAasOriginSourceState } from 'components/contexts/CurrentAasContext';
+import { mapFileDtoToBlob } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
type SingleMarkingsComponentProps = {
readonly file?: File;
@@ -42,13 +41,16 @@ export function SingleMarkingsComponent(props: SingleMarkingsComponentProps) {
useAsyncEffect(async () => {
if (!isValidUrl(file!.value)) {
const fileIdShort = idShortPath + '.' + file?.idShort;
- const response = await getAttachmentFromSubmodelElement(submodelId!, fileIdShort, aasOriginUrl);
- let image: Blob;
- if (isSuccessWithFile(response)) {
- image = base64ToBlob(response.result, response.fileType);
- setMarkingImage(URL.createObjectURL(image));
- } else {
+ const imageResponse = await getAttachmentFromSubmodelElement(
+ submodelId!,
+ fileIdShort,
+ aasOriginUrl ?? undefined,
+ );
+ if (!imageResponse.isSuccess) {
console.error('Image not found for file ID: ' + fileIdShort);
+ } else {
+ const image = mapFileDtoToBlob(imageResponse.result);
+ setMarkingImage(URL.createObjectURL(image));
}
} else {
if (file?.value) setMarkingImage(file.value);
diff --git a/src/app/[locale]/viewer/_components/submodel/SubmodelDetail.spec.tsx b/src/app/[locale]/viewer/_components/submodel/SubmodelDetail.spec.tsx
new file mode 100644
index 0000000..65d5042
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/SubmodelDetail.spec.tsx
@@ -0,0 +1,49 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { SubmodelDetail } from 'app/[locale]/viewer/_components/submodel/SubmodelDetail';
+import testSubmodel from '../submodel/carbon-footprint/test-submodel/carbonFootprint-test.json';
+import { Submodel } from '@aas-core-works/aas-core3.0-typescript/types';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+jest.mock('recharts', () => {
+ const OriginalRechartsModule = jest.requireActual('recharts');
+
+ return {
+ ...OriginalRechartsModule,
+ ResponsiveContainer: ({ height, children }: never) => (
+
+ {children}
+
+ ),
+ };
+});
+
+jest.mock('next-auth', jest.fn());
+
+describe('Submodel Detail', () => {
+ it('should render CarbonFootprintVisualizations for irdi id', async () => {
+ CustomRenderReactIntl(
+ ,
+ );
+ const map = screen.getByTestId('carbonFootprintVisualizations');
+ expect(map).toBeDefined();
+ expect(map).toBeInTheDocument();
+ });
+
+ it('should render CarbonFootprintVisualizations for URL id', async () => {
+ CustomRenderReactIntl(
+ ,
+ );
+ const map = screen.getByTestId('carbonFootprintVisualizations');
+ expect(map).toBeDefined();
+ expect(map).toBeInTheDocument();
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.spec.tsx
new file mode 100644
index 0000000..47f7e5d
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.spec.tsx
@@ -0,0 +1,72 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import testSubmodel from '../../submodel/carbon-footprint/test-submodel/carbonFootprint-test.json';
+import { Submodel } from '@aas-core-works/aas-core3.0-typescript/types';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import {
+ CarbonFootprintVisualizations
+} from 'app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+jest.mock('recharts', () => {
+ const OriginalRechartsModule = jest.requireActual('recharts');
+
+ return {
+ ...OriginalRechartsModule,
+ ResponsiveContainer: ({ height, children }: never) => (
+
+ {children}
+
+ ),
+ };
+});
+
+describe('CarbonFootprintVisualizations Detail', () => {
+ it('should render all submodel visualilzations for irdi id', async () => {
+ CustomRenderReactIntl(
+ ,
+ );
+ assertOnElements();
+ });
+
+ it('should render all submodel visualilzations for URL id', async () => {
+ CustomRenderReactIntl(
+ ,
+ );
+ assertOnElements();
+ });
+});
+
+function assertOnElements() {
+ const totalCo2Equivalents = screen.getByTestId('co2-equivalents');
+ expect(totalCo2Equivalents).toBeDefined();
+ expect(totalCo2Equivalents).toBeInTheDocument();
+
+ const productLifecycle = screen.getByTestId('product-lifecycle-stepper');
+ expect(productLifecycle).toBeDefined();
+ expect(productLifecycle).toBeInTheDocument();
+
+ const co2EquivalentsDistribution = screen.getByTestId('co2-equivalents-distribution-box');
+ expect(co2EquivalentsDistribution).toBeDefined();
+ expect(co2EquivalentsDistribution).toBeInTheDocument();
+
+ const co2Comparison = screen.getByTestId('co2-comparison-box');
+ expect(co2Comparison).toBeDefined();
+ expect(co2Comparison).toBeInTheDocument();
+
+ const productJourney = screen.getByTestId('product-journey-box');
+ expect(productJourney).toBeDefined();
+ expect(productJourney).toBeInTheDocument();
+
+ const calculationMethod = screen.getByTestId('co2-calculation-method-text');
+ expect(calculationMethod).toBeDefined();
+ expect(calculationMethod).toBeInTheDocument();
+}
+
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.tsx
index 2ac6ab9..b3e1002 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/CarbonFootprintVisualizations.tsx
@@ -53,7 +53,7 @@ export function CarbonFootprintVisualizations(props: { submodel: Submodel }) {
const calculationMethod = extractCalculationMethod(pcfSubmodelElements);
return (
-
+
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/test-submodel/carbonFootprint-test.json b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/test-submodel/carbonFootprint-test.json
new file mode 100644
index 0000000..79c2119
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/test-submodel/carbonFootprint-test.json
@@ -0,0 +1,2850 @@
+{
+ "carbonFootprint-IrdiId": {
+ "idShort": "CarbonFootprint",
+ "id": "https://i40.xitaso.com/submodel/testCarbonFootprint_00",
+ "dataSpecification": [],
+ "embeddedDataSpecifications": [],
+ "modelType": "Submodel",
+ "kind": "Instance",
+ "submodelElements": [
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE716#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ProductCarbonFootprint A4",
+ "value": [
+ {
+ "value": "GHG Protocol",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG854#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCalculationMethod",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Standard, method for determining the greenhouse gas emissions of a product"
+ }
+ ]
+ },
+ {
+ "value": "0.123",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG855#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCO2eq",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard"
+ }
+ ]
+ },
+ {
+ "value": "piece",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG856#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFReferenceValueForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity unit of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "1",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG857#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFQuantityOfMeasureForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "A4 - transport to final destination",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG858#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFLiveCyclePhase",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "OneToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABI497#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFGoodsAddressHandover",
+ "value": [
+ {
+ "value": "TestStr",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH956#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Street",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Street indication of the place of transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "35",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH957#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "HouseNumber",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Number for identification or differentiation of individual houses of a street"
+ }
+ ]
+ },
+ {
+ "value": "12345",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH958#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ZipCode",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Zip code of the goods transfer address"
+ }
+ ]
+ },
+ {
+ "value": "TestTown",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH959#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "CityTown",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indication of the city or town of the transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Germany",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-AAO259#005"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Country",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Country where the product is transmitted"
+ }
+ ]
+ },
+ {
+ "value": "48.389832",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH960#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Latitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Latitude (B), also called geodetic latitude or latitude (Latin latitudo, English latitude, international abbreviation Lat. or LAT), is the northerly or southerly distance of a point on the earth's surface from the equator, given in angular measure in the unit of measurement degrees"
+ }
+ ]
+ },
+ {
+ "value": "10.887690",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH961#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Longitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Geographic longitude, also called longitude (Latin longitudo, English longitude, international abbreviation long or LON), describes one of the two coordinates of a location on the earth's surface, namely its position east or west of a defined (arbitrarily determined) north-south line, the prime meridian"
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indicates the place of hand-over of the goods "
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE716#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ProductCarbonFootprint A1",
+ "value": [
+ {
+ "value": "GHG Protocol",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG854#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCalculationMethod",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Standard, method for determining the greenhouse gas emissions of a product"
+ }
+ ]
+ },
+ {
+ "value": "0.235",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG855#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCO2eq",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard"
+ }
+ ]
+ },
+ {
+ "value": "piece",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG856#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFReferenceValueForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity unit of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "1",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG857#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFQuantityOfMeasureForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "A1 - raw material supply (and upstream production)",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG858#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFLiveCyclePhase",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "OneToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABI497#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFGoodsAddressHandover",
+ "value": [
+ {
+ "value": "TestStr",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH956#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Street",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Street indication of the place of transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "122",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH957#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "HouseNumber",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Number for identification or differentiation of individual houses of a street"
+ }
+ ]
+ },
+ {
+ "value": "12345",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH958#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ZipCode",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Zip code of the goods transfer address"
+ }
+ ]
+ },
+ {
+ "value": "TestTown",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH959#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "CityTown",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indication of the city or town of the transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Germany",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-AAO259#005"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Country",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Country where the product is transmitted"
+ }
+ ]
+ },
+ {
+ "value": "51.151277079867036",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH960#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Latitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Latitude (B), also called geodetic latitude or latitude (Latin latitudo, English latitude, international abbreviation Lat. or LAT), is the northerly or southerly distance of a point on the earth's surface from the equator, given in angular measure in the unit of measurement degrees"
+ }
+ ]
+ },
+ {
+ "value": "6.777799507862787",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH961#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Longitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Geographic longitude, also called longitude (Latin longitudo, English longitude, international abbreviation long or LON), describes one of the two coordinates of a location on the earth's surface, namely its position east or west of a defined (arbitrarily determined) north-south line, the prime meridian"
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indicates the place of hand-over of the goods "
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE716#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ProductCarbonFootprint A3",
+ "value": [
+ {
+ "value": "GHG Protocol",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG854#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCalculationMethod",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Standard, method for determining the greenhouse gas emissions of a product"
+ }
+ ]
+ },
+ {
+ "value": "0.823",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG855#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCO2eq",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard"
+ }
+ ]
+ },
+ {
+ "value": "piece",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG856#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFReferenceValueForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity unit of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "1",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG857#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFQuantityOfMeasureForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "A3 - production",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG858#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFLiveCyclePhase",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "OneToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABI497#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFGoodsAddressHandover",
+ "value": [
+ {
+ "value": "TestStr",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH956#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Street",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Street indication of the place of transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Halle 8",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH957#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "HouseNumber",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Number for identification or differentiation of individual houses of a street"
+ }
+ ]
+ },
+ {
+ "value": "12345",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH958#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ZipCode",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Zip code of the goods transfer address"
+ }
+ ]
+ },
+ {
+ "value": "TestTown",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH959#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "CityTown",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indication of the city or town of the transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Germany",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-AAO259#005"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Country",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Country where the product is transmitted"
+ }
+ ]
+ },
+ {
+ "value": "52.322049093658464",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH960#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Latitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Latitude (B), also called geodetic latitude or latitude (Latin latitudo, English latitude, international abbreviation Lat. or LAT), is the northerly or southerly distance of a point on the earth's surface from the equator, given in angular measure in the unit of measurement degrees"
+ }
+ ]
+ },
+ {
+ "value": "9.81150344024394",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH961#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Longitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Geographic longitude, also called longitude (Latin longitudo, English longitude, international abbreviation long or LON), describes one of the two coordinates of a location on the earth's surface, namely its position east or west of a defined (arbitrarily determined) north-south line, the prime meridian"
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indicates the place of hand-over of the goods "
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE716#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ProductCarbonFootprint A2",
+ "value": [
+ {
+ "value": "GHG Protocol",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG854#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCalculationMethod",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Standard, method for determining the greenhouse gas emissions of a product"
+ }
+ ]
+ },
+ {
+ "value": "0.553",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG855#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCO2eq",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard"
+ }
+ ]
+ },
+ {
+ "value": "piece",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG856#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFReferenceValueForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity unit of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "1",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG857#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFQuantityOfMeasureForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "A2 - cradle-to-gate transport to factory",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG858#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFLiveCyclePhase",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "OneToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABI497#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFGoodsAddressHandover",
+ "value": [
+ {
+ "value": "TestStr",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH956#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Street",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Street indication of the place of transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "6",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH957#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "HouseNumber",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Number for identification or differentiation of individual houses of a street"
+ }
+ ]
+ },
+ {
+ "value": "12345",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH958#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ZipCode",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Zip code of the goods transfer address"
+ }
+ ]
+ },
+ {
+ "value": "TestTown",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH959#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "CityTown",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indication of the city or town of the transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Germany",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-AAO259#005"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Country",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Country where the product is transmitted"
+ }
+ ]
+ },
+ {
+ "value": "52.42771449186328",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH960#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Latitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Latitude (B), also called geodetic latitude or latitude (Latin latitudo, English latitude, international abbreviation Lat. or LAT), is the northerly or southerly distance of a point on the earth's surface from the equator, given in angular measure in the unit of measurement degrees"
+ }
+ ]
+ },
+ {
+ "value": "9.613097399392698",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH961#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Longitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Geographic longitude, also called longitude (Latin longitudo, English longitude, international abbreviation long or LON), describes one of the two coordinates of a location on the earth's surface, namely its position east or west of a defined (arbitrarily determined) north-south line, the prime meridian"
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indicates the place of hand-over of the goods "
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use"
+ }
+ ]
+ }
+ ],
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE712#001"
+ }
+ ],
+ "type": "ExternalReference"
+ }
+ },
+ "carbonFootprint-UrlId": {
+ "idShort": "CarbonFootprint",
+ "id": "https://i40.xitaso.com/submodel/testCarbonFootprint_01",
+ "dataSpecification": [],
+ "embeddedDataSpecifications": [],
+ "modelType": "Submodel",
+ "kind": "Instance",
+ "submodelElements": [
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#01-AHE716#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ProductCarbonFootprint A4",
+ "value": [
+ {
+ "value": "GHG Protocol",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG854#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCalculationMethod",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Standard, method for determining the greenhouse gas emissions of a product"
+ }
+ ]
+ },
+ {
+ "value": "0.123",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG855#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFCO2eq",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard"
+ }
+ ]
+ },
+ {
+ "value": "piece",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG856#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFReferenceValueForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity unit of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "1",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG857#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFQuantityOfMeasureForCalculation",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Quantity of the product to which the PCF information on the CO2 footprint refers"
+ }
+ ]
+ },
+ {
+ "value": "A4 - transport to final destination",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABG858#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFLiveCyclePhase",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "OneToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers"
+ }
+ ]
+ },
+ {
+ "ordered": false,
+ "allowDuplicates": false,
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABI497#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "PCFGoodsAddressHandover",
+ "value": [
+ {
+ "value": "TestStr",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH956#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Street",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Street indication of the place of transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "35",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH957#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "HouseNumber",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Number for identification or differentiation of individual houses of a street"
+ }
+ ]
+ },
+ {
+ "value": "12345",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH958#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "ZipCode",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Zip code of the goods transfer address"
+ }
+ ]
+ },
+ {
+ "value": "TestTown",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH959#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "CityTown",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indication of the city or town of the transfer of goods"
+ }
+ ]
+ },
+ {
+ "value": "Germany",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-AAO259#005"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Country",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Country where the product is transmitted"
+ }
+ ]
+ },
+ {
+ "value": "48.389832",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH960#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Latitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Latitude (B), also called geodetic latitude or latitude (Latin latitudo, English latitude, international abbreviation Lat. or LAT), is the northerly or southerly distance of a point on the earth's surface from the equator, given in angular measure in the unit of measurement degrees"
+ }
+ ]
+ },
+ {
+ "value": "10.887690",
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "0173-1#02-ABH961#001"
+ }
+ ],
+ "type": "ExternalReference"
+ },
+ "idShort": "Longitude",
+ "category": "PARAMETER",
+ "valueType": "xs:string",
+ "kind": "Instance",
+ "modelType": "Property",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToOne"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Geographic longitude, also called longitude (Latin longitudo, English longitude, international abbreviation long or LON), describes one of the two coordinates of a location on the earth's surface, namely its position east or west of a defined (arbitrarily determined) north-south line, the prime meridian"
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "One"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Indicates the place of hand-over of the goods "
+ }
+ ]
+ }
+ ],
+ "kind": "Instance",
+ "modelType": "SubmodelElementCollection",
+ "qualifiers": [
+ {
+ "semanticId": {
+ "type": "ExternalReference",
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0"
+ }
+ ]
+ },
+ "kind": "TemplateQualifier",
+ "type": "SMT/Cardinality",
+ "valueType": "xs:string",
+ "value": "ZeroToMany"
+ }
+ ],
+ "description": [
+ {
+ "language": "en",
+ "text": "Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use"
+ }
+ ]
+ }
+ ],
+ "semanticId": {
+ "keys": [
+ {
+ "type": "GlobalReference",
+ "value": "https://admin-shell.io/idta/CarbonFootprint/CarbonFootprint/0/9"
+ }
+ ],
+ "type": "ExternalReference"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.spec.tsx
new file mode 100644
index 0000000..9992f6f
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.spec.tsx
@@ -0,0 +1,47 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import { CO2EBarchart } from 'app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+jest.mock('recharts', () => {
+ const OriginalRechartsModule = jest.requireActual('recharts');
+
+ return {
+ ...OriginalRechartsModule,
+ ResponsiveContainer: ({ children }: never) => (
+
+ {children}
+
+ ),
+ };
+});
+
+const co2EquivalentsPerLifecycleStage = {
+ A1: 0.235,
+ A2: 0.553,
+ A3: 0.823,
+ A4: 0.123,
+};
+
+describe('CarbonFootprint - CO2EquivalentsDistribution', () => {
+ it('should renders correct axis descriptions', async () => {
+ CustomRenderReactIntl();
+ expect(screen.getByText('kg CO2e')).toBeInTheDocument();
+ expect(screen.getByText('CO2 Equivalents')).toBeInTheDocument();
+ expect(screen.getByText('A3')).toBeInTheDocument();
+ expect(screen.getByText('A2')).toBeInTheDocument();
+ expect(screen.getByText('A1')).toBeInTheDocument();
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.tsx
index a43b64d..f4b727f 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionBarchart.tsx
@@ -50,7 +50,7 @@ export function CO2EBarchart(props: {
};
return (
-
+
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionList.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionList.tsx
index c035e4f..ad971c0 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionList.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EDistributionDiagrams/CO2EDistributionList.tsx
@@ -3,6 +3,7 @@ import { ProductLifecycleStage } from 'lib/enums/ProductLifecycleStage.enum';
import { messages } from 'lib/i18n/localization';
import { useIntl } from 'react-intl';
import { cutDecimalPlaces } from 'lib/util/NumberUtil';
+import React from 'react';
export function CO2EList(props: { co2EquivalentsPerLifecycleStage: Partial> }) {
const intl = useIntl();
@@ -23,23 +24,23 @@ export function CO2EList(props: { co2EquivalentsPerLifecycleStage: Partial co2EquivalentsPerLifecycleStage[b] - co2EquivalentsPerLifecycleStage[a])
.map((val, index) => (
- <>
-
+
+
{`${cutDecimalPlaces(
co2EquivalentsPerLifecycleStage[val],
3,
)} kg`}
-
+
{intl.formatMessage(messages.mnestix.productCarbonFootprint.lifecycleStages[val])}
- >
+
));
return (
-
+
{rows}
);
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.spec.tsx
new file mode 100644
index 0000000..63ba3ff
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.spec.tsx
@@ -0,0 +1,35 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import { CO2Equivalents } from 'app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents';
+
+describe('CarbonFootprint - CO2 Equivalents', () => {
+ it('should use correct style', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-equivalents');
+ expect(component).toBeDefined();
+ expect(component).toBeInTheDocument();
+ expect(component).toHaveStyle('color: rgb(25, 118, 210)');
+ expect(component).toHaveStyle('fontSize: [72, 96]');
+ expect(component).toHaveStyle('font-weight: 700');
+ expect(component).toHaveStyle('lineHeight: 1');
+ });
+
+ it('should display three digit number', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-equivalents');
+ expect(component).toHaveTextContent('7.125 kg');
+ });
+
+ it('should round to three digits', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-equivalents');
+ expect(component).toHaveTextContent('7.126 kg');
+ });
+
+ it('should not fill to three digits', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-equivalents');
+ expect(component).toHaveTextContent('7.1 kg');
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.tsx
index aeffafc..de3f4a0 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2Equivalents.tsx
@@ -5,7 +5,10 @@ const unit = 'kg';
export function CO2Equivalents(props: { totalCO2Equivalents: number }) {
return (
-
+
{`${cutDecimalPlaces(props.totalCO2Equivalents, 3)} ${unit}`}
);
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.spec.tsx
new file mode 100644
index 0000000..eb79f9d
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.spec.tsx
@@ -0,0 +1,76 @@
+import { fireEvent, screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import { CO2EquivalentsDistribution } from 'app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+jest.mock('recharts', () => {
+ const OriginalRechartsModule = jest.requireActual('recharts');
+
+ return {
+ ...OriginalRechartsModule,
+ ResponsiveContainer: ({ height, children }: never) => (
+
+ {children}
+
+ ),
+ };
+});
+
+const co2EquivalentsPerLifecycleStage = {
+ A1: 0.235,
+ A2: 0.553,
+ A3: 0.823,
+ A4: 0.123,
+};
+
+describe('CarbonFootprint - CO2EquivalentsDistribution', () => {
+ beforeEach(() => {
+ CustomRenderReactIntl(
+ ,
+ );
+ });
+
+ it('should render header with correct style', async () => {
+ const headerTypography = screen.getByTestId('co2-equivalents-totalEquivalents-typography');
+ expect(headerTypography).toBeDefined();
+ expect(headerTypography).toBeInTheDocument();
+ expect(headerTypography).toHaveStyle({
+ color: 'rgb(25, 118, 210)',
+ fontSize: '24',
+ });
+ expect(headerTypography.children[0]).toHaveStyle({
+ fontWeight: 600,
+ });
+ });
+
+ it('should render total CO2 equivalents correctly', async () => {
+ const headerTypography = screen.getByTestId('co2-equivalents-totalEquivalents-typography');
+ expect(headerTypography).toHaveTextContent('1.745 kg in total');
+ });
+
+ it('should render barchart as default', async () => {
+ expect(screen.getByTestId('co2-equivalents-distribution-box')).toBeInTheDocument();
+ expect(screen.queryByTestId('co2e-barchart')).toBeInTheDocument();
+ });
+
+ it('should switch between chart views', async () => {
+ expect(screen.getByTestId('co2-equivalents-distribution-box')).toBeInTheDocument();
+ expect(screen.queryByTestId('co2e-list')).toBeNull();
+
+ const listToggleButton = screen.getByTestId('list-toggle-button');
+ fireEvent.click(listToggleButton);
+
+ expect(screen.getByTestId('co2e-list')).toBeInTheDocument();
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.tsx
index 5df2822..98b8106 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CO2EquivalentsDistribution.tsx
@@ -33,9 +33,12 @@ export function CO2EquivalentsDistribution(props: {
};
return (
-
+
-
+
{`${cutDecimalPlaces(props.totalCO2Equivalents, 3)} ${unit} `}
@@ -50,7 +53,7 @@ export function CO2EquivalentsDistribution(props: {
-
+
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.spec.tsx
new file mode 100644
index 0000000..de48580
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.spec.tsx
@@ -0,0 +1,33 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import {
+ CalculationMethod,
+ LinkGHG,
+} from 'app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod';
+
+describe('Calculation Method', () => {
+ it('should use correct style', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-calculation-method-text');
+ expect(component).toBeDefined();
+ expect(component).toBeInTheDocument();
+ expect(component).toHaveStyle('color: rgb(25, 118, 210)');
+ expect(component).toHaveStyle('fontSize: [72, 96]');
+ expect(component).toHaveStyle('font-weight: 700');
+ });
+
+ it('should display the method', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-calculation-method-text');
+ expect(component).toHaveTextContent('Artificial Goon');
+ });
+
+ it('should link to GHG website', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-calculation-method-link');
+ expect(component).toBeDefined();
+ expect(component).toBeInTheDocument();
+ expect(component).toHaveAttribute('href', LinkGHG);
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.tsx
index 2070f20..636f518 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/CalculationMethod.tsx
@@ -1,16 +1,17 @@
import { Link, Typography } from '@mui/material';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
-const LinkGHG = 'https://ghgprotocol.org/';
+export const LinkGHG = 'https://ghgprotocol.org/';
export function CalculationMethod(props: { calculationMethod: string }) {
const { calculationMethod } = props;
return (
-
+
{calculationMethod}{' '}
{calculationMethod === 'GHG Protocol' && (
-
+
)}
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.spec.tsx
new file mode 100644
index 0000000..39e96a2
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.spec.tsx
@@ -0,0 +1,52 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import { Comparison } from './Comparison';
+
+describe('CarbonFootprint - CO2 Comparison', () => {
+ it('should use correct style', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-value');
+ expect(component).toBeDefined();
+ expect(component).toBeInTheDocument();
+ expect(component).toHaveStyle('color: rgb(25, 118, 210)');
+ expect(component).toHaveStyle('fontSize: 36');
+ expect(component).toHaveStyle('font-weight: 700');
+ });
+
+ it('should show years to one decimal', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-value');
+ expect(component).toHaveTextContent('1.3 Years');
+ });
+
+ it('should show months to one decimal', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-value');
+ expect(component).toHaveTextContent('1.7 Months');
+ });
+
+ it('should show months below 12 months', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-value');
+ expect(component).toHaveTextContent('12 Months');
+ });
+
+ it('should switch to years above 12 months', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-value');
+ expect(component).toHaveTextContent('1 Years');
+ });
+
+ it('should display the tree', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-tree');
+ expect(component).toBeInTheDocument();
+ });
+
+ it('should show the assumption text', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('co2-comparison-assumption');
+ expect(component).toBeInTheDocument();
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.tsx
index 7e9f3d2..d4e1a00 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/Comparison.tsx
@@ -18,20 +18,23 @@ export function Comparison(props: { co2Equivalents: number }) {
const { value: timePeriod, unit: unitOfTimePeriod } = determineTimePeriod(props.co2Equivalents);
return (
-
+
-
+
1 {intl.formatMessage(messages.mnestix.productCarbonFootprint.beech)}
-
+
{timePeriod} {intl.formatMessage(messages.mnestix.productCarbonFootprint[unitOfTimePeriod])}
-
+
{intl.formatMessage(messages.mnestix.productCarbonFootprint.comparisonAssumption)}
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.spec.tsx
new file mode 100644
index 0000000..c3b28a0
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.spec.tsx
@@ -0,0 +1,65 @@
+import { CustomRender } from 'test-utils/CustomRender';
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { AddressPerLifeCyclePhase, ProductJourney } from './ProductJourney';
+import { ProductLifecycleStage } from 'lib/enums/ProductLifecycleStage.enum';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+const firstAddress: AddressPerLifeCyclePhase = {
+ address: {
+ street: 'teststreet',
+ cityTown: 'testtowm',
+ houseNumber: '2',
+ country: 'testcountry',
+ zipCode: 'testzipcode',
+ latitude: 52.321705475489594,
+ longitude: 9.811600807768746,
+ },
+ lifeCyclePhase: ProductLifecycleStage.A3Production,
+};
+const secondAddress: AddressPerLifeCyclePhase = {
+ address: {
+ street: 'teststreet',
+ cityTown: 'testtowm',
+ houseNumber: '3',
+ country: 'testcountry',
+ zipCode: 'testzipcode',
+ latitude: 48.36854557956184,
+ longitude: 10.93445997546203,
+ },
+ lifeCyclePhase: ProductLifecycleStage.B6UsageEnergy,
+};
+
+describe('ProductJourney', () => {
+ it('renders the ProductJourney', async () => {
+
+ CustomRender();
+ const map = screen.getByTestId('product-journey-box');
+ expect(map).toBeDefined();
+ expect(map).toBeInTheDocument();
+
+ const addressList = screen.getAllByTestId('test-address-list');
+ expect(addressList).toBeDefined();
+ expect(addressList.length).toBe(2);
+ expect(addressList[0]).toBeInTheDocument();
+ expect(addressList[1]).toBeInTheDocument();
+ });
+
+ it('shows positions on the map', async () => {
+
+ CustomRender();
+
+ const map = screen.getByTestId('product-journey-box');
+ expect(map).toBeDefined();
+ expect(map).toBeInTheDocument();
+
+ expect(map.firstChild).toHaveClass('ol-viewport');
+ });
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.tsx
index 0e32b83..6e17b7e 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourney.tsx
@@ -90,6 +90,7 @@ export function ProductJourney(props: { addressesPerLifeCyclePhase: AddressPerLi
<>
-
+
>
);
}
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourneyAddressList.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourneyAddressList.tsx
index 7ac4c06..1565e64 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourneyAddressList.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductJourneyAddressList.tsx
@@ -6,7 +6,7 @@ export function ProductJourneyAddressList(props: { addressesPerLifeCyclePhase: A
.filter((v) => v.address.street && v.address.houseNumber && v.address.zipCode && v.address.cityTown)
.map((phase, index) => {
return (
-
+
{`${phase.lifeCyclePhase}:`}{' '}
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.spec.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.spec.tsx
new file mode 100644
index 0000000..ac7bcf3
--- /dev/null
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.spec.tsx
@@ -0,0 +1,60 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { ProductLifecycleStage } from 'lib/enums/ProductLifecycleStage.enum';
+import {
+ ProductLifecycle
+} from 'app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
+
+const completedStages =
+ [ProductLifecycleStage.A1RawMaterialSupply,
+ ProductLifecycleStage.A2CradleToGate,
+ ProductLifecycleStage.A3Production,
+ ProductLifecycleStage.A4TransportToFinalDestination
+ ] ;
+
+describe('ProductLifecycle', () => {
+ it('should render the ProductLifecycle with all steps', async () => {
+ CustomRenderReactIntl();
+ const stepper = screen.getByTestId('product-lifecycle-stepper');
+ expect(stepper).toBeDefined();
+ expect(stepper).toBeInTheDocument();
+
+ const completedSteps = screen.getAllByTestId('product-lifecycle-completed-step');
+ expect(completedSteps).toBeDefined();
+ expect(completedSteps.length).toBe(4);
+
+ completedSteps.forEach((el) => {
+ expect(el).toBeInTheDocument();
+ });
+
+ const nextSteps = screen.getAllByTestId('product-lifecycle-next-step');
+ expect(nextSteps).toBeDefined();
+ expect(nextSteps.length).toBe(1);
+ });
+
+ it('should render no completed steps if none are completed', async () => {
+ CustomRenderReactIntl();
+ const stepper = screen.getByTestId('product-lifecycle-stepper');
+ expect(stepper).toBeDefined();
+ expect(stepper).toBeInTheDocument();
+
+ const completedSteps= screen.queryByTestId('product-lifecycle-completed-step')
+ expect(completedSteps).toBeNull();
+
+ const addressList = screen.getAllByTestId('product-lifecycle-next-step');
+ expect(addressList).toBeDefined();
+ expect(addressList.length).toBe(1);
+ });
+
+
+});
diff --git a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.tsx b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.tsx
index 4270a32..d68897b 100644
--- a/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.tsx
+++ b/src/app/[locale]/viewer/_components/submodel/carbon-footprint/visualization-components/ProductLifecycle.tsx
@@ -27,11 +27,12 @@ export function ProductLifecycle(props: { completedStages: ProductLifecycleStage
activeStep={props.completedStages.length}
orientation="vertical"
sx={{ '& .Muistel-root': { color: 'blue' } }}
+ data-testid="product-lifecycle-stepper"
>
{props.completedStages.map((step, index) => (
-
+
-
+
{!!step &&
intl.formatMessage(messages.mnestix.productCarbonFootprint.lifecycleStages[step])}
@@ -39,8 +40,8 @@ export function ProductLifecycle(props: { completedStages: ProductLifecycleStage
))}
{nextStage && (
-
-
+
+
{intl.formatMessage(messages.mnestix.productCarbonFootprint.lifecycleStages[nextStage])}{' '}
(not yet included)
diff --git a/src/app/[locale]/viewer/_components/transfer/TargetRepositories.tsx b/src/app/[locale]/viewer/_components/transfer/TargetRepositories.tsx
index 03ca02e..a28490f 100644
--- a/src/app/[locale]/viewer/_components/transfer/TargetRepositories.tsx
+++ b/src/app/[locale]/viewer/_components/transfer/TargetRepositories.tsx
@@ -1,22 +1,11 @@
-import {
- Box,
- DialogActions,
- Divider,
- FormControl,
- MenuItem,
- Skeleton,
- TextField,
- Typography
-} from '@mui/material';
+import { Box, DialogActions, Divider, FormControl, MenuItem, Skeleton, TextField, Typography } from '@mui/material';
import { messages } from 'lib/i18n/localization';
import { FormattedMessage, useIntl } from 'react-intl';
-import {
- getConnectionDataByTypeAction
-} from 'lib/services/database/connectionServerActions';
+import { getConnectionDataByTypeAction } from 'lib/services/database/connectionServerActions';
import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
import { Fragment, useState } from 'react';
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
-import { ConnectionTypeEnum } from 'lib/services/database/ConnectionTypeEnum';
+import { ConnectionTypeEnum, getTypeAction } from 'lib/services/database/ConnectionTypeEnum';
import { Controller, useForm } from 'react-hook-form';
import { LoadingButton } from '@mui/lab';
import { useEnv } from 'app/env/provider';
@@ -43,10 +32,10 @@ export function TargetRepositories(props: TargetRepositoryProps) {
useAsyncEffect(async () => {
try {
setIsLoading(true);
- const aasRepositories = await getConnectionDataByTypeAction({ id: '0', typeName: ConnectionTypeEnum.AAS_REPOSITORY });
+ const aasRepositories = await getConnectionDataByTypeAction(getTypeAction(ConnectionTypeEnum.AAS_REPOSITORY));
if(env.AAS_REPO_API_URL) aasRepositories.push(env.AAS_REPO_API_URL)
setAasRepositories(aasRepositories);
- const submodelRepositories = await getConnectionDataByTypeAction({ id: '2', typeName: ConnectionTypeEnum.SUBMODEL_REPOSITORY });
+ const submodelRepositories = await getConnectionDataByTypeAction(getTypeAction(ConnectionTypeEnum.SUBMODEL_REPOSITORY));
if(env.SUBMODEL_REPO_API_URL) submodelRepositories.push(env.SUBMODEL_REPO_API_URL)
setSubmodelRepositories(submodelRepositories);
} catch(error) {
diff --git a/src/app/[locale]/viewer/_components/transfer/TransferDialog.tsx b/src/app/[locale]/viewer/_components/transfer/TransferDialog.tsx
index 86dfd2d..0dfcd62 100644
--- a/src/app/[locale]/viewer/_components/transfer/TransferDialog.tsx
+++ b/src/app/[locale]/viewer/_components/transfer/TransferDialog.tsx
@@ -20,7 +20,7 @@ import { useState } from 'react';
import { useAasOriginSourceState, useAasState, useSubmodelState } from 'components/contexts/CurrentAasContext';
import { transferAasWithSubmodels } from 'lib/services/transfer-service/transferActions';
import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
-import { TransferDto, TransferResult, TransferSubmodel } from 'lib/types/TransferServiceData';
+import { TransferAas, TransferDto, TransferResult, TransferSubmodel } from 'lib/types/TransferServiceData';
import { useEnv } from 'app/env/provider';
import { Reference } from '@aas-core-works/aas-core3.0-typescript/types';
@@ -28,6 +28,7 @@ export type TransferFormModel = {
targetAasRepositoryFormModel: TargetRepositoryFormData;
};
+// TODO pull aas and origin URLs into props
export function TransferDialog(props: DialogProps) {
const [transferDto, setTransferDto] = useState();
const [submodelsFromContext] = useSubmodelState();
@@ -42,12 +43,12 @@ export function TransferDialog(props: DialogProps) {
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
function buildTransferDto(values: TargetRepositoryFormData) {
- if (!values.repository || !aasFromContext) {
+ if (!values.repository || !aasFromContext || !aasOriginUrl) {
return;
}
// As long as we cannot adjust the IDs in the UI, we append '_copy' to every ID
- const submodelsToTransfer = submodelsFromContext
+ const submodelsToTransfer = structuredClone(submodelsFromContext)
.filter((sub) => sub.submodel)
.map((sub) => sub.submodel!)
.map((sub) => {
@@ -55,9 +56,12 @@ export function TransferDialog(props: DialogProps) {
submodelToTransfer.submodel.id = `${sub.id}_copy`;
return submodelToTransfer;
});
- const aasToTransfer = structuredClone(aasFromContext);
+ const aasToTransfer: TransferAas = {
+ aas: structuredClone(aasFromContext),
+ originalAasId: aasFromContext.id,
+ };
- aasToTransfer.id = `${aasFromContext.id}_copy`;
+ aasToTransfer.aas.id = `${aasFromContext.id}_copy`;
// Adapt Submodel References of the AAS
const submodelReferencesToTransfer: Reference[] = [];
@@ -71,18 +75,19 @@ export function TransferDialog(props: DialogProps) {
submodelReferencesToTransfer.push(newSubmodelReference);
}
});
- aasToTransfer.submodels = submodelReferencesToTransfer;
+ aasToTransfer.aas.submodels = submodelReferencesToTransfer;
+ // TODO use separate submodel URLs
const dtoToSubmit: TransferDto = {
- submodels: submodelsToTransfer,
aas: aasToTransfer,
- originalAasId: aasFromContext.id,
+ submodels: submodelsToTransfer,
targetAasRepositoryBaseUrl: values.repository,
sourceAasRepositoryBaseUrl: aasOriginUrl,
targetSubmodelRepositoryBaseUrl:
values.submodelRepository && values.submodelRepository !== '0'
? values.submodelRepository
: values.repository,
+ sourceSubmodelRepositoryBaseUrl: aasOriginUrl,
apikey: values.repositoryApiKey,
targetDiscoveryBaseUrl: env.DISCOVERY_API_URL,
};
diff --git a/src/app/[locale]/viewer/discovery/_components/DiscoveryListView.tsx b/src/app/[locale]/viewer/discovery/_components/DiscoveryListView.tsx
index 272d516..47bc85d 100644
--- a/src/app/[locale]/viewer/discovery/_components/DiscoveryListView.tsx
+++ b/src/app/[locale]/viewer/discovery/_components/DiscoveryListView.tsx
@@ -15,6 +15,7 @@ import ListHeader from 'components/basics/ListHeader';
import { performDiscoveryAasSearch, performRegistryAasSearch } from 'lib/services/search-actions/searchActions';
import { performSearchAasFromAllRepositories } from 'lib/services/repository-access/repositorySearchActions';
import { RepoSearchResult } from 'lib/services/repository-access/RepositorySearchService';
+import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript/types';
async function getRepositoryUrl(aasId: string): Promise {
const registrySearchResult = await performRegistryAasSearch(aasId);
@@ -61,12 +62,12 @@ export const DiscoveryListView = () => {
);
} else if (aasId) {
const response = await performSearchAasFromAllRepositories(encodeBase64(aasId));
- let searchResults: RepoSearchResult[] = [];
+ let searchResults: RepoSearchResult[] = [];
if (response.isSuccess) searchResults = response.result;
else setIsError(true);
for (const searchResult of searchResults) {
entryList.push({
- aasId: searchResult.aas.id,
+ aasId: searchResult.searchResult.id,
repositoryUrl: searchResult.location,
});
}
diff --git a/src/app/env/env.ts b/src/app/env/env.ts
index 2576b91..9a139c5 100644
--- a/src/app/env/env.ts
+++ b/src/app/env/env.ts
@@ -11,6 +11,7 @@ export const getEnv = async (): Promise => {
AUTHENTICATION_FEATURE_FLAG: false,
COMPARISON_FEATURE_FLAG: process.env.COMPARISON_FEATURE_FLAG?.toLowerCase() === 'true'.toLowerCase(),
AAS_LIST_FEATURE_FLAG: false,
+ AAS_LIST_V2_FEATURE_FLAG: process.env.AAS_LIST_V2_FEATURE_FLAG?.toLowerCase() === 'true'.toLowerCase(),
TRANSFER_FEATURE_FLAG: process.env.TRANSFER_FEATURE_FLAG?.toLowerCase() === 'true'.toLowerCase(),
};
@@ -32,6 +33,8 @@ export const getEnv = async (): Promise => {
SUBMODEL_REPO_API_URL: process.env.SUBMODEL_REPO_API_URL,
MNESTIX_BACKEND_API_URL: process.env.MNESTIX_BACKEND_API_URL,
KEYCLOAK_ENABLED: process.env.KEYCLOAK_ENABLED?.toLowerCase() === 'true'.toLowerCase(),
+ IMPRINT_URL: process.env.IMPRINT_URL,
+ DATA_PRIVACY_URL: process.env.DATA_PRIVACY_URL,
};
const themingVariables = {
@@ -65,6 +68,7 @@ export type EnvironmentalVariables = {
AUTHENTICATION_FEATURE_FLAG: boolean;
COMPARISON_FEATURE_FLAG: boolean;
AAS_LIST_FEATURE_FLAG: boolean;
+ AAS_LIST_V2_FEATURE_FLAG: boolean;
TRANSFER_FEATURE_FLAG: boolean;
DISCOVERY_API_URL: string | undefined;
REGISTRY_API_URL: string | undefined;
@@ -77,4 +81,6 @@ export type EnvironmentalVariables = {
THEME_BASE64_LOGO: string | undefined;
THEME_LOGO_URL: string | undefined;
KEYCLOAK_ENABLED: boolean;
+ IMPRINT_URL: string | undefined;
+ DATA_PRIVACY_URL: string | undefined;
};
diff --git a/src/app/env/provider.tsx b/src/app/env/provider.tsx
index 7cbe6ce..058f939 100644
--- a/src/app/env/provider.tsx
+++ b/src/app/env/provider.tsx
@@ -6,6 +6,7 @@ import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner
const initialValues: EnvironmentalVariables = {
AAS_LIST_FEATURE_FLAG: false,
+ AAS_LIST_V2_FEATURE_FLAG: false,
COMPARISON_FEATURE_FLAG: false,
TRANSFER_FEATURE_FLAG: false,
AUTHENTICATION_FEATURE_FLAG: false,
@@ -24,6 +25,8 @@ const initialValues: EnvironmentalVariables = {
THEME_BASE64_LOGO: undefined,
THEME_LOGO_URL: undefined,
KEYCLOAK_ENABLED: false,
+ IMPRINT_URL: '',
+ DATA_PRIVACY_URL: '',
};
export const EnvContext = createContext(initialValues);
diff --git a/src/app/[locale]/list/_components/StyledImageWithFallBack.tsx b/src/components/basics/StyledImageWithFallBack.tsx
similarity index 98%
rename from src/app/[locale]/list/_components/StyledImageWithFallBack.tsx
rename to src/components/basics/StyledImageWithFallBack.tsx
index 53419eb..7182add 100644
--- a/src/app/[locale]/list/_components/StyledImageWithFallBack.tsx
+++ b/src/components/basics/StyledImageWithFallBack.tsx
@@ -14,7 +14,7 @@ const StyledImage = styled('img')(({ size }) => ({
}));
type StyledImageWithFallBackProps = {
- src: string | undefined;
+ src: string;
alt: string;
size: number;
onClickHandler?: () => void;
diff --git a/src/components/contexts/CompareAasContext.tsx b/src/components/contexts/CompareAasContext.tsx
index d1c3bb5..17778e2 100644
--- a/src/components/contexts/CompareAasContext.tsx
+++ b/src/components/contexts/CompareAasContext.tsx
@@ -2,10 +2,12 @@
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react';
import { SubmodelCompareData } from 'lib/types/SubmodelCompareData';
import { generateSubmodelCompareData, isCompareData, isCompareDataRecord } from 'lib/util/CompareAasUtil';
-import { getSubmodelFromSubmodelDescriptor, performFullAasSearch } from 'lib/services/search-actions/searchActions';
+import {
+ getSubmodelFromSubmodelDescriptor,
+ performFullAasSearch,
+ performSubmodelFullSearch,
+} from 'lib/services/search-actions/searchActions';
import { SubmodelDescriptor } from 'lib/types/registryServiceTypes';
-import { getSubmodelDescriptorsById } from 'lib/services/submodelRegistryApiActions';
-import { getSubmodelById } from 'lib/services/repository-access/repositorySearchActions';
import { AasData } from 'lib/services/search-actions/AasSearcher';
type CompareAasContextType = {
@@ -19,7 +21,7 @@ type CompareAasContextType = {
type CompareAAS = {
aas: AssetAdministrationShell;
aasOrigin: string | null;
-}
+};
const aasCompareStorage = 'aas';
const compareDataStorage = 'compareData';
@@ -55,10 +57,10 @@ export const CompareAasContextProvider = (props: PropsWithChildren) => {
*/
const addAas = async (inputAas: AssetAdministrationShell, data: AasData) => {
if (compareAas.length < 3) {
- const newAas ={
+ const newAas = {
aas: inputAas,
- aasOrigin: data.aasRepositoryOrigin
- }
+ aasOrigin: data.aasRepositoryOrigin,
+ };
setCompareAas((prevList) => [...prevList, newAas]);
if (inputAas.submodels) {
const compareDataTemp = await loadSubmodelDataIntoState(
@@ -84,9 +86,9 @@ export const CompareAasContextProvider = (props: PropsWithChildren) => {
const { isSuccess, result } = await performFullAasSearch(aasId);
if (isSuccess && result.aas) {
const aas = {
- aas: result.aas,
- aasOrigin: result.aasData?.aasRepositoryOrigin ?? null
- }
+ aas: result.aas,
+ aasOrigin: result.aasData?.aasRepositoryOrigin ?? null,
+ };
aasList.push(aas);
if (result.aas.submodels) {
compareDataTemp = await loadSubmodelDataIntoState(
@@ -145,6 +147,7 @@ export const CompareAasContextProvider = (props: PropsWithChildren) => {
submodelDescriptors?: SubmodelDescriptor[],
) => {
const newCompareData: SubmodelCompareData[] = [];
+
if (submodelDescriptors && submodelDescriptors.length > 0) {
for (const submodelDescriptor of submodelDescriptors) {
const submodelResponse = await getSubmodelFromSubmodelDescriptor(
@@ -156,32 +159,15 @@ export const CompareAasContextProvider = (props: PropsWithChildren) => {
}
}
} else {
- for (const reference of input as Reference[]) {
- let submodelAdded = false;
- const descriptorResponse = await getSubmodelDescriptorsById(reference.keys[0].value);
- if (descriptorResponse.isSuccess) {
- const endpoint = descriptorResponse.result.endpoints[0].protocolInformation.href;
- const submodelResponse = await getSubmodelFromSubmodelDescriptor(endpoint);
- if (submodelResponse.isSuccess) {
- const dataRecord = generateSubmodelCompareData(submodelResponse.result);
+ await Promise.all(
+ input.map(async (reference) => {
+ const { result: submodel, isSuccess: success } = await performSubmodelFullSearch(reference);
+ if (success) {
+ const dataRecord = generateSubmodelCompareData(submodel);
newCompareData.push(dataRecord);
- submodelAdded = true;
}
- } else {
- console.warn(
- `Could not be found in Submodel Registry, will continue to look in the repository. ${descriptorResponse.message}`,
- );
- }
-
- // Submodel registry is not available or submodel not found there -> search in repo
- if (!submodelAdded) {
- const submodelData = await getSubmodelById(reference.keys[0].value);
- if (submodelData.isSuccess) {
- const dataRecord = generateSubmodelCompareData(submodelData.result);
- newCompareData.push(dataRecord);
- }
- }
- }
+ }),
+ );
}
return addCompareData(previousCompareData, newCompareData, aasCount);
};
diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx
index 1d61fe4..2119228 100644
--- a/src/layout/Header.tsx
+++ b/src/layout/Header.tsx
@@ -1,7 +1,5 @@
import { AppBar, Box, styled, Toolbar } from '@mui/material';
import MainMenu from './menu/MainMenu';
-import { useAuth } from 'lib/hooks/UseAuth';
-import { useIsTablet } from 'lib/hooks/UseBreakpoints';
import { HeaderLogo } from './HeaderLogo';
const Offset = styled(Box)(({ theme }) => theme.mixins.toolbar);
@@ -18,22 +16,12 @@ const StyledLogoWrapper = styled(Box)(() => ({
},
}));
-const StyledToolbar = styled(Toolbar)(() => ({
- '&.hidden': {
- minHeight: 0,
- height: 0,
- overflow: 'hidden',
- },
-}));
-
export function Header() {
- const auth = useAuth();
- const isTablet = useIsTablet();
return (
<>
-
-
+ theme.zIndex.drawer + 1 }}>
+
-
+
>
diff --git a/src/layout/LayoutRoot.tsx b/src/layout/LayoutRoot.tsx
index 7796c24..1f8120d 100644
--- a/src/layout/LayoutRoot.tsx
+++ b/src/layout/LayoutRoot.tsx
@@ -1,23 +1,16 @@
'use client';
import { Box, styled } from '@mui/material';
import { NotificationOutlet } from 'components/basics/NotificationOutlet';
-import { useAuth } from 'lib/hooks/UseAuth';
-import { useIsTablet } from 'lib/hooks/UseBreakpoints';
import { Header } from './Header';
import { ReactNode } from 'react';
const StyledBox = styled(Box)(() => ({
- '&.sidebar-visible': {
- marginLeft: 275,
- },
- '&.topbar-visible': {
// toolbar min-height is 56px
marginTop: 56,
'@media(min-width:600px)': {
// toolbar min-height is 64px
marginTop: 64,
},
- },
}));
type Props = {
@@ -25,17 +18,10 @@ type Props = {
};
export function LayoutRoot({ children }: Props) {
- const auth = useAuth();
- const isTablet = useIsTablet();
-
return (
-
+
{children}
diff --git a/src/layout/menu/ExternalLink.spec.tsx b/src/layout/menu/ExternalLink.spec.tsx
new file mode 100644
index 0000000..9314dec
--- /dev/null
+++ b/src/layout/menu/ExternalLink.spec.tsx
@@ -0,0 +1,27 @@
+import { screen } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import { CustomRenderReactIntl } from 'test-utils/CustomRenderReactIntl';
+import { ExternalLink } from 'layout/menu/ExternalLink';
+import { messages } from 'lib/i18n/localization';
+
+describe('ExternalLink', () => {
+ it('should render the component', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('external-link');
+ expect(component).toBeInTheDocument();
+ });
+
+ it('should open link in new tab natively', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('external-link');
+ expect(component).toHaveAttribute('href', 'https://xitaso.com/');
+ expect(component).toHaveAttribute('target', '_blank');
+ expect(component).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+
+ it('should show the translated text', async () => {
+ CustomRenderReactIntl();
+ const component = screen.getByTestId('external-link');
+ expect(component).toHaveTextContent('Imprint');
+ });
+});
diff --git a/src/layout/menu/ExternalLink.tsx b/src/layout/menu/ExternalLink.tsx
new file mode 100644
index 0000000..b1d877e
--- /dev/null
+++ b/src/layout/menu/ExternalLink.tsx
@@ -0,0 +1,22 @@
+import { Link } from '@mui/material';
+import { FormattedMessage } from 'react-intl';
+import { MessageDescriptorWithId } from 'lib/i18n/localization';
+
+export type ExternalLinkProps = {
+ href: string | undefined;
+ descriptor: MessageDescriptorWithId;
+};
+
+export function ExternalLink({ href, descriptor }: ExternalLinkProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/layout/menu/MainMenu.tsx b/src/layout/menu/MainMenu.tsx
index 3518e54..47db4bd 100644
--- a/src/layout/menu/MainMenu.tsx
+++ b/src/layout/menu/MainMenu.tsx
@@ -1,24 +1,24 @@
import MenuIcon from '@mui/icons-material/Menu';
import { alpha, Box, Divider, Drawer, IconButton, List, styled, Typography } from '@mui/material';
-import { Dashboard, Home, Login, Logout, OpenInNew, Settings } from '@mui/icons-material';
+import { Dashboard, Login, Logout, OpenInNew, Settings } from '@mui/icons-material';
import React, { useState } from 'react';
import { useAuth } from 'lib/hooks/UseAuth';
import { FormattedMessage } from 'react-intl';
import { messages } from 'lib/i18n/localization';
import { TemplateIcon } from 'components/custom-icons/TemplateIcon';
import { MenuHeading } from './MenuHeading';
-import { useIsTablet } from 'lib/hooks/UseBreakpoints';
import { MenuListItem, MenuListItemProps } from './MenuListItem';
import ListIcon from '@mui/icons-material/List';
-import { HeaderLogo } from 'layout/HeaderLogo';
import packageJson from '../../../package.json';
import { useEnv } from 'app/env/provider';
+import { ExternalLink } from 'layout/menu/ExternalLink';
const StyledDrawer = styled(Drawer)(({ theme }) => ({
'.MuiDrawer-paper': {
color: theme.palette.primary.contrastText,
background: theme.palette.primary.main,
width: '275px',
+ paddingTop: theme.mixins.toolbar.minHeight,
'.MuiListItem-root, .MuiListItemButton-root': {
'.MuiTypography-root': {
textOverflow: 'ellipsis',
@@ -44,6 +44,9 @@ const StyledDrawer = styled(Drawer)(({ theme }) => ({
marginTop: 'auto',
},
},
+ '.MuiBackdrop-root': {
+ background: 'none',
+ },
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
@@ -51,26 +54,23 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
opacity: 0.3,
}));
-const StyledLogoWrapper = styled(Box)(({ theme }) => ({
- width: '100%',
- display: 'flex',
- justifyContent: 'center',
- alignContent: 'center',
- padding: theme.spacing(3),
- paddingTop: theme.spacing(5),
- '.logo': {
- maxWidth: 240,
- height: 32,
- },
-}));
-
export default function MainMenu() {
const [drawerOpen, setDrawerOpen] = useState(false);
const auth = useAuth();
- const isTablet = useIsTablet();
const env = useEnv();
const useAuthentication = env.AUTHENTICATION_FEATURE_FLAG;
+ const copyrightString = `Copyright © ${new Date().getFullYear()} XITASO GmbH`;
const versionString = 'Version ' + packageJson.version;
+ const imprintString = env.IMPRINT_URL;
+ const dataPrivacyString = env.DATA_PRIVACY_URL;
+
+ const getAuthName = () => {
+ const user = auth?.getAccount()?.user;
+ if (!user) return;
+ if (user.email) return user.email;
+ if (user.name) return user.name;
+ return;
+ };
const handleMenuInteraction = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
@@ -110,14 +110,9 @@ export default function MainMenu() {
const guestMainMenu: MenuListItemProps[] = [
{
- label: ,
- icon: ,
- onClick: () => auth.login(),
- },
- {
- label: ,
+ label: ,
to: '/',
- icon: ,
+ icon: ,
},
];
@@ -131,6 +126,14 @@ export default function MainMenu() {
},
];
+ const guestBottomMenu: MenuListItemProps[] = [
+ {
+ label: ,
+ icon: ,
+ onClick: () => auth.login(),
+ },
+ ];
+
if (env.AAS_LIST_FEATURE_FLAG) {
const listItemToAdd = {
label: ,
@@ -148,31 +151,19 @@ export default function MainMenu() {
-
- {(!useAuthentication || auth.isLoggedIn) && (
-
-
-
-
-
- )}
+
+
+
+
{!useAuthentication || auth.isLoggedIn ? (
<>
-
-
-
{adminMainMenu.map((props, i) => (
))}
@@ -193,33 +184,41 @@ export default function MainMenu() {
)}
-
- {`Copyright © ${new Date().getFullYear()} XITASO GmbH`}
-
- {(!useAuthentication || !auth.isLoggedIn) && (
-
- {versionString}
-
- )}
- {useAuthentication && auth.isLoggedIn && (
-
+
+ {imprintString && (
+
+
+
+ )}
+ {dataPrivacyString && (
+
+
+
+ )}
+ {copyrightString}
+ {versionString}
+
+ {useAuthentication && (
+ <>
+
-
- {versionString}
-
-
- {auth.getAccount()?.user?.email}
- {adminBottomMenu.map((props, i) => (
-
- ))}
+ {auth.isLoggedIn && (
+ <>
+ {getAuthName() && {getAuthName()}}
+ {adminBottomMenu.map((props, i) => (
+
+ ))}
+ >
+ )}
+ {!auth.isLoggedIn && (
+ <>
+ {guestBottomMenu.map((props, i) => (
+
+ ))}
+ >
+ )}
-
+ >
)}
>
diff --git a/src/layout/menu/MenuHeading.tsx b/src/layout/menu/MenuHeading.tsx
index 4bbc423..8930510 100644
--- a/src/layout/menu/MenuHeading.tsx
+++ b/src/layout/menu/MenuHeading.tsx
@@ -1,10 +1,14 @@
import { ListItem, ListItemProps, Typography } from '@mui/material';
-export function MenuHeading(props: ListItemProps) {
+export interface MenuHeadingProps extends ListItemProps {
+ marginTop?: string | number;
+}
+
+export function MenuHeading({ children, marginTop = 2 }: MenuHeadingProps) {
return (
-
+
- {props.children}
+ {children}
);
diff --git a/src/lib/api/basyx-v3/api.ts b/src/lib/api/basyx-v3/api.ts
index 5123c68..b6c1061 100644
--- a/src/lib/api/basyx-v3/api.ts
+++ b/src/lib/api/basyx-v3/api.ts
@@ -1,5 +1,4 @@
/* eslint-disable */
-import isomorphicFetch from 'isomorphic-fetch';
import url from 'url';
import { Configuration } from './configuration';
import { AssetAdministrationShell, Reference, Submodel } from '@aas-core-works/aas-core3.0-typescript/types';
@@ -7,61 +6,22 @@ import { encodeBase64 } from 'lib/util/Base64Util';
import { IAssetAdministrationShellRepositoryApi, ISubmodelRepositoryApi } from 'lib/api/basyx-v3/apiInterface';
import {
AssetAdministrationShellRepositoryApiInMemory,
- INullableAasRepositoryEntries,
- SubmodelRepositoryApiInMemory
+ SubmodelRepositoryApiInMemory,
} from 'lib/api/basyx-v3/apiInMemory';
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
import { AttachmentDetails } from 'lib/types/TransferServiceData';
import { mnestixFetch } from 'lib/api/infrastructure';
+import ServiceReachable from 'test-utils/TestUtils';
-const BASE_PATH = '/'.replace(/\/+$/, '');
-
-/**
- *
- * @export
- * @interface FetchAPI
- */
export type FetchAPI = {
fetch: (url: RequestInfo, init?: RequestInit) => Promise>;
};
-/**
- *
- * @export
- * @interface FetchArgs
- */
export interface FetchArgs {
url: string;
options: any;
}
-/**
- *
- * @export
- * @class BaseAPI
- */
-export class BaseAPI {
- protected configuration: Configuration;
-
- constructor(
- configuration?: Configuration,
- protected basePath: string = BASE_PATH,
- protected fetch: FetchAPI = isomorphicFetch,
- ) {
- if (configuration) {
- this.configuration = configuration;
- this.basePath = configuration.basePath || this.basePath;
- this.fetch = configuration.fetch || this.fetch;
- }
- }
-}
-
-/**
- *
- * @export
- * @class RequiredError
- * @extends {Error}
- */
export class RequiredError extends Error {
name: 'RequiredError';
@@ -73,57 +33,59 @@ export class RequiredError extends Error {
}
}
+export type AasRepositoryResponse = {
+ paging_metadata: { cursor: string };
+ result: AssetAdministrationShell[];
+};
+
/**
* AssetAdministrationShellRepositoryApi - object-oriented interface
* @class AssetAdministrationShellRepositoryApi
- * @extends {BaseAPI}
*/
export class AssetAdministrationShellRepositoryApi implements IAssetAdministrationShellRepositoryApi {
private constructor(
+ private basePath: string,
private http: {
fetch(url: RequestInfo | URL, init?: RequestInit): Promise>;
},
private configuration?: Configuration | undefined,
- private basePath?: string,
) {}
static create(
+ baseUrl: string,
http: FetchAPI,
- configuration?: Configuration | undefined,
- basePath?: string,
+ configuration?: Configuration,
): AssetAdministrationShellRepositoryApi {
- return new AssetAdministrationShellRepositoryApi(http, configuration, basePath);
+ return new AssetAdministrationShellRepositoryApi(baseUrl, http, configuration);
}
- static createNull(options: {
- shellsSavedInTheRepositories: INullableAasRepositoryEntries[] | null;
- }): AssetAdministrationShellRepositoryApiInMemory {
- return new AssetAdministrationShellRepositoryApiInMemory(options);
+ static createNull(
+ baseUrl: string,
+ shellsInRepositories: AssetAdministrationShell[],
+ reachable: ServiceReachable = ServiceReachable.Yes,
+ ): AssetAdministrationShellRepositoryApiInMemory {
+ return new AssetAdministrationShellRepositoryApiInMemory(baseUrl, shellsInRepositories, reachable);
+ }
+
+ getBaseUrl(): string {
+ return this.basePath;
+ }
+
+ async getAllAssetAdministrationShells(limit?: number, cursor?: string, options?: any) {
+ return AssetAdministrationShellRepositoryApiFp(this.configuration).getAllAssetAdministrationShells(
+ limit,
+ cursor,
+ options,
+ )(this.http, this.basePath);
}
- /**
- * @summary Retrieves a specific Asset Administration Shell from the Asset Administration Shell repository
- * @param {string} aasId The Asset Administration Shell's unique id
- * @param {*} [options] Override http request option.
- * @param {string} [basePath] The URL for the current repository endpoint.
- * @throws {RequiredError}
- * @memberof AssetAdministrationShellRepositoryApi
- */
- async getAssetAdministrationShellById(aasId: string, options?: any, basePath?: string) {
+ async getAssetAdministrationShellById(aasId: string, options?: any) {
return AssetAdministrationShellRepositoryApiFp(this.configuration).getAssetAdministrationShellById(
aasId,
options,
- )(this.http, basePath ?? this.basePath);
+ )(this.http, this.basePath);
}
- /**
- *
- * @summary Retrieves all Submodels from the Asset Administration Shell
- * @param {string} aasId The Asset Administration Shell's unique id
- * @param {*} [options] Override http request option.
- * @throws {RequiredError}
- * @memberof AssetAdministrationShellRepositoryApi
- */
async getSubmodelReferencesFromShell(aasId: string, options?: any) {
return AssetAdministrationShellRepositoryApiFp(this.configuration).getSubmodelReferencesFromShell(
aasId,
@@ -131,53 +93,28 @@ export class AssetAdministrationShellRepositoryApi implements IAssetAdministrati
)(this.http, this.basePath);
}
- /**
- * @summary Retrieves the thumbnail from the Asset Administration Shell.
- * @param aasId aasId The ID of the Asset Administration Shell.
- * @param options {*} [options] Override http request option.
- * @param {string} [basePath] The URL for the current repository endpoint.
- * @returns The thumbnail retrieved from the Asset Administration Shell.
- */
- async getThumbnailFromShell(aasId: string, options?: any, basePath?: string) {
+ async getThumbnailFromShell(aasId: string, options?: any) {
return AssetAdministrationShellRepositoryApiFp(this.configuration).getThumbnailFromAssetInformation(
aasId,
options,
- )(this.http, basePath ?? this.basePath);
+ )(this.http, this.basePath);
}
- /**
- * @summary Uploads a thumbnail to the specified Asset Administration Shell (AAS).
- * @param {string} aasId - The unique identifier of the Asset Administration Shell.
- * @param {Blob} image - The image file to be uploaded as the thumbnail.
- * @param fileName - Name of the image file to be uploaded.
- * @param {object} [options] - Optional. Override HTTP request options.
- * @param {string} [basePath] - Optional. The base URL of the repository endpoint.
- * @returns {Promise} A promise that resolves to the server's response after the thumbnail upload.
- * @memberof AssetAdministrationShellRepositoryApi
- */
- putThumbnailToShell(
+ async putThumbnailToShell(
aasId: string,
image: Blob,
fileName: string,
options?: any,
- basePath?: string,
): Promise> {
return AssetAdministrationShellRepositoryApiFp(this.configuration).putThumbnailToShell(
aasId,
image,
fileName,
options,
- )(mnestixFetch(), basePath ?? this.basePath);
+ )(mnestixFetch(), this.basePath);
}
- /**
- * @summary Creates a new Asset Administration Shell (AAS) in the repository.
- * @param {AssetAdministrationShell} aas - The Asset Administration Shell object to be created.
- * @param {object} [options] - Optional. Additional options to override the default HTTP request settings.
- * @returns {Promise} A promise that resolves to the newly created Asset Administration Shell.
- * @memberof AssetAdministrationShellRepositoryApi
- */
- postAssetAdministrationShell(
+ async postAssetAdministrationShell(
aas: AssetAdministrationShell,
options?: object | undefined,
): Promise> {
@@ -190,25 +127,38 @@ export class AssetAdministrationShellRepositoryApi implements IAssetAdministrati
/**
* AssetAdministrationShellRepositoryApi - functional programming interface
- * @export
*/
export const AssetAdministrationShellRepositoryApiFp = function (configuration?: Configuration) {
return {
+ getAllAssetAdministrationShells(limit: number | undefined, cursor: string | undefined, options: any) {
+ return async (requestHandler: FetchAPI, basePath: string) => {
+ const localVarRequestOptions = Object.assign({ method: 'GET' }, options);
+ const localVarHeaderParameter = {
+ Accept: 'application/json',
+ } as any;
+
+ const cursorQueryParameter = cursor ?? '';
+
+ localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options?.headers);
+
+ return await requestHandler.fetch(
+ basePath + `/shells?limit=${limit}&cursor=${cursorQueryParameter}`,
+ localVarRequestOptions,
+ );
+ };
+ },
+
/**
* @summary Retrieves a specific Asset Administration Shell from the Asset Administration Shell repository
* @param {string} aasId The Asset Administration Shell's unique id
* @param {*} [options] Override http request option
* @throws {RequiredError}
*/
- getAssetAdministrationShellById(
- aasId: string,
- options?: any,
- ): (fetch?: FetchAPI, basePath?: string) => Promise> {
- // HINT: AssetAdministrationShell is taken from aas_core_meta
+ getAssetAdministrationShellById(aasId: string, options?: any) {
const localVarFetchArgs = AssetAdministrationShellRepositoryApiFetchParamCreator(
configuration,
).getAssetAdministrationShellById(aasId, options);
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ return async (requestHandler: FetchAPI, basePath: string) => {
return requestHandler.fetch(
basePath + localVarFetchArgs.url,
localVarFetchArgs.options,
@@ -222,14 +172,11 @@ export const AssetAdministrationShellRepositoryApiFp = function (configuration?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getSubmodelReferencesFromShell(
- aasId: string,
- options?: any,
- ): (fetch?: FetchAPI, basePath?: string) => Promise> {
+ getSubmodelReferencesFromShell(aasId: string, options?: any) {
const localVarFetchArgs = AssetAdministrationShellRepositoryApiFetchParamCreator(
configuration,
).getSubmodelReferencesFromShell(aasId, options);
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ return async (requestHandler: FetchAPI, basePath: string) => {
return requestHandler.fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options);
};
},
@@ -240,14 +187,11 @@ export const AssetAdministrationShellRepositoryApiFp = function (configuration?:
* @param options {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getThumbnailFromAssetInformation(
- aasId: string,
- options?: any,
- ): (fetch?: FetchAPI, basePath?: string) => Promise> {
+ getThumbnailFromAssetInformation(aasId: string, options?: any) {
const localVarFetchArgs = AssetAdministrationShellRepositoryApiFetchParamCreator(
configuration,
).getThumbnailFromAssetInformation(aasId, options);
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ return async (requestHandler: FetchAPI, basePath: string) => {
return requestHandler.fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options);
};
},
@@ -260,13 +204,8 @@ export const AssetAdministrationShellRepositoryApiFp = function (configuration?:
* @param {object} [options] - Optional. Override HTTP request options.
* @throws {RequiredError}
*/
- putThumbnailToShell(
- aasId: string,
- image: Blob,
- fileName: string,
- options?: any,
- ): (fetch: FetchAPI, basePath?: string) => Promise> {
- return async (requestHandler: FetchAPI, basePath: string = BASE_PATH) => {
+ putThumbnailToShell(aasId: string, image: Blob, fileName: string, options?: any) {
+ return async (requestHandler: FetchAPI, basePath: string) => {
const localVarRequestOptions = Object.assign({ method: 'PUT' }, options);
const localVarHeaderParameter = {
Accept: 'application/json',
@@ -293,11 +232,8 @@ export const AssetAdministrationShellRepositoryApiFp = function (configuration?:
* @param {object} [options] - Optional. Additional options to override the default HTTP request settings.
* @throws {RequiredError}
*/
- createAssetAdministrationShell(
- aas: AssetAdministrationShell,
- options?: any,
- ): (fetch: FetchAPI, basePath?: string) => Promise> {
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ createAssetAdministrationShell(aas: AssetAdministrationShell, options?: any) {
+ return async (requestHandler: FetchAPI, basePath: string) => {
const localVarRequestOptions = Object.assign({ method: 'POST' }, options);
const localVarHeaderParameter = {
Accept: 'application/json',
@@ -422,45 +358,37 @@ export const AssetAdministrationShellRepositoryApiFetchParamCreator = function (
/**
* SubmodelRepositoryApi - object-oriented interface
* @class SubmodelRepositoryApi
- * @extends {BaseAPI}
*/
export class SubmodelRepositoryApi implements ISubmodelRepositoryApi {
private constructor(
- private fetch?: FetchAPI,
- private configuration?: Configuration | undefined,
- private basePath?: string,
+ private baseUrl: string,
+ private http: FetchAPI,
+ private configuration?: Configuration,
) {}
- static create(http: FetchAPI, configuration?: Configuration | undefined, basePath?: string): SubmodelRepositoryApi {
- return new SubmodelRepositoryApi(http, configuration, basePath);
+ static create(baseUrl: string, http: FetchAPI, configuration?: Configuration): SubmodelRepositoryApi {
+ return new SubmodelRepositoryApi(baseUrl, http, configuration);
}
- static createNull(options: { submodelsSavedInTheRepository: Submodel[] | null }): SubmodelRepositoryApiInMemory {
- return new SubmodelRepositoryApiInMemory(options);
+ static createNull(
+ basePath: string,
+ submodelsInRepository: Submodel[],
+ reachable: ServiceReachable = ServiceReachable.Yes,
+ ): SubmodelRepositoryApiInMemory {
+ return new SubmodelRepositoryApiInMemory(basePath, submodelsInRepository, reachable);
}
- /**
- * @summary Retrieves the submodel
- * @param {string} submodelId The Submodels unique id
- * @param {*} [options] Override http request option
- * @param {string} [basePath] The URL for the current repository endpoint.
- * @throws {RequiredError}
- * @memberof SubmodelRepositoryApi
- */
- async getSubmodelById(submodelId: string, options?: any, basePath?: string): Promise> {
+ getBaseUrl(): string {
+ return this.baseUrl;
+ }
+
+ async getSubmodelById(submodelId: string, options?: any): Promise> {
return SubmodelRepositoryApiFp(this.configuration).getSubmodelById(submodelId, options)(
- this.fetch,
- basePath ?? this.basePath,
+ this.http,
+ this.baseUrl,
);
}
- /**
- * @summary Retrieves the attachment from a submodel element
- * @param submodelId The id of the submodel element is part of
- * @param submodelElementPath The path to the submodel element
- * @param {*} [options] Override http request option
- * @memberof SubmodelRepositoryApi
- */
async getAttachmentFromSubmodelElement(
submodelId: string,
submodelElementPath: string,
@@ -470,28 +398,13 @@ export class SubmodelRepositoryApi implements ISubmodelRepositoryApi {
submodelId,
submodelElementPath,
options,
- )(this.fetch, this.basePath);
+ )(this.http, this.baseUrl);
}
- /**
- * @summary Creates a new submodel in the Submodel repository.
- * @param {Submodel} submodel - The submodel object to be created.
- * @param {object} [options] - Optional. Additional options to override default HTTP request settings.
- * @returns {Promise} A promise that resolves to the newly created submodel.
- * @memberof SubmodelRepositoryApi
- */
- postSubmodel(submodel: Submodel, options?: object | undefined): Promise> {
- return SubmodelRepositoryApiFp(this.configuration).createSubmodel(submodel, options)(this.fetch, this.basePath);
+ postSubmodel(submodel: Submodel, options?: object): Promise> {
+ return SubmodelRepositoryApiFp(this.configuration).createSubmodel(submodel, options)(this.http, this.baseUrl);
}
- /**
- * @summary Uploads an attachment to a specific submodel element.
- * @param {string} submodelId - The unique identifier of the submodel containing the submodel element.
- * @param {AttachmentDetails} attachmentDetails - The attachment data to be uploaded to the submodel element.
- * @param {object} [options] - Optional. Additional options to override default HTTP request settings.
- * @returns {Promise} A promise that resolves to the server's response after the attachment upload.
- * @memberof SubmodelRepositoryApi
- */
putAttachmentToSubmodelElement(
submodelId: string,
attachmentDetails: AttachmentDetails,
@@ -501,7 +414,7 @@ export class SubmodelRepositoryApi implements ISubmodelRepositoryApi {
submodelId,
attachmentDetails,
options,
- )(this.fetch, this.basePath);
+ )(this.http, this.baseUrl);
}
}
@@ -516,16 +429,13 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
* @param {*} [options] Override http request option
* @throws {RequiredError}
*/
- getSubmodelById(
- submodelId: string,
- options?: any,
- ): (fetch?: FetchAPI, basePath?: string) => Promise> {
+ getSubmodelById(submodelId: string, options?: any) {
const localVarFetchArgs = SubmodelRepositoryApiFetchParamCreator(configuration).getSubmodelById(
encodeBase64(submodelId),
options,
);
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
- return requestHandler.fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options);
+ return async (requestHandler: FetchAPI, baseUrl: string) => {
+ return requestHandler.fetch(baseUrl + localVarFetchArgs.url, localVarFetchArgs.options);
};
},
/**
@@ -534,16 +444,12 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
* @param submodelElementPath The path to the submodel element
* @param {*} [options] Override http request option
*/
- getAttachmentFromSubmodelElement(
- submodelId: string,
- submodelElementPath: string,
- options?: any,
- ): (requestHandler?: FetchAPI, basePath?: string) => Promise> {
+ getAttachmentFromSubmodelElement(submodelId: string, submodelElementPath: string, options?: any) {
const localVarFetchArgs = SubmodelRepositoryApiFetchParamCreator(
configuration,
).getAttachmentFromSubmodelElement(submodelId, submodelElementPath, options);
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
- return requestHandler.fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options);
+ return async (requestHandler: FetchAPI, baseUrl: string) => {
+ return requestHandler.fetch(baseUrl + localVarFetchArgs.url, localVarFetchArgs.options);
};
},
@@ -553,11 +459,8 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
* @param {object} [options] - Optional. Additional options to override default HTTP request settings.
* @throws {RequiredError}
*/
- createSubmodel(
- submodel: Submodel,
- options?: any,
- ): (requestHandler?: FetchAPI, basePath?: string) => Promise> {
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ createSubmodel(submodel: Submodel, options?: any) {
+ return async (requestHandler: FetchAPI, baseUrl: string) => {
const localVarRequestOptions = Object.assign({ method: 'POST' }, options);
const localVarHeaderParameter = {
Accept: 'application/json',
@@ -567,7 +470,7 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options?.headers);
localVarRequestOptions.body = JSON.stringify(submodel);
- return await requestHandler.fetch(basePath + '/submodels', localVarRequestOptions);
+ return await requestHandler.fetch(baseUrl + '/submodels', localVarRequestOptions);
};
},
@@ -579,7 +482,7 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
* @throws {RequiredError}
*/
putAttachmentToSubmodelElement(submodelId: string, attachmentDetails: AttachmentDetails, options: any) {
- return async (requestHandler: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => {
+ return async (requestHandler: FetchAPI, baseUrl: string) => {
const localVarRequestOptions = Object.assign({ method: 'PUT' }, options);
const localVarHeaderParameter = {
Accept: 'application/json',
@@ -591,7 +494,7 @@ export const SubmodelRepositoryApiFp = function (configuration?: Configuration)
localVarRequestOptions.body = formData;
return await requestHandler.fetch(
- basePath +
+ baseUrl +
`/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment?fileName={fileName}`
.replace(`{submodelIdentifier}`, encodeBase64(String(submodelId)))
.replace(`{idShortPath}`, attachmentDetails.idShortPath)
diff --git a/src/lib/api/basyx-v3/apiInMemory.ts b/src/lib/api/basyx-v3/apiInMemory.ts
index a43d726..448c115 100644
--- a/src/lib/api/basyx-v3/apiInMemory.ts
+++ b/src/lib/api/basyx-v3/apiInMemory.ts
@@ -1,75 +1,99 @@
import { IAssetAdministrationShellRepositoryApi, ISubmodelRepositoryApi } from 'lib/api/basyx-v3/apiInterface';
import { AssetAdministrationShell, Reference, Submodel } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
-import { decodeBase64, encodeBase64 } from 'lib/util/Base64Util';
import {
ApiResponseWrapper,
ApiResultStatus,
wrapErrorCode,
wrapResponse,
+ wrapSuccess,
} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
import { AttachmentDetails } from 'lib/types/TransferServiceData';
+import { AasRepositoryResponse } from 'lib/api/basyx-v3/api';
+import { encodeBase64, safeBase64Decode } from 'lib/util/Base64Util';
+import ServiceReachable from 'test-utils/TestUtils';
-export interface INullableAasRepositoryEntries {
- repositoryUrl: string;
- aas: AssetAdministrationShell;
-}
+const options = {
+ headers: { 'Content-type': 'application/json; charset=utf-8' },
+};
export class AssetAdministrationShellRepositoryApiInMemory implements IAssetAdministrationShellRepositoryApi {
- private shellsSavedInTheRepositories: INullableAasRepositoryEntries[] | null | undefined;
+ readonly shellsInRepository: Map;
+
+ constructor(
+ readonly baseUrl: string,
+ shellsInRepository: AssetAdministrationShell[] = [],
+ readonly reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {
+ this.shellsInRepository = new Map();
+ shellsInRepository.forEach((value) => this.shellsInRepository.set(encodeBase64(value.id), value));
+ }
+
+ getBaseUrl(): string {
+ return this.baseUrl;
+ }
+
+ async getAllAssetAdministrationShells(
+ _limit?: number | undefined,
+ _cursor?: string | undefined,
+ _options?: object | undefined,
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
- constructor(options: { shellsSavedInTheRepositories: INullableAasRepositoryEntries[] | null }) {
- this.shellsSavedInTheRepositories = options.shellsSavedInTheRepositories;
+ const shells = this.shellsInRepository;
+ let cursor = '';
+ if (shells.size > 0) {
+ cursor = [...shells].pop()?.[0] ?? '';
+ }
+ const response = new Response(
+ JSON.stringify({
+ paging_metadata: { cursor: cursor },
+ result: Array.from(shells.values()),
+ }),
+ options,
+ );
+ return await wrapResponse(response);
}
- postAssetAdministrationShell(
- _aas: AssetAdministrationShell,
+ async postAssetAdministrationShell(
+ aas: AssetAdministrationShell,
_options?: object | undefined,
): Promise> {
- throw new Error('Method not implemented.');
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ if (this.shellsInRepository.get(aas.id))
+ return wrapErrorCode(
+ ApiResultStatus.INTERNAL_SERVER_ERROR,
+ `AAS repository already has an AAS with id '${aas.id}`,
+ );
+ this.shellsInRepository.set(aas.id, aas);
+ return wrapSuccess(aas);
}
putThumbnailToShell(
_aasId: string,
_image: Blob,
_fileName: string,
- _options?: object | undefined,
- _basePath?: string | undefined,
+ _options?: object,
): Promise> {
throw new Error('Method not implemented.');
}
- static getDefaultRepositoryUrl(): string {
- return 'www.aas.default.com/repository';
- }
-
async getAssetAdministrationShellById(
aasId: string,
- _options?: object | undefined,
- _basePath?: string | undefined,
+ _options?: object,
): Promise> {
- if (!this.shellsSavedInTheRepositories) return Promise.reject('no repository configuration');
- const defaultRepositoryUrl = AssetAdministrationShellRepositoryApiInMemory.getDefaultRepositoryUrl();
- const isSearchingInDefaultRepository = _basePath === defaultRepositoryUrl || _basePath === undefined;
- for (const entry of this.shellsSavedInTheRepositories) {
- if (encodeBase64(entry.aas.id) === aasId) {
- const isInDefaultRepository = entry.repositoryUrl === defaultRepositoryUrl;
- if (isInDefaultRepository || !isSearchingInDefaultRepository) {
- const response = new Response(JSON.stringify(entry.aas));
- return await wrapResponse(response);
- }
- }
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundAas = this.shellsInRepository.get(aasId);
+ if (foundAas) {
+ const response = new Response(JSON.stringify(foundAas), options);
+ return await wrapResponse(response);
}
- const targetRepositoryKind = isSearchingInDefaultRepository ? 'default' : 'foreign';
return Promise.resolve(
wrapErrorCode(
ApiResultStatus.NOT_FOUND,
- 'no aas found in the ' +
- targetRepositoryKind +
- ' repository for aasId: ' +
- aasId +
- ', which is :' +
- decodeBase64(aasId) +
- ' encoded in base64',
+ `no aas found in the repository '${this.getBaseUrl()}' for aasId: '${aasId}', which is :'${safeBase64Decode(aasId)}' encoded in base64`,
),
);
}
@@ -81,58 +105,67 @@ export class AssetAdministrationShellRepositoryApiInMemory implements IAssetAdmi
throw new Error('Method not implemented.');
}
- async getThumbnailFromShell(
- _aasId: string,
- _options?: object | undefined,
- _basePath?: string | undefined,
- ): Promise> {
+ async getThumbnailFromShell(_aasId: string, _options?: object): Promise> {
throw new Error('Method not implemented.');
}
}
export class SubmodelRepositoryApiInMemory implements ISubmodelRepositoryApi {
- private submodelsSavedInTheRepository: Submodel[] | null | undefined;
+ readonly submodelsInRepository: Map;
- constructor(options: { submodelsSavedInTheRepository: Submodel[] | null }) {
- this.submodelsSavedInTheRepository = options.submodelsSavedInTheRepository;
+ constructor(
+ readonly baseUrl: string,
+ submodelsInRepository: Submodel[],
+ readonly reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {
+ this.submodelsInRepository = new Map();
+ submodelsInRepository.forEach((submodel) => {
+ this.submodelsInRepository.set(submodel.id, submodel);
+ });
+ }
+
+ getBaseUrl(): string {
+ return this.baseUrl;
}
putAttachmentToSubmodelElement(
_submodelId: string,
_attachmentData: AttachmentDetails,
- _options?: object | undefined,
+ _options?: object,
): Promise> {
throw new Error('Method not implemented.');
}
- postSubmodel(_submodel: Submodel, _options?: object | undefined): Promise> {
- throw new Error('Method not implemented.');
+ async postSubmodel(submodel: Submodel, _options?: object): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ if (this.submodelsInRepository.has(submodel.id))
+ return wrapErrorCode(
+ ApiResultStatus.UNKNOWN_ERROR,
+ `Submodel repository '${this.getBaseUrl()}' already has a submodel '${submodel.id}'`,
+ );
+ this.submodelsInRepository.set(submodel.id, submodel);
+ return wrapSuccess(submodel);
}
- async getSubmodelById(
- submodelId: string,
- _options?: object | undefined,
- _basePath?: string | undefined,
- ): Promise> {
- if (!this.submodelsSavedInTheRepository) return Promise.reject('no repository configuration');
- for (const submodel of this.submodelsSavedInTheRepository) {
- if (encodeBase64(submodel.id) === submodelId) {
- const response = new Response(JSON.stringify(submodel));
- return await wrapResponse(response);
- }
+ async getSubmodelById(submodelId: string, _options?: object): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundAas = this.submodelsInRepository.get(submodelId);
+ if (foundAas) {
+ const response = new Response(JSON.stringify(foundAas), options);
+ return wrapResponse(response);
}
- return Promise.resolve(
- wrapErrorCode(
- ApiResultStatus.NOT_FOUND,
- 'no submodel found in the default repository for submodelId: ' + submodelId,
- ),
+ return wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `no submodel found in the repository: '${this.baseUrl}' for submodel: '${submodelId}'`,
);
}
async getAttachmentFromSubmodelElement(
_submodelId: string,
_submodelElementPath: string,
- _options?: object | undefined,
+ _options?: object,
): Promise> {
throw new Error('Method not implemented.');
}
diff --git a/src/lib/api/basyx-v3/apiInterface.ts b/src/lib/api/basyx-v3/apiInterface.ts
index 11537fc..f1159a0 100644
--- a/src/lib/api/basyx-v3/apiInterface.ts
+++ b/src/lib/api/basyx-v3/apiInterface.ts
@@ -2,20 +2,30 @@ import { AssetAdministrationShell, Reference } from '@aas-core-works/aas-core3.0
import { Submodel } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
import { AttachmentDetails } from 'lib/types/TransferServiceData';
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { AasRepositoryResponse } from 'lib/api/basyx-v3/api';
export interface IAssetAdministrationShellRepositoryApi {
+ /**
+ * Returns the base URL of this repository endpoint.
+ */
+ getBaseUrl(): string;
+
+ getAllAssetAdministrationShells(
+ limit?: number,
+ cursor?: string,
+ options?: object,
+ ): Promise>;
+
/**
* @summary Retrieves a specific Asset Administration Shell from the Asset Administration Shell repository
* @param {string} aasId The Asset Administration Shell's unique id
* @param {*} [options] Override http request option.
- * @param {string} [basePath] The URL for the current repository endpoint.
* @throws {RequiredError}
* @memberof AssetAdministrationShellRepositoryApi
*/
getAssetAdministrationShellById(
aasId: string,
options?: object,
- basePath?: string,
): Promise>;
/**
@@ -32,11 +42,10 @@ export interface IAssetAdministrationShellRepositoryApi {
* @summary Retrieves the thumbnail from the Asset Administration Shell.
* @param aasId aasId The ID of the Asset Administration Shell.
* @param options {*} [options] Override http request option.
- * @param {string} [basePath] The URL for the current repository endpoint.
* @returns The thumbnail retrieved from the Asset Administration Shell.
* @memberof AssetAdministrationShellRepositoryApi
*/
- getThumbnailFromShell(aasId: string, options?: object, basePath?: string): Promise>;
+ getThumbnailFromShell(aasId: string, options?: object): Promise>;
/**
* @summary Uploads a thumbnail to the specified Asset Administration Shell (AAS).
@@ -44,7 +53,6 @@ export interface IAssetAdministrationShellRepositoryApi {
* @param {Blob} image - The image file to be uploaded as the thumbnail.
* @param fileName - Name of the image file to be uploaded.
* @param {object} [options] - Optional. Override HTTP request options.
- * @param {string} [basePath] - Optional. The base URL of the repository endpoint.
* @returns {PromiseResponse>} A promise that resolves to the server's response after the thumbnail uploa>d.
* @memberof AssetAdministrationShellRepositoryApi
*/
@@ -53,7 +61,6 @@ export interface IAssetAdministrationShellRepositoryApi {
image: Blob,
fileName: string,
options?: object,
- basePath?: string,
): Promise>;
/**
@@ -70,19 +77,23 @@ export interface IAssetAdministrationShellRepositoryApi {
}
export interface ISubmodelRepositoryApi {
+ /**
+ * Returns the base URL of this repository.
+ */
+ getBaseUrl(): string;
+
/**
* @summary Retrieves the submodel
* @param {string} submodelId The Submodels unique id
* @param {*} [options] Override http request option
- * @param {string} [basePath] The URL for the current repository endpoint.
* @throws {RequiredError}
* @memberof SubmodelRepositoryApi
*/
- getSubmodelById(submodelId: string, options?: object, basePath?: string): Promise>;
+ getSubmodelById(submodelId: string, options?: object): Promise>;
/**
* @summary Retrieves the attachment from a submodel element
- * @param submodelId The id of the submodel the submodel element is part of
+ * @param submodelId The id of the submodel element is part of
* @param submodelElementPath The path to the submodel element
* @param {*} [options] Override http request option
* @memberof SubmodelRepositoryApi
@@ -97,7 +108,7 @@ export interface ISubmodelRepositoryApi {
* @summary Creates a new submodel in the Submodel repository.
* @param {Submodel} submodel - The submodel object to be created.
* @param {object} [options] - Optional. Additional options to override default HTTP request settings.
- * @returns {PromiseSubmodel>} A promise that resolves to the newly created submode>l.
+ * @returns {PromiseSubmodel>} A promise that resolves to the newly created submodel.
* @memberof SubmodelRepositoryApi
*/
postSubmodel(submodel: Submodel, options?: object): Promise>;
diff --git a/src/lib/api/configuration-shell-api/configurationShellApi.ts b/src/lib/api/configuration-shell-api/configurationShellApi.ts
index 2227904..cdc596f 100644
--- a/src/lib/api/configuration-shell-api/configurationShellApi.ts
+++ b/src/lib/api/configuration-shell-api/configurationShellApi.ts
@@ -1,7 +1,7 @@
import { Submodel } from '@aas-core-works/aas-core3.0-typescript/types';
-import { ConfigurationShellApiInterface } from 'lib/api/configuration-shell-api/configurationShellApiInterface';
+import { IConfigurationShellApi } from 'lib/api/configuration-shell-api/configurationShellApiInterface';
-export class ConfigurationShellApi implements ConfigurationShellApiInterface {
+export class ConfigurationShellApi implements IConfigurationShellApi {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise };
basePath: string;
use_authentication: boolean;
diff --git a/src/lib/api/configuration-shell-api/configurationShellApiInterface.ts b/src/lib/api/configuration-shell-api/configurationShellApiInterface.ts
index 110ceb1..5d1c94a 100644
--- a/src/lib/api/configuration-shell-api/configurationShellApiInterface.ts
+++ b/src/lib/api/configuration-shell-api/configurationShellApiInterface.ts
@@ -1,6 +1,6 @@
import { Submodel } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
-export interface ConfigurationShellApiInterface {
+export interface IConfigurationShellApi {
getIdGenerationSettings(): Promise;
processGetIdGenerationSettings(response: Response): Promise;
@@ -14,12 +14,7 @@ export interface ConfigurationShellApiInterface {
},
): Promise;
- putSingleSettingValue(
- path: string,
- bearerToken: string,
- value: string,
- settingsType: string,
- ): Promise;
+ putSingleSettingValue(path: string, bearerToken: string, value: string, settingsType: string): Promise;
processPutSingleSettingValue(response: Response): Promise;
}
diff --git a/src/lib/api/discovery-service-api/discoveryServiceApi.ts b/src/lib/api/discovery-service-api/discoveryServiceApi.ts
index f03cf7d..b79c314 100644
--- a/src/lib/api/discovery-service-api/discoveryServiceApi.ts
+++ b/src/lib/api/discovery-service-api/discoveryServiceApi.ts
@@ -1,42 +1,61 @@
import { encodeBase64 } from 'lib/util/Base64Util';
import { DiscoveryEntry, IDiscoveryServiceApi } from 'lib/api/discovery-service-api/discoveryServiceApiInterface';
import { DiscoveryServiceApiInMemory } from 'lib/api/discovery-service-api/discoveryServiceApiInMemory';
-import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { ApiResponseWrapper, wrapErrorCode, wrapSuccess } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { SpecificAssetId } from '@aas-core-works/aas-core3.0-typescript/types';
+import * as path from 'node:path';
+import ServiceReachable from 'test-utils/TestUtils';
-export class DiscoveryServiceApi implements IDiscoveryServiceApi {
- baseUrl: string;
+type DiscoveryEntryResponse = {
+ paging_metadata: object;
+ result: string[];
+};
+export class DiscoveryServiceApi implements IDiscoveryServiceApi {
private constructor(
- protected _baseUrl: string = '',
+ protected baseUrl: string,
protected http: {
fetch(url: RequestInfo, init?: RequestInit): Promise>;
},
- ) {
- this.baseUrl = _baseUrl;
- }
+ ) {}
static create(
- _baseUrl: string = '',
+ baseUrl: string,
http: {
fetch(url: RequestInfo, init?: RequestInit): Promise>;
},
): DiscoveryServiceApi {
- return new DiscoveryServiceApi(_baseUrl, http);
+ return new DiscoveryServiceApi(baseUrl, http);
}
- static createNull(options: {
- discoveryEntries: { assetId: string; aasIds: string[] }[];
- }): DiscoveryServiceApiInMemory {
- return new DiscoveryServiceApiInMemory(options);
+ static createNull(
+ baseUrl: string,
+ discoveryEntries: { assetId: string; aasId: string }[],
+ reachable: ServiceReachable = ServiceReachable.Yes,
+ ): DiscoveryServiceApiInMemory {
+ return new DiscoveryServiceApiInMemory(baseUrl, discoveryEntries, reachable);
}
- async linkAasIdAndAssetId(aasId: string, assetId: string) {
- return this.postAllAssetLinksById(aasId, [
- {
- name: 'globalAssetId',
- value: assetId,
- },
- ]);
+ getBaseUrl(): string {
+ return this.baseUrl;
+ }
+
+ async linkAasIdAndAssetId(
+ aasId: string,
+ assetId: string,
+ apikey?: string,
+ ): Promise> {
+ const assetLink = {
+ name: 'globalAssetId',
+ value: assetId,
+ } as SpecificAssetId;
+ const options = apikey ? { ApiKey: apikey } : undefined;
+ const response = await this.postAllAssetLinksById(aasId, assetLink, options);
+ if (!response.isSuccess) return wrapErrorCode(response.errorCode, response.message);
+ return wrapSuccess({
+ aasId: aasId,
+ asset: response.result,
+ });
}
async getAasIdsByAssetId(assetId: string) {
@@ -44,53 +63,47 @@ export class DiscoveryServiceApi implements IDiscoveryServiceApi {
{
name: 'globalAssetId',
value: assetId,
- },
+ } as SpecificAssetId,
]);
}
- async deleteAllAssetLinksById(aasId: string): Promise> {
- const b64_aasId = encodeBase64(aasId);
-
- const headers = {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- };
-
- return this.http.fetch(`${this.baseUrl}/lookup/shells/${b64_aasId}`, {
- method: 'DELETE',
- headers,
- });
- }
-
async getAllAssetAdministrationShellIdsByAssetLink(
- assetIds: { name: string; value: string }[],
- ): Promise> {
+ assetIds: SpecificAssetId[],
+ options?: object,
+ ): Promise> {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
+ ...options,
};
- const url = new URL(`${this.baseUrl}/lookup/shells`);
+ const url = new URL(path.posix.join(this.baseUrl, 'lookup/shells'));
assetIds.forEach((obj) => {
url.searchParams.append('assetIds', encodeBase64(JSON.stringify(obj)));
});
- return this.http.fetch(url.toString(), {
+ const response = await this.http.fetch(url.toString(), {
method: 'GET',
headers,
});
+
+ if (!response.isSuccess) return wrapErrorCode(response.errorCode, response.message);
+
+ return wrapSuccess(response.result.result);
}
- async getAllAssetLinksById(aasId: string): Promise> {
+ async getAllAssetLinksById(aasId: string, options?: object): Promise> {
const b64_aasId = encodeBase64(aasId);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
+ ...options,
};
- return this.http.fetch(`${this.baseUrl}/lookup/shells/${b64_aasId}`, {
+ const url = path.posix.join(this.baseUrl, 'lookup/shells', b64_aasId);
+ return this.http.fetch(url, {
method: 'GET',
headers,
});
@@ -98,21 +111,38 @@ export class DiscoveryServiceApi implements IDiscoveryServiceApi {
async postAllAssetLinksById(
aasId: string,
- assetLinks: DiscoveryEntry[],
- apiKey?: string,
- ): Promise> {
+ assetLinks: SpecificAssetId, // this is NOT a list in the specification
+ options?: object,
+ ): Promise> {
const b64_aasId = encodeBase64(aasId);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
- ...(apiKey && { ApiKey: apiKey }),
+ ...options,
};
- return this.http.fetch(`${this.baseUrl}/lookup/shells/${b64_aasId}`, {
+ const url = path.posix.join(this.baseUrl, 'lookup/shells', b64_aasId);
+ return this.http.fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(assetLinks),
});
}
+
+ async deleteAllAssetLinksById(aasId: string, options?: object): Promise> {
+ const b64_aasId = encodeBase64(aasId);
+
+ const headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ ...options,
+ };
+
+ const url = path.posix.join(this.baseUrl, 'lookup/shells', b64_aasId);
+ return this.http.fetch(url, {
+ method: 'DELETE',
+ headers,
+ });
+ }
}
diff --git a/src/lib/api/discovery-service-api/discoveryServiceApiInMemory.ts b/src/lib/api/discovery-service-api/discoveryServiceApiInMemory.ts
index 7d9cc07..e8dc886 100644
--- a/src/lib/api/discovery-service-api/discoveryServiceApiInMemory.ts
+++ b/src/lib/api/discovery-service-api/discoveryServiceApiInMemory.ts
@@ -1,30 +1,67 @@
import { DiscoveryEntry, IDiscoveryServiceApi } from 'lib/api/discovery-service-api/discoveryServiceApiInterface';
-import { ApiResponseWrapper, ApiResultStatus, wrapErrorCode, wrapSuccess } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import {
+ ApiResponseWrapper,
+ ApiResultStatus,
+ wrapErrorCode,
+ wrapSuccess,
+} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { SpecificAssetId } from '@aas-core-works/aas-core3.0-typescript/types';
+import { isEqual } from 'lodash';
+import ServiceReachable from 'test-utils/TestUtils';
export class DiscoveryServiceApiInMemory implements IDiscoveryServiceApi {
- private discoveryEntries: { assetId: string; aasIds: string[] }[];
+ constructor(
+ protected baseUrl: string,
+ protected discoveryEntries: { assetId: string; aasId: string }[],
+ protected reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {}
- constructor(options: { discoveryEntries: { assetId: string; aasIds: string[] }[] }) {
- this.discoveryEntries = options.discoveryEntries;
+ getBaseUrl(): string {
+ return this.baseUrl;
}
- async linkAasIdAndAssetId(_aasId: string, _assetId: string): Promise> {
- throw new Error('Method not implemented.');
+ async linkAasIdAndAssetId(
+ aasId: string,
+ assetId: string,
+ _apikey?: string,
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const newEntry = {
+ aasId: aasId,
+ assetId: assetId,
+ };
+ if (this.discoveryEntries.find((value) => isEqual(value, newEntry))) {
+ return wrapErrorCode(
+ ApiResultStatus.UNKNOWN_ERROR,
+ `Link for AAS '${aasId}' and asset '${assetId}' already in Discovery '${this.baseUrl}'`,
+ );
+ }
+ this.discoveryEntries.push(newEntry);
+ const discoveryEntry = {
+ aasId: aasId,
+ asset: {
+ name: 'globalAssetId',
+ value: assetId,
+ },
+ } as DiscoveryEntry;
+ return wrapSuccess(discoveryEntry);
}
- async getAasIdsByAssetId(
- assetId: string,
- ): Promise> {
- for (const discoveryEntry of this.discoveryEntries) {
- if (discoveryEntry.assetId === assetId)
- return Promise.resolve(
- wrapSuccess({
- paging_metadata: '',
- result: discoveryEntry.aasIds,
- }),
- );
+ async getAasIdsByAssetId(assetId: string): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundEntries = this.discoveryEntries.filter((entry) => entry.assetId === assetId);
+ if (foundEntries.length <= 0) {
+ return Promise.resolve(
+ wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `No AAS with ID '${assetId}' found in Discovery '${this.baseUrl}'`,
+ ),
+ );
}
- return Promise.resolve(wrapErrorCode(ApiResultStatus.NOT_FOUND, 'not found'));
+
+ return Promise.resolve(wrapSuccess(foundEntries.map((entry) => entry.aasId)));
}
async deleteAllAssetLinksById(_aasId: string): Promise> {
@@ -32,19 +69,45 @@ export class DiscoveryServiceApiInMemory implements IDiscoveryServiceApi {
}
async getAllAssetAdministrationShellIdsByAssetLink(
- _assetIds: { name: string; value: string }[],
- ): Promise> {
- throw new Error('Method not implemented.');
+ assetIds: SpecificAssetId[],
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const ids = assetIds.map((id) => id.value);
+ const foundEntries = this.discoveryEntries.filter((entry) => ids.includes(entry.assetId));
+ if (foundEntries.length <= 0) {
+ return Promise.resolve(
+ wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `No AAS with given specific asset IDs found in Discovery '${this.baseUrl}'`,
+ ),
+ );
+ }
+
+ return wrapSuccess(foundEntries.map((entry) => entry.aasId));
}
- async getAllAssetLinksById(_aasId: string): Promise> {
+ async getAllAssetLinksById(_aasId: string): Promise> {
throw new Error('Method not implemented.');
}
async postAllAssetLinksById(
- _aasId: string,
- _assetLinks: DiscoveryEntry[],
- ): Promise> {
- throw new Error('Method not implemented.');
+ aasId: string,
+ assetLinks: SpecificAssetId,
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const newEntry = {
+ aasId: aasId,
+ assetId: assetLinks.value,
+ };
+ if (this.discoveryEntries.includes(newEntry)) {
+ return wrapErrorCode(
+ ApiResultStatus.UNKNOWN_ERROR,
+ `Link for AAS '${aasId}' and asset '${assetLinks.value}' already in Discovery '${this.baseUrl}'`,
+ );
+ }
+ this.discoveryEntries.push(newEntry);
+ return wrapSuccess(assetLinks);
}
}
diff --git a/src/lib/api/discovery-service-api/discoveryServiceApiInterface.ts b/src/lib/api/discovery-service-api/discoveryServiceApiInterface.ts
index 94e8e1e..ebfbc71 100644
--- a/src/lib/api/discovery-service-api/discoveryServiceApiInterface.ts
+++ b/src/lib/api/discovery-service-api/discoveryServiceApiInterface.ts
@@ -1,26 +1,62 @@
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { SpecificAssetId } from '@aas-core-works/aas-core3.0-typescript/types';
export type DiscoveryEntry = {
- name: string;
- value: string;
+ aasId: string;
+ asset: SpecificAssetId;
};
export interface IDiscoveryServiceApi {
- linkAasIdAndAssetId(aasId: string, assetId: string): Promise>;
+ /**
+ * Returns the base URL of this discovery endpoint.
+ */
+ getBaseUrl(): string;
- getAasIdsByAssetId(assetId: string): Promise>;
+ /**
+ * Post a single AAS - asset link to the discovery service.
+ * @param aasId the ID of the AAS.
+ * @param assetId the ID of the asset.
+ * @param apikey the apikey to access the discovery service
+ */
+ linkAasIdAndAssetId(aasId: string, assetId: string, apikey?: string): Promise>;
- deleteAllAssetLinksById(aasId: string): Promise>;
+ /**
+ * Returns a list of all AAS IDs for the given global assetId.
+ * @param assetId The asset ID to search for.
+ */
+ getAasIdsByAssetId(assetId: string): Promise>;
- getAllAssetAdministrationShellIdsByAssetLink(
- assetIds: DiscoveryEntry[],
- ): Promise>;
+ /**
+ * Returns a list of Asset Administration Shell ids based on asset identifier key-value-pairs.
+ * @param assetIds The specific assetId of an asset identifier, which could be the globalAssetId or specificAssetIds.
+ * @param options additional options passed to the fetch method
+ * @return Identifiers of all Asset Administration Shells which contain all asset identifier key-value-pairs in their asset information, i.e. AND-match of key-value-pairs per Asset Administration Shell.
+ */
+ getAllAssetAdministrationShellIdsByAssetLink(assetIds: SpecificAssetId[], options?: object): Promise>;
- getAllAssetLinksById(aasId: string): Promise>;
+ /**
+ * Returns a list of asset identifier key-value-pairs based on an Asset Administration Shell id to edit discoverable content.
+ * @param aasId The Asset Administration Shell’s unique id.
+ * @param options additional options passed to the fetch method
+ * @return Requested asset identifier, which could be the globalAssetId or specificAssetIds.
+ */
+ getAllAssetLinksById(aasId: string, options?: object): Promise>;
- postAllAssetLinksById(
- aasId: string,
- assetLinks: DiscoveryEntry[],
- apiKey?: string,
- ): Promise>;
+ /**
+ * Creates new asset identifier key-value-pairs linked to an Asset Administration Shell for discoverable content. The existing content might have to be deleted first.
+ * @param aasId The Asset Administration Shell’s unique id.
+ * @param assetLinks Asset identifier, which could be the globalAssetId or specificAssetIds.
+ * @param options additional options passed to the fetch method
+ * @return Asset identifier created successfully.
+ *
+ * @remarks The specification explicitly gives a cardinality of one here.
+ */
+ postAllAssetLinksById(aasId: string, assetLinks: SpecificAssetId, options?: object): Promise>;
+
+ /**
+ * Deletes all asset identifier key-value-pairs linked to an Asset Administration Shell to edit discoverable content.
+ * @param aasId The Asset Administration Shell’s unique id.
+ * @param options additional options passed to the fetch method
+ */
+ deleteAllAssetLinksById(aasId: string, options?: object): Promise>;
}
diff --git a/src/lib/api/registry-service-api/registryServiceApi.ts b/src/lib/api/registry-service-api/registryServiceApi.ts
index 100645f..9d18483 100644
--- a/src/lib/api/registry-service-api/registryServiceApi.ts
+++ b/src/lib/api/registry-service-api/registryServiceApi.ts
@@ -2,38 +2,42 @@ import { encodeBase64 } from 'lib/util/Base64Util';
import { AssetAdministrationShellDescriptor } from 'lib/types/registryServiceTypes';
import { IRegistryServiceApi } from 'lib/api/registry-service-api/registryServiceApiInterface';
import {
- INullableAasRegistryEndpointEntries,
+ AasRegistryEndpointEntryInMemory,
RegistryServiceApiInMemory,
} from 'lib/api/registry-service-api/registryServiceApiInMemory';
import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript/types';
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import path from 'node:path';
+import ServiceReachable from 'test-utils/TestUtils';
export class RegistryServiceApi implements IRegistryServiceApi {
- baseUrl: string;
-
constructor(
+ protected baseUrl: string = '',
protected http: {
fetch(url: RequestInfo | URL, init?: RequestInit): Promise>;
},
- protected _baseUrl: string = '',
- ) {
- this.baseUrl = _baseUrl;
- }
+ ) {}
static create(
- _baseUrl: string | undefined,
+ baseUrl: string,
mnestixFetch: {
- fetch(url: RequestInfo, init?: RequestInit | undefined): Promise>;
+ fetch(url: RequestInfo, init?: RequestInit): Promise>;
},
) {
- return new RegistryServiceApi(mnestixFetch, _baseUrl);
+ return new RegistryServiceApi(baseUrl, mnestixFetch);
+ }
+
+ static createNull(
+ baseUrl: string,
+ registryShellDescriptors: AssetAdministrationShellDescriptor[],
+ registryShellEndpoints: AasRegistryEndpointEntryInMemory[],
+ reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {
+ return new RegistryServiceApiInMemory(baseUrl, registryShellDescriptors, registryShellEndpoints, reachable);
}
- static createNull(options: {
- registryShellDescriptorEntries: AssetAdministrationShellDescriptor[] | null;
- shellsAvailableOnEndpoints: INullableAasRegistryEndpointEntries[] | null;
- }) {
- return new RegistryServiceApiInMemory(options);
+ getBaseUrl(): string {
+ return this.baseUrl;
}
async getAssetAdministrationShellDescriptorById(
@@ -46,7 +50,7 @@ export class RegistryServiceApi implements IRegistryServiceApi {
'Content-Type': 'application/json',
};
- const url = new URL(`/shell-descriptors/${b64_aasId}`, this.baseUrl);
+ const url = new URL(path.posix.join(this.baseUrl, 'shell-descriptors', b64_aasId));
return this.http.fetch(url, {
method: 'GET',
@@ -62,7 +66,7 @@ export class RegistryServiceApi implements IRegistryServiceApi {
'Content-Type': 'application/json',
};
- const url = new URL('/shell-descriptors', this.baseUrl);
+ const url = new URL(path.posix.join(this.baseUrl, 'shell-descriptors'));
return await this.http.fetch(url.toString(), {
method: 'POST',
@@ -71,14 +75,6 @@ export class RegistryServiceApi implements IRegistryServiceApi {
});
}
- async getAssetAdministrationShellFromEndpoint(
- endpoint: URL,
- ): Promise> {
- return this.http.fetch(endpoint.toString(), {
- method: 'GET',
- });
- }
-
async putAssetAdministrationShellDescriptorById(
aasId: string,
shellDescriptor: AssetAdministrationShellDescriptor,
@@ -90,7 +86,7 @@ export class RegistryServiceApi implements IRegistryServiceApi {
'Content-Type': 'application/json',
};
- const url = new URL(`/shell-descriptors/${b64_aasId}`, this.baseUrl);
+ const url = new URL(path.posix.join(this.baseUrl, 'shell-descriptors', b64_aasId));
return this.http.fetch(url, {
method: 'PUT',
@@ -98,4 +94,12 @@ export class RegistryServiceApi implements IRegistryServiceApi {
body: JSON.stringify(shellDescriptor),
});
}
+
+ async getAssetAdministrationShellFromEndpoint(
+ endpoint: URL,
+ ): Promise> {
+ return this.http.fetch(endpoint.toString(), {
+ method: 'GET',
+ });
+ }
}
diff --git a/src/lib/api/registry-service-api/registryServiceApiInMemory.ts b/src/lib/api/registry-service-api/registryServiceApiInMemory.ts
index 073b001..74d2f1b 100644
--- a/src/lib/api/registry-service-api/registryServiceApiInMemory.ts
+++ b/src/lib/api/registry-service-api/registryServiceApiInMemory.ts
@@ -1,61 +1,90 @@
import { IRegistryServiceApi } from 'lib/api/registry-service-api/registryServiceApiInterface';
import { AssetAdministrationShellDescriptor } from 'lib/types/registryServiceTypes';
import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
-import { ApiResponseWrapper, ApiResultStatus, wrapErrorCode, wrapResponse } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import {
+ ApiResponseWrapper,
+ ApiResultStatus,
+ wrapErrorCode,
+ wrapResponse,
+ wrapSuccess,
+} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import ServiceReachable from 'test-utils/TestUtils';
-export interface INullableAasRegistryEndpointEntries {
+export type AasRegistryEndpointEntryInMemory = {
endpoint: URL | string;
aas: AssetAdministrationShell;
-}
+};
+
+const options = {
+ headers: { 'Content-type': 'application/json; charset=utf-8' },
+};
export class RegistryServiceApiInMemory implements IRegistryServiceApi {
- private readonly registryShellDescriptorEntries: AssetAdministrationShellDescriptor[] | null;
- private readonly shellsAvailableOnEndpoints: INullableAasRegistryEndpointEntries[] | null;
- baseUrl: string;
+ constructor(
+ readonly baseUrl: string,
+ readonly registryShellDescriptors: AssetAdministrationShellDescriptor[],
+ readonly registryShellEndpoints: AasRegistryEndpointEntryInMemory[],
+ readonly reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {}
- constructor(options: {
- registryShellDescriptorEntries: AssetAdministrationShellDescriptor[] | null;
- shellsAvailableOnEndpoints: INullableAasRegistryEndpointEntries[] | null;
- }) {
- this.registryShellDescriptorEntries = options.registryShellDescriptorEntries;
- this.shellsAvailableOnEndpoints = options.shellsAvailableOnEndpoints;
+ getBaseUrl(): string {
+ return this.baseUrl;
}
- postAssetAdministrationShellDescriptor(
- _shellDescriptor: AssetAdministrationShellDescriptor,
+ async postAssetAdministrationShellDescriptor(
+ shellDescriptor: AssetAdministrationShellDescriptor,
): Promise> {
- throw new Error('Method not implemented.');
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ if (this.registryShellDescriptors.find((descriptor) => descriptor.id === shellDescriptor.id))
+ return wrapErrorCode(
+ ApiResultStatus.UNKNOWN_ERROR,
+ `Shell Descriptor for AAS '${shellDescriptor.id}' already registered at '${this.getBaseUrl()}'`,
+ );
+ this.registryShellDescriptors.push(shellDescriptor);
+ return wrapSuccess(undefined);
}
async getAssetAdministrationShellDescriptorById(
aasId: string,
): Promise> {
- if (!this.registryShellDescriptorEntries) return Promise.reject(new Error('no registry configuration'));
- let shellDescriptor: AssetAdministrationShellDescriptor;
- for (shellDescriptor of this.registryShellDescriptorEntries) {
- if (shellDescriptor.id === aasId) {
- const response = new Response(JSON.stringify(shellDescriptor));
- const value = await wrapResponse(response);
- return Promise.resolve(value);
- }
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundDescriptor = this.registryShellDescriptors.find((shellDescriptor) => shellDescriptor.id === aasId);
+ if (!foundDescriptor) {
+ return Promise.resolve(
+ wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `no shell descriptor for aasId '${aasId}' found in registry '${this.baseUrl}'`,
+ ),
+ );
}
- return Promise.resolve(wrapErrorCode(ApiResultStatus.NOT_FOUND, 'no shell descriptor for aasId:' + aasId));
+
+ const response = new Response(JSON.stringify(foundDescriptor), options);
+ const value = await wrapResponse(response);
+ return Promise.resolve(value);
}
async getAssetAdministrationShellFromEndpoint(
endpoint: URL,
): Promise> {
- if (!this.shellsAvailableOnEndpoints) return Promise.reject(new Error('no registry configuration'));
- let registryEndpoint: INullableAasRegistryEndpointEntries;
- for (registryEndpoint of this.shellsAvailableOnEndpoints) {
- if (registryEndpoint.endpoint.toString() === endpoint.toString()) {
- const value = await wrapResponse(
- new Response(JSON.stringify(registryEndpoint.aas)),
- );
- return Promise.resolve(value);
- }
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundEndpoint = this.registryShellEndpoints.find(
+ (shellEndpoint) => shellEndpoint.endpoint.toString() === endpoint.toString(),
+ );
+ if (!foundEndpoint) {
+ return Promise.resolve(
+ wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `no shell for endpoint '${endpoint}' found in registry '${this.getBaseUrl()}`,
+ ),
+ );
}
- return Promise.resolve(wrapErrorCode(ApiResultStatus.NOT_FOUND, 'no shell for url:' + endpoint));
+
+ const response = new Response(JSON.stringify(foundEndpoint.aas), options);
+ const value = await wrapResponse(response);
+ return Promise.resolve(value);
}
async putAssetAdministrationShellDescriptorById(
diff --git a/src/lib/api/registry-service-api/registryServiceApiInterface.ts b/src/lib/api/registry-service-api/registryServiceApiInterface.ts
index 649723e..ac9d022 100644
--- a/src/lib/api/registry-service-api/registryServiceApiInterface.ts
+++ b/src/lib/api/registry-service-api/registryServiceApiInterface.ts
@@ -3,7 +3,10 @@ import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
export interface IRegistryServiceApi {
- baseUrl: string;
+ /**
+ * Returns the base URL of this AAS registry endpoint.
+ */
+ getBaseUrl(): string;
getAssetAdministrationShellDescriptorById(
aasId: string,
@@ -16,5 +19,7 @@ export interface IRegistryServiceApi {
getAssetAdministrationShellFromEndpoint(endpoint: URL): Promise>;
- postAssetAdministrationShellDescriptor(shellDescriptor: AssetAdministrationShellDescriptor): Promise>;
+ postAssetAdministrationShellDescriptor(
+ shellDescriptor: AssetAdministrationShellDescriptor,
+ ): Promise>;
}
diff --git a/src/lib/api/serverFetch.ts b/src/lib/api/serverFetch.ts
index e2a7a50..3b3b15a 100644
--- a/src/lib/api/serverFetch.ts
+++ b/src/lib/api/serverFetch.ts
@@ -4,7 +4,6 @@ import {
ApiResponseWrapper,
ApiResultStatus,
wrapErrorCode,
- wrapFileResponse,
wrapResponse,
} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
@@ -27,14 +26,7 @@ export async function performServerFetch(
): Promise> {
try {
const response = await fetch(input, init);
-
- const contentType = response.headers.get('Content-Type') || '';
-
- if (contentType && !contentType.includes('application/json')) {
- return wrapFileResponse(response);
- }
-
- return wrapResponse(response);
+ return await wrapResponse(response);
} catch (e) {
const message = 'this could be a network error';
console.warn(message, '\nException message:', e.message);
diff --git a/src/lib/api/submodel-registry-service/submodelRegistryServiceApi.ts b/src/lib/api/submodel-registry-service/submodelRegistryServiceApi.ts
index 7a7a80a..fd3aecd 100644
--- a/src/lib/api/submodel-registry-service/submodelRegistryServiceApi.ts
+++ b/src/lib/api/submodel-registry-service/submodelRegistryServiceApi.ts
@@ -1,20 +1,37 @@
import { SubmodelDescriptor } from 'lib/types/registryServiceTypes';
import { encodeBase64 } from 'lib/util/Base64Util';
-import { ISubmodelRegistryServiceApiInterface } from 'lib/api/submodel-registry-service/ISubmodelRegistryServiceApiInterface';
+import { ISubmodelRegistryServiceApi } from 'lib/api/submodel-registry-service/submodelRegistryServiceApiInterface';
import { FetchAPI } from 'lib/api/basyx-v3/api';
-import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
-
-export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiInterface {
- baseUrl: string;
- http: FetchAPI;
+import {
+ ApiResponseWrapper,
+ ApiResultStatus,
+ wrapErrorCode,
+ wrapSuccess,
+} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { Submodel } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
+import path from 'node:path';
+import ServiceReachable from 'test-utils/TestUtils';
+
+export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApi {
+ constructor(
+ private baseUrl: string,
+ private http: FetchAPI,
+ ) {}
+
+ static create(baseUrl: string, mnestixFetch: FetchAPI) {
+ return new SubmodelRegistryServiceApi(baseUrl, mnestixFetch);
+ }
- constructor(http: FetchAPI, _baseUrl: string = '') {
- this.http = http;
- this.baseUrl = _baseUrl;
+ static createNull(
+ baseUrl: string,
+ registrySubmodelDescriptors: SubmodelDescriptor[],
+ reachable: ServiceReachable = ServiceReachable.Yes,
+ ) {
+ return new SubmodelRegistryServiceApiInMemory(baseUrl, registrySubmodelDescriptors, reachable);
}
- static create(_baseUrl: string | undefined, mnestixFetch: FetchAPI) {
- return new SubmodelRegistryServiceApi(mnestixFetch, _baseUrl);
+ getBasePath(): string {
+ return this.baseUrl;
}
async getSubmodelDescriptorById(submodelId: string): Promise> {
@@ -24,7 +41,7 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
Accept: 'application/json',
};
- const url = new URL(`${this.baseUrl}/submodel-descriptors/${b64_submodelId}`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors', b64_submodelId));
return await this.http.fetch(url.toString(), {
method: 'GET',
@@ -33,17 +50,16 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
}
async putSubmodelDescriptorById(
- submodelId: string,
submodelDescriptor: SubmodelDescriptor,
): Promise> {
- const b64_submodelId = encodeBase64(submodelId);
+ const b64_submodelId = encodeBase64(submodelDescriptor.id);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
- const url = new URL(`${this.baseUrl}/submodel-descriptors/${b64_submodelId}`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors', b64_submodelId));
return await this.http.fetch(url.toString(), {
method: 'PUT',
@@ -55,7 +71,7 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
async deleteSubmodelDescriptorById(submodelId: string): Promise> {
const b64_submodelId = encodeBase64(submodelId);
- const url = new URL(`${this.baseUrl}/submodel-descriptors/${b64_submodelId}`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors', b64_submodelId));
return await this.http.fetch(url.toString(), {
method: 'DELETE',
@@ -67,7 +83,7 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
Accept: 'application/json',
};
- const url = new URL(`${this.baseUrl}/submodel-descriptors`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors'));
return await this.http.fetch(url.toString(), {
method: 'GET',
@@ -83,7 +99,7 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
'Content-Type': 'application/json',
};
- const url = new URL(`${this.baseUrl}/submodel-descriptors`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors'));
return await this.http.fetch(url.toString(), {
method: 'POST',
@@ -93,10 +109,93 @@ export class SubmodelRegistryServiceApi implements ISubmodelRegistryServiceApiIn
}
async deleteAllSubmodelDescriptors(): Promise> {
- const url = new URL(`${this.baseUrl}/submodel-descriptors`);
+ const url = new URL(path.posix.join(this.baseUrl, 'submodel-descriptors'));
return this.http.fetch(url.toString(), {
method: 'DELETE',
});
}
+
+ async getSubmodelFromEndpoint(endpoint: string): Promise> {
+ return this.http.fetch(endpoint.toString(), {
+ method: 'GET',
+ });
+ }
+}
+
+class SubmodelRegistryServiceApiInMemory implements ISubmodelRegistryServiceApi {
+ readonly registrySubmodelDescriptors: Map;
+
+ constructor(
+ readonly baseUrl: string,
+ registrySubmodelDescriptors: SubmodelDescriptor[],
+ readonly reachable: ServiceReachable,
+ ) {
+ this.registrySubmodelDescriptors = new Map();
+ registrySubmodelDescriptors.forEach((submodelDescriptor) => {
+ this.registrySubmodelDescriptors.set(submodelDescriptor.id, submodelDescriptor);
+ });
+ }
+
+ getBasePath(): string {
+ return this.baseUrl;
+ }
+
+ async getSubmodelDescriptorById(submodelId: string): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ const foundDescriptor = this.registrySubmodelDescriptors.get(submodelId);
+ if (foundDescriptor) return wrapSuccess(foundDescriptor);
+ return wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `No Submodel descriptor for submodel id '${submodelId}' found in '${this.getBasePath()}'`,
+ );
+ }
+
+ async putSubmodelDescriptorById(
+ submodelDescriptor: SubmodelDescriptor,
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ this.registrySubmodelDescriptors.set(submodelDescriptor.id, submodelDescriptor);
+ return wrapSuccess(submodelDescriptor);
+ }
+
+ async deleteSubmodelDescriptorById(submodelId: string): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ this.registrySubmodelDescriptors.delete(submodelId);
+ return wrapSuccess(undefined);
+ }
+
+ async getAllSubmodelDescriptors(): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ return wrapSuccess([...this.registrySubmodelDescriptors.values()]);
+ }
+
+ async postSubmodelDescriptor(
+ submodelDescriptor: SubmodelDescriptor,
+ ): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ if (this.registrySubmodelDescriptors.has(submodelDescriptor.id))
+ return wrapErrorCode(
+ ApiResultStatus.UNKNOWN_ERROR,
+ `Submodel registry '${this.getBasePath()}' already has a submodel descriptor for '${submodelDescriptor.id}'`,
+ );
+ this.registrySubmodelDescriptors.set(submodelDescriptor.id, submodelDescriptor);
+ return wrapSuccess(submodelDescriptor);
+ }
+
+ async deleteAllSubmodelDescriptors(): Promise> {
+ if (this.reachable !== ServiceReachable.Yes)
+ return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable');
+ this.registrySubmodelDescriptors.clear();
+ return wrapSuccess(undefined);
+ }
+
+ getSubmodelFromEndpoint(_endpoint: string): Promise> {
+ throw new Error('Method not implemented.');
+ }
}
diff --git a/src/lib/api/submodel-registry-service/ISubmodelRegistryServiceApiInterface.ts b/src/lib/api/submodel-registry-service/submodelRegistryServiceApiInterface.ts
similarity index 59%
rename from src/lib/api/submodel-registry-service/ISubmodelRegistryServiceApiInterface.ts
rename to src/lib/api/submodel-registry-service/submodelRegistryServiceApiInterface.ts
index 3c2b70f..c5f86f4 100644
--- a/src/lib/api/submodel-registry-service/ISubmodelRegistryServiceApiInterface.ts
+++ b/src/lib/api/submodel-registry-service/submodelRegistryServiceApiInterface.ts
@@ -1,15 +1,16 @@
import { SubmodelDescriptor } from 'lib/types/registryServiceTypes';
import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { Submodel } from '@aas-core-works/aas-core3.0-typescript/types';
-export interface ISubmodelRegistryServiceApiInterface {
- baseUrl: string;
+export interface ISubmodelRegistryServiceApi {
+ /**
+ * Return the basePath of this registry service endpoint.
+ */
+ getBasePath(): string;
getSubmodelDescriptorById(submodelId: string): Promise>;
- putSubmodelDescriptorById(
- submodelId: string,
- submodelDescriptor: SubmodelDescriptor,
- ): Promise>;
+ putSubmodelDescriptorById(submodelDescriptor: SubmodelDescriptor): Promise>;
deleteSubmodelDescriptorById(submodelId: string): Promise>;
@@ -18,4 +19,6 @@ export interface ISubmodelRegistryServiceApiInterface {
postSubmodelDescriptor(submodelDescriptor: SubmodelDescriptor): Promise>;
deleteAllSubmodelDescriptors(): Promise>;
+
+ getSubmodelFromEndpoint(endpoint: string): Promise>;
}
diff --git a/src/lib/i18n/de.mnestix.ts b/src/lib/i18n/de.mnestix.ts
index 33d25b4..c9d6172 100644
--- a/src/lib/i18n/de.mnestix.ts
+++ b/src/lib/i18n/de.mnestix.ts
@@ -175,6 +175,8 @@ export const deMnestix = {
language: 'Sprache',
redirectsTo: 'Leitet weiter auf',
endResult: 'Endergebnis',
+ imprint: 'Impressum',
+ dataPrivacy: 'Datenschutz',
assetIdDocumentation: {
title: 'Wie Sie Ihre Asset ID mit Ihrem Repository verbinden',
industry40Heading: 'Industrie 4.0 Kontext',
diff --git a/src/lib/i18n/en.mnestix.ts b/src/lib/i18n/en.mnestix.ts
index da5fddf..3b54d54 100644
--- a/src/lib/i18n/en.mnestix.ts
+++ b/src/lib/i18n/en.mnestix.ts
@@ -172,6 +172,8 @@ export const enMnestix = {
language: 'Language',
redirectsTo: 'Redirects to',
endResult: 'End result',
+ imprint: 'Imprint',
+ dataPrivacy: 'Data Privacy',
assetIdDocumentation: {
title: 'How to connect your Asset ID with your AAS Repository',
industry40Heading: 'Industry 4.0 context',
diff --git a/src/lib/services/database/ConnectionTypeEnum.ts b/src/lib/services/database/ConnectionTypeEnum.ts
index 79cd7a5..aca8011 100644
--- a/src/lib/services/database/ConnectionTypeEnum.ts
+++ b/src/lib/services/database/ConnectionTypeEnum.ts
@@ -1,4 +1,15 @@
export enum ConnectionTypeEnum {
AAS_REPOSITORY = 'AAS_REPOSITORY',
SUBMODEL_REPOSITORY = 'SUBMODEL_REPOSITORY',
-}
\ No newline at end of file
+}
+
+export function getTypeAction(type: ConnectionTypeEnum): { id: string; typeName: string } {
+ switch (type) {
+ case ConnectionTypeEnum.AAS_REPOSITORY:
+ return { id: '0', typeName: ConnectionTypeEnum.AAS_REPOSITORY };
+ case ConnectionTypeEnum.SUBMODEL_REPOSITORY:
+ return { id: '2', typeName: ConnectionTypeEnum.SUBMODEL_REPOSITORY };
+ default:
+ throw new Error(`ConnectionType '${type}' not implemented.`);
+ }
+}
diff --git a/src/lib/services/database/PrismaConnector.ts b/src/lib/services/database/PrismaConnector.ts
index 4762687..1431e56 100644
--- a/src/lib/services/database/PrismaConnector.ts
+++ b/src/lib/services/database/PrismaConnector.ts
@@ -52,7 +52,7 @@ export class PrismaConnector implements IPrismaConnector {
return new PrismaConnector();
}
- static createNull(options: { aasUrls: string[] }) {
- return new PrismaConnectorInMemory(options.aasUrls);
+ static createNull(aasUrls: string[], submodelUrls: string[]) {
+ return new PrismaConnectorInMemory(aasUrls, submodelUrls);
}
}
diff --git a/src/lib/services/database/PrismaConnectorInMemory.ts b/src/lib/services/database/PrismaConnectorInMemory.ts
index 7b47fc4..c409d83 100644
--- a/src/lib/services/database/PrismaConnectorInMemory.ts
+++ b/src/lib/services/database/PrismaConnectorInMemory.ts
@@ -1,8 +1,12 @@
import { IPrismaConnector } from 'lib/services/database/PrismaConnectorInterface';
import { DataSourceFormData } from 'lib/services/database/PrismaConnector';
+import { isEqual } from 'lodash';
export class PrismaConnectorInMemory implements IPrismaConnector {
- constructor(protected connectionData: string[]) {}
+ constructor(
+ protected aasData: string[],
+ protected submodelData: string[],
+ ) {}
getConnectionData(): Promise<
{ type: { id: string; typeName: string } } & {
@@ -18,7 +22,10 @@ export class PrismaConnectorInMemory implements IPrismaConnector {
throw new Error('Method not implemented.');
}
- getConnectionDataByTypeAction(_type: { id: string; typeName: string }): Promise {
- return Promise.resolve(this.connectionData);
+ async getConnectionDataByTypeAction(type: { id: string; typeName: string }): Promise {
+ if (isEqual(type, { id: '0', typeName: 'AAS_REPOSITORY' })) return this.aasData;
+ if (isEqual(type, { id: '2', typeName: 'SUBMODEL_REPOSITORY' })) return this.submodelData;
+
+ throw new Error('Method not implemented.');
}
}
diff --git a/src/lib/services/list-service/ListService.data.json b/src/lib/services/list-service/ListService.data.json
new file mode 100644
index 0000000..4351f8a
--- /dev/null
+++ b/src/lib/services/list-service/ListService.data.json
@@ -0,0 +1,77 @@
+{
+ "assetAdministrationShells": [
+ {
+ "modelType": "AssetAdministrationShell",
+ "assetInformation": {
+ "assetKind": "Instance",
+ "assetType": "",
+ "defaultThumbnail": {
+ "contentType": "image/jpeg",
+ "path": "https://i40.xitaso.com/testListAas_thumbnail"
+ },
+ "globalAssetId": "https://i40.xitaso.com/testListAas_00",
+ "specificAssetIds": [
+ {
+ "name": "assetIdShort",
+ "value": "testListAas_00"
+ }
+ ]
+ },
+ "submodels": [],
+ "id": "https://i40.xitaso.com/aas/testListAas_00",
+ "idShort": "testListAas_00"
+ },
+ {
+ "modelType": "AssetAdministrationShell",
+ "assetInformation": {
+ "assetKind": "Instance",
+ "globalAssetId": "https://i40.xitaso.com/testListAas_01",
+ "specificAssetIds": [
+ {
+ "name": "assetIdShort",
+ "value": "testListAas_01"
+ }
+ ]
+ },
+ "id": "https://i40.xitaso.com/aas/testListAas_01",
+ "idShort": "testListAas_01"
+ },
+ {
+ "modelType": "AssetAdministrationShell",
+ "assetInformation": {
+ "assetKind": "Instance",
+ "globalAssetId": "https://i40.xitaso.com/testListAas_02",
+ "specificAssetIds": [
+ {
+ "name": "assetIdShort",
+ "value": "testListAas_02"
+ }
+ ]
+ },
+ "submodels": [],
+ "id": "https://i40.xitaso.com/aas/testListAas_02",
+ "idShort": "testListAas_02"
+ }
+ ],
+ "expectedResult": {
+ "success": true,
+ "entities": [
+ {
+ "aasId": "https://i40.xitaso.com/aas/testListAas_00",
+ "assetId": "https://i40.xitaso.com/testListAas_00",
+ "thumbnail": "https://i40.xitaso.com/testListAas_thumbnail"
+ },
+ {
+ "aasId": "https://i40.xitaso.com/aas/testListAas_01",
+ "assetId": "https://i40.xitaso.com/testListAas_01",
+ "thumbnail": ""
+ },
+ {
+ "aasId": "https://i40.xitaso.com/aas/testListAas_02",
+ "assetId": "https://i40.xitaso.com/testListAas_02",
+ "thumbnail": ""
+ }
+ ],
+ "cursor": "aHR0cHM6Ly9pNDAueGl0YXNvLmNvbS9hYXMvdGVzdExpc3RBYXNfMDI"
+ }
+}
\ No newline at end of file
diff --git a/src/lib/services/list-service/ListService.spec.ts b/src/lib/services/list-service/ListService.spec.ts
new file mode 100644
index 0000000..7326c32
--- /dev/null
+++ b/src/lib/services/list-service/ListService.spec.ts
@@ -0,0 +1,48 @@
+import { expect } from '@jest/globals';
+import { AasListDto, ListService } from 'lib/services/list-service/ListService';
+import testData from 'lib/services/list-service/ListService.data.json';
+import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript/types';
+import ServiceReachable from 'test-utils/TestUtils';
+
+const assetAdministrationShells = testData.assetAdministrationShells as unknown as AssetAdministrationShell[];
+const expectedData = testData.expectedResult as AasListDto;
+
+describe('ListService: Return List Entities', function () {
+ it('returns proper object when aas list is returned from aas repository', async () => {
+ // ARRANGE
+ const listService = ListService.createNull(assetAdministrationShells);
+
+ //ACT
+ const listServiceResult = await listService.getAasListEntities(5);
+
+ //ASSERT
+ expect(listServiceResult).toEqual(expectedData);
+ });
+
+ it('returns empty object when no aas is returned from aas repository', async () => {
+ // ARRANGE
+ const listService = ListService.createNull();
+
+ //ACT
+ const listServiceResult = await listService.getAasListEntities(5);
+
+ //ASSERT
+ expect(listServiceResult).toEqual({
+ success: true,
+ entities: [],
+ cursor: '',
+ });
+ });
+
+ it('return success false when aas repository is not reachable and returns error', async () => {
+ // ARRANGE
+ const listService = ListService.createNull([], ServiceReachable.No);
+
+ //ACT
+ const listServiceResult = await listService.getAasListEntities(5);
+
+ //ASSERT
+ expect(listServiceResult.success).toEqual(false);
+ expect(listServiceResult).toHaveProperty('error');
+ });
+});
diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts
new file mode 100644
index 0000000..eaf9d46
--- /dev/null
+++ b/src/lib/services/list-service/ListService.ts
@@ -0,0 +1,61 @@
+import { IAssetAdministrationShellRepositoryApi } from 'lib/api/basyx-v3/apiInterface';
+import { AssetAdministrationShellRepositoryApi } from 'lib/api/basyx-v3/api';
+import { mnestixFetch } from 'lib/api/infrastructure';
+import { AssetAdministrationShell } from '@aas-core-works/aas-core3.0-typescript/types';
+import ServiceReachable from 'test-utils/TestUtils';
+
+export type ListEntityDto = {
+ aasId: string;
+ assetId: string;
+ thumbnail: string;
+};
+
+export type AasListDto = {
+ success: boolean;
+ entities?: ListEntityDto[];
+ error?: object;
+ cursor?: string;
+};
+
+export class ListService {
+ private constructor(readonly targetAasRepositoryClient: IAssetAdministrationShellRepositoryApi) {}
+
+ static create(targetAasRepositoryBaseUrl: string): ListService {
+ const targetAasRepositoryClient = AssetAdministrationShellRepositoryApi.create(
+ targetAasRepositoryBaseUrl,
+ mnestixFetch(),
+ );
+ return new ListService(targetAasRepositoryClient);
+ }
+
+ static createNull(
+ shellsInRepositories: AssetAdministrationShell[] = [],
+ targetAasRepository = ServiceReachable.Yes,
+ ): ListService {
+ const targetAasRepositoryClient = AssetAdministrationShellRepositoryApi.createNull(
+ 'https://targetAasRepositoryClient.com',
+ shellsInRepositories,
+ targetAasRepository,
+ );
+ return new ListService(targetAasRepositoryClient);
+ }
+
+ async getAasListEntities(limit: number, cursor?: string): Promise {
+ const response = await this.targetAasRepositoryClient.getAllAssetAdministrationShells(limit, cursor);
+
+ if (!response.isSuccess) {
+ return { success: false, error: response };
+ }
+
+ const { result: assetAdministrationShells, paging_metadata } = response.result;
+ const nextCursor = paging_metadata.cursor;
+
+ const aasListDtos = assetAdministrationShells.map((aas) => ({
+ aasId: aas.id,
+ assetId: aas.assetInformation?.globalAssetId ?? '',
+ thumbnail: aas.assetInformation?.defaultThumbnail?.path ?? '',
+ }));
+
+ return { success: true, entities: aasListDtos, cursor: nextCursor };
+ }
+}
diff --git a/src/lib/services/aasListApiActions.ts b/src/lib/services/list-service/aasListApiActions.ts
similarity index 55%
rename from src/lib/services/aasListApiActions.ts
rename to src/lib/services/list-service/aasListApiActions.ts
index 1cc4d43..8d86e2a 100644
--- a/src/lib/services/aasListApiActions.ts
+++ b/src/lib/services/list-service/aasListApiActions.ts
@@ -2,9 +2,15 @@
import { mnestixFetchLegacy } from 'lib/api/infrastructure';
import { AasListClient, AasListEntry } from 'lib/api/generated-api/clients.g';
+import { ListService } from 'lib/services/list-service/ListService';
const aasListApi = AasListClient.create(process.env.MNESTIX_BACKEND_API_URL, mnestixFetchLegacy());
export async function getAasListEntries(): Promise {
return aasListApi.getAasListEntries();
}
+
+export async function getAasListEntities(targetRepository: string, limit: number, cursor?: string) {
+ const listService = ListService.create(targetRepository);
+ return listService.getAasListEntities(limit, cursor);
+}
diff --git a/src/lib/services/repository-access/RepositorySearchService.ts b/src/lib/services/repository-access/RepositorySearchService.ts
index c253671..13a692f 100644
--- a/src/lib/services/repository-access/RepositorySearchService.ts
+++ b/src/lib/services/repository-access/RepositorySearchService.ts
@@ -1,9 +1,7 @@
-import { IAssetAdministrationShellRepositoryApi, ISubmodelRepositoryApi } from 'lib/api/basyx-v3/apiInterface';
import { Log } from 'lib/util/Log';
import { AssetAdministrationShellRepositoryApi, SubmodelRepositoryApi } from 'lib/api/basyx-v3/api';
import { mnestixFetch } from 'lib/api/infrastructure';
import { AssetAdministrationShell, Submodel } from '@aas-core-works/aas-core3.0-typescript/dist/types/types';
-import { INullableAasRepositoryEntries } from 'lib/api/basyx-v3/apiInMemory';
import { PrismaConnector } from 'lib/services/database/PrismaConnector';
import { IPrismaConnector } from 'lib/services/database/PrismaConnectorInterface';
import { Reference } from '@aas-core-works/aas-core3.0-typescript/types';
@@ -13,173 +11,325 @@ import {
wrapErrorCode,
wrapSuccess,
} from 'lib/util/apiResponseWrapper/apiResponseWrapper';
+import { IAssetAdministrationShellRepositoryApi, ISubmodelRepositoryApi } from 'lib/api/basyx-v3/apiInterface';
-export type RepoSearchResult = {
- aas: AssetAdministrationShell;
+export type RepoSearchResult = {
+ searchResult: T;
location: string;
};
-export interface NullableMultipleDataSourceSetupParameters {
- shellsSavedInTheRepositories?: INullableAasRepositoryEntries[] | null;
- submodelsSavedInTheRepository?: Submodel[] | null;
- log?: Log | null;
-}
+const noDefaultAasRepository = () =>
+ wrapErrorCode(ApiResultStatus.INTERNAL_SERVER_ERROR, 'No default AAS repository configured');
+const noDefaultSubmodelRepository = () =>
+ wrapErrorCode(ApiResultStatus.INTERNAL_SERVER_ERROR, 'No default Submodel repository configured');
export class RepositorySearchService {
private constructor(
- protected readonly repositoryClient: IAssetAdministrationShellRepositoryApi,
- protected readonly submodelRepositoryClient: ISubmodelRepositoryApi,
protected readonly prismaConnector: IPrismaConnector,
+ protected readonly getAasRepositoryClient: (basePath: string) => IAssetAdministrationShellRepositoryApi,
+ protected readonly getSubmodelRepositoryClient: (basePath: string) => ISubmodelRepositoryApi,
protected readonly log: Log,
) {}
- static create(baseRepositoryUrl?: string | null): RepositorySearchService {
- const repositoryClient = AssetAdministrationShellRepositoryApi.create(
- mnestixFetch(),
- undefined,
- baseRepositoryUrl ?? process.env.AAS_REPO_API_URL,
- );
- const submodelRepositoryClient = SubmodelRepositoryApi.create(
- mnestixFetch(),
- undefined,
- baseRepositoryUrl ?? process.env.SUBMODEL_REPO_API_URL ?? process.env.AAS_REPO_API_URL,
- );
+ static create(): RepositorySearchService {
const log = Log.create();
const prismaConnector = PrismaConnector.create();
- return new RepositorySearchService(repositoryClient, submodelRepositoryClient, prismaConnector, log);
+ return new RepositorySearchService(
+ prismaConnector,
+ (baseUrl) => AssetAdministrationShellRepositoryApi.create(baseUrl, mnestixFetch()),
+ (baseUrl) => SubmodelRepositoryApi.create(baseUrl, mnestixFetch()),
+ log,
+ );
}
- static createNull({
- shellsSavedInTheRepositories = [],
- submodelsSavedInTheRepository = [],
+ static createNull(
+ shellsInRepositories: RepoSearchResult[] = [],
+ submodelsInRepositories: RepoSearchResult[] = [],
log = null,
- }: NullableMultipleDataSourceSetupParameters = {}): RepositorySearchService {
- const aasUrls = [...new Set(shellsSavedInTheRepositories?.map((entry) => entry.repositoryUrl))];
+ ): RepositorySearchService {
+ const shellUrls = new Set(shellsInRepositories.map((value) => value.location));
+ const submodelUrls = new Set(submodelsInRepositories.map((value) => value.location));
return new RepositorySearchService(
- AssetAdministrationShellRepositoryApi.createNull({ shellsSavedInTheRepositories }),
- SubmodelRepositoryApi.createNull({ submodelsSavedInTheRepository }),
- PrismaConnector.createNull({ aasUrls }),
+ PrismaConnector.createNull([...shellUrls], [...submodelUrls]),
+ (baseUrl) =>
+ AssetAdministrationShellRepositoryApi.createNull(
+ baseUrl,
+ shellsInRepositories
+ .filter((value) => value.location == baseUrl)
+ .map((value) => value.searchResult),
+ ),
+ (baseUrl) =>
+ SubmodelRepositoryApi.createNull(
+ baseUrl,
+ submodelsInRepositories
+ .filter((value) => value.location == baseUrl)
+ .map((value) => value.searchResult),
+ ),
log ?? Log.createNull(),
);
}
- async getAasFromDefaultRepository(aasId: string): Promise> {
- const response = await this.repositoryClient.getAssetAdministrationShellById(aasId);
- if (response.isSuccess) return response;
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'AAS not found');
+ async getAasRepositories() {
+ return this.prismaConnector.getConnectionDataByTypeAction({
+ id: '0',
+ typeName: 'AAS_REPOSITORY',
+ });
}
- async getAasFromRepo(aasId: string, repoUrl: string): Promise> {
- const response = await this.repositoryClient.getAssetAdministrationShellById(aasId, undefined, repoUrl);
+ async getSubmodelRepositories() {
+ return this.prismaConnector.getConnectionDataByTypeAction({
+ id: '2',
+ typeName: 'SUBMODEL_REPOSITORY',
+ });
+ }
+
+ async getAasFromDefaultRepo(aasId: string): Promise> {
+ const client = this.getDefaultAasRepositoryClient();
+ if (!client) return noDefaultAasRepository();
+ const response = await client.getAssetAdministrationShellById(aasId);
if (response.isSuccess) return response;
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, `AAS '${aasId}' not found in repository '${repoUrl}'`);
+ return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'AAS not found');
}
- async getAasFromAllRepos(aasId: string): Promise> {
- const basePathUrls = await this.prismaConnector.getConnectionDataByTypeAction({
- id: '0',
- typeName: 'AAS_REPOSITORY',
- });
+ async getAasFromAllRepos(aasId: string): Promise[]>> {
+ return this.getFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getAasFromRepo(aasId, basePath),
+ `Could not find AAS ${aasId} in any Repository`,
+ );
+ }
- const promises = basePathUrls.map(
- (url) =>
- this.getAasFromRepo(aasId, url).then((response: ApiResponseWrapper) => {
- return { wrapper: response, location: url };
- }), // add the URL to the resolved value
+ async getFirstAasFromAllRepos(
+ aasId: string,
+ ): Promise>> {
+ return this.getFirstFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getAasFromRepo(aasId, basePath),
+ `Could not find AAS ${aasId} in any Repository`,
);
+ }
- const responses = await Promise.allSettled(promises);
- const fulfilledResponses = responses.filter(
- (result) => result.status === 'fulfilled' && result.value.wrapper.isSuccess,
- );
-
- if (fulfilledResponses.length > 0) {
- return wrapSuccess(
- fulfilledResponses.map(
- (
- result: PromiseFulfilledResult<{
- wrapper: ApiResponseWrapper;
- location: string;
- }>,
- ) => ({ aas: result.value.wrapper.result!, location: result.value.location }),
- ),
- );
- } else {
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, `Could not find AAS ${aasId} in any Repository`);
- }
+ async getSubmodelFromAllRepos(submodelId: string): Promise[]>> {
+ return this.getFromAllRepos(
+ await this.getSubmodelRepositories(),
+ (basePath) => this.getSubmodelFromRepo(submodelId, basePath),
+ `Could not find Submodel '${submodelId}' in any Repository`,
+ );
}
- async getSubmodelById(id: string): Promise> {
- const result = await this.submodelRepositoryClient.getSubmodelById(id);
+ async getSubmodelFromDefaultRepo(id: string): Promise> {
+ const client = this.getDefaultSubmodelRepositoryClient();
+ if (!client) return noDefaultSubmodelRepository();
+ const result = await client.getSubmodelById(id);
if (result.isSuccess) return result;
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, `Submodel with id ${id} not found`);
+ return wrapErrorCode(ApiResultStatus.NOT_FOUND, `Submodel with id '${id}' not found`);
}
- async getAttachmentFromSubmodelElement(
+ private async getSubmodelFromRepo(submodelId: string, repoUrl: string): Promise> {
+ const client = this.getSubmodelRepositoryClient(repoUrl);
+ const response = await client.getSubmodelById(submodelId);
+ if (response.isSuccess) return response;
+ return Promise.reject(`Unable to fetch Submodel '${submodelId}' from '${repoUrl}'`);
+ }
+
+ async getFirstSubmodelFromAllRepos(submodelId: string): Promise>> {
+ return this.getFirstFromAllRepos(
+ await this.getSubmodelRepositories(),
+ (basePath) => this.getSubmodelFromRepo(submodelId, basePath),
+ `Could not find Submodel '${submodelId}' in any Repository`,
+ );
+ }
+
+ async getAttachmentFromSubmodelElementFromAllRepos(
submodelId: string,
submodelElementPath: string,
- ): Promise> {
- const response = await this.submodelRepositoryClient.getAttachmentFromSubmodelElement(
- submodelId,
- submodelElementPath,
+ ): Promise[]>> {
+ return this.getFromAllRepos(
+ await this.getSubmodelRepositories(),
+ (basePath) => this.getAttachmentFromSubmodelElementFromRepo(submodelId, submodelElementPath, basePath),
+ `Attachment for Submodel with id ${submodelId} at path ${submodelElementPath} not found in any repository`,
);
+ }
+
+ async getAttachmentFromSubmodelElementFromDefaultRepo(
+ submodelId: string,
+ submodelElementPath: string,
+ ): Promise> {
+ const client = this.getDefaultSubmodelRepositoryClient();
+ if (!client) return noDefaultSubmodelRepository();
+ const response = await client.getAttachmentFromSubmodelElement(submodelId, submodelElementPath);
if (response.isSuccess) return response;
return wrapErrorCode(
ApiResultStatus.NOT_FOUND,
- `Attachment for Submodel with id ${submodelId} at path ${submodelElementPath} not found`,
+ `Attachment for Submodel with id '${submodelId}' at path '${submodelElementPath}' not found in repository '${client.getBaseUrl()}'`,
);
}
- async getSubmodelFromAllRepos(submodelId: string): Promise> {
- const basePathUrls = await this.prismaConnector.getConnectionDataByTypeAction({
- id: '2',
- typeName: 'SUBMODEL_REPOSITORY',
- });
- const promises = basePathUrls.map(async (url) => {
- const response = await this.submodelRepositoryClient.getSubmodelById(submodelId, undefined, url);
- if (response.isSuccess) return response;
- return Promise.reject();
- });
+ private async getAttachmentFromSubmodelElementFromRepo(
+ submodelId: string,
+ submodelElementPath: string,
+ repoUrl: string,
+ ) {
+ const client = this.getSubmodelRepositoryClient(repoUrl);
+ const response = await client.getAttachmentFromSubmodelElement(submodelId, submodelElementPath);
+ if (response.isSuccess) return response;
+ return Promise.reject(
+ `Unable to fetch Attachment '${submodelElementPath}' in submodel '${submodelId}' from '${repoUrl}'`,
+ );
+ }
- try {
- return await Promise.any(promises);
- } catch (e) {
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'Submodel not found');
- }
+ async getFirstAttachmentFromSubmodelElementFromAllRepos(
+ submodelId: string,
+ submodelElementPath: string,
+ ): Promise>> {
+ return this.getFirstFromAllRepos(
+ await this.getSubmodelRepositories(),
+ (basePath) => this.getAttachmentFromSubmodelElementFromRepo(submodelId, submodelElementPath, basePath),
+ `Attachment for Submodel with id ${submodelId} at path ${submodelElementPath} not found in any repository`,
+ );
}
- async getSubmodelReferencesFromShell(aasId: string): Promise> {
- const response = await this.repositoryClient.getSubmodelReferencesFromShell(aasId);
+ async getSubmodelReferencesFromShellFromAllRepos(aasId: string) {
+ return this.getFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getSubmodelReferencesFromShellFromRepo(basePath, aasId),
+ `Submodel references for '${aasId}' not found in any repository`,
+ );
+ }
+
+ async getSubmodelReferencesFromShellFromDefaultRepo(aasId: string): Promise> {
+ const client = this.getDefaultAasRepositoryClient();
+ if (!client) return noDefaultAasRepository();
+ const response = await client.getSubmodelReferencesFromShell(aasId);
if (response.isSuccess) return response;
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'Submodel Reference not found');
+ return wrapErrorCode(
+ ApiResultStatus.NOT_FOUND,
+ `Submodel references for '${aasId}' not found in default repository`,
+ );
}
- async getThumbnailFromShell(aasId: string): Promise> {
- const response = await this.repositoryClient.getThumbnailFromShell(aasId);
+ private async getSubmodelReferencesFromShellFromRepo(aasId: string, repoUrl: string) {
+ const client = this.getAasRepositoryClient(repoUrl);
+ const response = await client.getSubmodelReferencesFromShell(aasId);
if (response.isSuccess) return response;
+ return Promise.reject(`Unable to fetch submodel references for AAS '${aasId}' from '${repoUrl}'`);
+ }
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'Thumbnail not found');
+ async getFirstSubmodelReferencesFromShellFromAllRepos(aasId: string) {
+ return this.getFirstFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getSubmodelReferencesFromShellFromRepo(basePath, aasId),
+ `Submodel references for '${aasId}' not found in any repository`,
+ );
}
- async getAasThumbnailFromAllRepos(aasId: string): Promise> {
- const basePathUrls = await this.prismaConnector.getConnectionDataByTypeAction({
- id: '0',
- typeName: 'AAS_REPOSITORY',
- });
+ async getAasThumbnailFromAllRepos(aasId: string): Promise[]>> {
+ return this.getFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getAasThumbnailFromRepo(aasId, basePath),
+ `Thumbnail for '${aasId}' not found in any repository`,
+ );
+ }
- const promises = basePathUrls.map((url) => {
- return this.repositoryClient.getThumbnailFromShell(aasId, undefined, url).then((response) => {
- if (response.isSuccess && response.result instanceof Blob && response.result.size === 0) {
- return Promise.reject('Empty image');
- }
- return response;
- });
- });
+ async getAasThumbnailFromDefaultRepo(aasId: string): Promise> {
+ const client = this.getDefaultAasRepositoryClient();
+ if (!client) return noDefaultAasRepository();
+ const response = await client.getThumbnailFromShell(aasId);
+ if (response.isSuccess) return response;
+ return wrapErrorCode(ApiResultStatus.NOT_FOUND, `Thumbnail for '${aasId}' not found in default repository`);
+ }
+
+ private async getAasThumbnailFromRepo(aasId: string, repoUrl: string): Promise> {
+ const client = this.getAasRepositoryClient(repoUrl);
+ const response = await client.getThumbnailFromShell(aasId);
+ if (!response.isSuccess) {
+ return Promise.reject(`Unable to fetch thumbnail for AAS '${aasId}' from '${repoUrl}'`);
+ }
+ if (response.result instanceof Blob && response.result.size === 0) {
+ return Promise.reject(`Empty thumbnail image in AAS '${aasId}' from '${repoUrl}'`);
+ }
+ return response;
+ }
+
+ async getFirstAasThumbnailFromAllRepos(aasId: string): Promise>> {
+ return this.getFirstFromAllRepos(
+ await this.getAasRepositories(),
+ (basePath) => this.getAasThumbnailFromRepo(aasId, basePath),
+ `Thumbnail for '${aasId}' not found in any repository`,
+ );
+ }
+
+ async getFirstFromAllRepos(
+ basePathUrls: string[],
+ kernel: (url: string) => Promise>,
+ errorMsg: string,
+ ): Promise>> {
+ const promises = basePathUrls.map(async (url) =>
+ kernel(url).then((response: ApiResponseWrapper) => {
+ if (!response.isSuccess) return Promise.reject('Fetch call was not successful');
+ return { searchResult: response.result, location: url };
+ }),
+ );
try {
- return await Promise.any(promises);
+ const firstResult = await Promise.any(promises);
+ return wrapSuccess(firstResult);
} catch {
- return wrapErrorCode(ApiResultStatus.NOT_FOUND, 'Image not found');
+ return wrapErrorCode(ApiResultStatus.NOT_FOUND, errorMsg);
}
}
+
+ private getDefaultAasRepositoryClient() {
+ const defaultUrl = process.env.AAS_REPO_API_URL;
+ if (!defaultUrl) return null;
+ return this.getAasRepositoryClient(defaultUrl);
+ }
+
+ private getDefaultSubmodelRepositoryClient() {
+ const defaultUrl = process.env.SUBMODEL_REPO_API_URL ?? process.env.AAS_REPO_API_URL;
+ if (!defaultUrl) return null;
+ return this.getSubmodelRepositoryClient(defaultUrl);
+ }
+
+ async getFromAllRepos(
+ basePathUrls: string[],
+ kernel: (url: string) => Promise>,
+ errorMsg: string,
+ ): Promise[]>> {
+ const promises = basePathUrls.map(async (url) =>
+ kernel(url).then((response: ApiResponseWrapper) => {
+ return { searchResult: response, location: url };
+ }),
+ );
+
+ const responses = await Promise.allSettled(promises);
+ const fulfilledResponses = responses.filter(
+ (result) => result.status === 'fulfilled' && result.value.searchResult.isSuccess,
+ );
+
+ if (fulfilledResponses.length <= 0) {
+ return wrapErrorCode(ApiResultStatus.NOT_FOUND, errorMsg);
+ }
+
+ return wrapSuccess[]>(
+ fulfilledResponses.map(
+ (
+ result: PromiseFulfilledResult<{
+ searchResult: ApiResponseWrapper;
+ location: string;
+ }>,
+ ) => ({ searchResult: result.value.searchResult.result!, location: result.value.location }),
+ ),
+ );
+ }
+
+ private async getAasFromRepo(
+ aasId: string,
+ repoUrl: string,
+ ): Promise