diff --git a/aas-web-ui/public/config/basyx-infra.yml b/aas-web-ui/public/config/basyx-infra.yml index f3aff823e..4dc2c9314 100644 --- a/aas-web-ui/public/config/basyx-infra.yml +++ b/aas-web-ui/public/config/basyx-infra.yml @@ -8,6 +8,9 @@ # The configuration will be loaded at application startup and merged with user settings. # When VITE_ENDPOINT_CONFIG_AVAILABLE=false, this configuration takes full precedence. # When VITE_ENDPOINT_CONFIG_AVAILABLE=true, users can edit these infrastructures. +# Optional component flags: +# - hasRegistryIntegration: Backend repository creates/updates/deletes descriptors automatically. +# - hasDiscoveryIntegration: Backend AAS registry creates/updates/deletes discovery asset links automatically. infrastructures: # Specify which infrastructure should be selected by default @@ -21,12 +24,15 @@ infrastructures: baseUrl: "http://localhost:9084" aasRegistry: baseUrl: "http://localhost:9082" + hasDiscoveryIntegration: true submodelRegistry: baseUrl: "http://localhost:9083" aasRepository: baseUrl: "http://localhost:9081" + hasRegistryIntegration: true submodelRepository: baseUrl: "http://localhost:9081" + hasRegistryIntegration: true conceptDescriptionRepository: baseUrl: "http://localhost:9081" security: diff --git a/aas-web-ui/src/components/AppNavigation/DeleteAAS.vue b/aas-web-ui/src/components/AppNavigation/DeleteAAS.vue index 696aad843..c5bcacea2 100644 --- a/aas-web-ui/src/components/AppNavigation/DeleteAAS.vue +++ b/aas-web-ui/src/components/AppNavigation/DeleteAAS.vue @@ -37,11 +37,15 @@ diff --git a/aas-web-ui/src/components/AppNavigation/Settings/InfrastructureManagement.vue b/aas-web-ui/src/components/AppNavigation/Settings/InfrastructureManagement.vue index 63e313565..f0c141ab1 100644 --- a/aas-web-ui/src/components/AppNavigation/Settings/InfrastructureManagement.vue +++ b/aas-web-ui/src/components/AppNavigation/Settings/InfrastructureManagement.vue @@ -60,7 +60,9 @@ :component-testing-loading="componentTestingLoading" @test-connection="testComponentConnection" @update:component-url="handleComponentUrlUpdate" - @update:connection-status="handleConnectionStatusUpdate" /> + @update:connection-status="handleConnectionStatusUpdate" + @update:registry-integration="handleRegistryIntegrationUpdate" + @update:discovery-integration="handleDiscoveryIntegrationUpdate" /> Security Configuration @@ -368,4 +370,12 @@ function handleConnectionStatusUpdate(componentKey: BaSyxComponentKey, status: boolean | null): void { componentConnectionStatus.value[componentKey] = status; } + + function handleRegistryIntegrationUpdate(componentKey: BaSyxComponentKey, enabled: boolean): void { + editingInfrastructure.value.components[componentKey].hasRegistryIntegration = enabled; + } + + function handleDiscoveryIntegrationUpdate(componentKey: BaSyxComponentKey, enabled: boolean): void { + editingInfrastructure.value.components[componentKey].hasDiscoveryIntegration = enabled; + } diff --git a/aas-web-ui/src/components/AppNavigation/UploadAAS.vue b/aas-web-ui/src/components/AppNavigation/UploadAAS.vue index cdbbbb427..78ffd010f 100644 --- a/aas-web-ui/src/components/AppNavigation/UploadAAS.vue +++ b/aas-web-ui/src/components/AppNavigation/UploadAAS.vue @@ -27,13 +27,20 @@ label="Ignore Duplicates" hide-details class="mt-3"> - - Descriptor creation is unavailable because AAS and Submodel registries are not connected. + Manual descriptor sync is required by infrastructure settings, but AAS and Submodel registries are + not connected. + + + Manual discovery sync is required by infrastructure settings, but AAS discovery is not connected. infrastructureStore.getAASRepoURL); - const smRepositoryUrl = computed(() => infrastructureStore.getSubmodelRepoURL); - // Composables - const { fetchAas, uploadAas } = useAASRepositoryClient(); + const { fetchAas, uploadAas, getAasEndpointById } = useAASRepositoryClient(); + const { getSmEndpointById } = useSMRepositoryClient(); const { fetchSm } = useSMHandling(); const { importAasxFileClient, importEnvironmentFileClient } = useAASXImport(); + const { createAssetLinksFromAssetInformation, upsertAssetLinksForAas } = useAASDiscoveryClient(); const { postAasDescriptor, putAasDescriptor, createDescriptorFromAAS } = useAASRegistryClient(); const { postSubmodelDescriptor, putSubmodelDescriptor, createDescriptorFromSubmodel } = useSMRegistryClient(); @@ -109,7 +115,6 @@ const aasFiles = ref([]); const loadingUpload = ref(false); const ignoreDuplicates = ref(true); - const createDescriptors = ref(false); const uploadProgress = ref(0); const currentFileLabel = ref(''); const uploadMode = ref<'client' | 'server'>('client'); @@ -133,6 +138,30 @@ const submodelRegistryConnected = isConnected(components?.SubmodelRegistry?.connected); return aasRegistryConnected && submodelRegistryConnected; }); + const discoveryAvailable = computed(() => { + const components = infrastructureStore.getBasyxComponents as Record; + const connected = components?.AASDiscovery?.connected; + + if (connected === true) return true; + if (!connected || typeof connected !== 'object') return false; + const maybeRef = connected as { value?: unknown }; + return maybeRef.value === true; + }); + + const selectedInfrastructure = computed(() => infrastructureStore.getSelectedInfrastructure); + const aasRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.AASRepo?.hasRegistryIntegration ?? true + ); + const submodelRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.SubmodelRepo?.hasRegistryIntegration ?? true + ); + const aasRegistryHasDiscoveryIntegration = computed( + () => selectedInfrastructure.value?.components?.AASRegistry?.hasDiscoveryIntegration ?? true + ); + const manualDescriptorSyncRequired = computed( + () => !aasRepoHasRegistryIntegration.value || !submodelRepoHasRegistryIntegration.value + ); + const manualDiscoverySyncRequired = computed(() => !aasRegistryHasDiscoveryIntegration.value); watch( () => props.modelValue, @@ -176,9 +205,17 @@ } try { - if (createDescriptors.value && !descriptorsAvailable.value) { - summary.warnings.push('Descriptor creation skipped because registries are not connected.'); + if (manualDescriptorSyncRequired.value && !descriptorsAvailable.value) { + summary.warnings.push('Manual descriptor sync skipped because registries are not connected.'); } + if (manualDiscoverySyncRequired.value && !discoveryAvailable.value) { + summary.warnings.push('Manual discovery sync skipped because AAS discovery is not connected.'); + } + + const shouldSyncAasDescriptor = !aasRepoHasRegistryIntegration.value && descriptorsAvailable.value; + const shouldSyncSubmodelDescriptor = + !submodelRepoHasRegistryIntegration.value && descriptorsAvailable.value; + const shouldSyncDiscovery = !aasRegistryHasDiscoveryIntegration.value && discoveryAvailable.value; for (let index = 0; index < aasFiles.value.length; index++) { const aasFile = aasFiles.value[index]; @@ -191,9 +228,14 @@ summary.failed.push(`${aasFile.name}: upload failed.`); } else { summary.succeeded++; - if (createDescriptors.value && descriptorsAvailable.value) { + if (shouldSyncAasDescriptor || shouldSyncSubmodelDescriptor || shouldSyncDiscovery) { const createdWarnings = await createAndPostDescriptorsFromAasIds( - Array.isArray(response?.data?.aasIds) ? response.data.aasIds : [] + Array.isArray(response?.data?.aasIds) ? response.data.aasIds : [], + { + syncAasDescriptor: shouldSyncAasDescriptor, + syncSubmodelDescriptor: shouldSyncSubmodelDescriptor, + syncDiscovery: shouldSyncDiscovery, + } ); summary.warnings.push(...createdWarnings.map((warning) => `${aasFile.name}: ${warning}`)); } @@ -221,10 +263,15 @@ ...result.warnings.map((warning: string) => `${aasFile.name}: ${warning}`) ); - if (createDescriptors.value && descriptorsAvailable.value) { + if (shouldSyncAasDescriptor || shouldSyncSubmodelDescriptor || shouldSyncDiscovery) { const descriptorWarnings = await createAndPostDescriptorsFromPayload( result.importedAas, - result.importedSubmodels + result.importedSubmodels, + { + syncAasDescriptor: shouldSyncAasDescriptor, + syncSubmodelDescriptor: shouldSyncSubmodelDescriptor, + syncDiscovery: shouldSyncDiscovery, + } ); summary.warnings.push( ...descriptorWarnings.map((warning: string) => `${aasFile.name}: ${warning}`) @@ -290,7 +337,6 @@ aasFiles.value = []; uploadAASDialog.value = false; loadingUpload.value = false; - createDescriptors.value = false; uploadProgress.value = 0; currentFileLabel.value = ''; uploadMode.value = 'client'; @@ -315,12 +361,15 @@ return typeof value === 'string' ? value : ''; } - async function createAndPostDescriptorsFromAasIds(aasIds: string[]): Promise { + async function createAndPostDescriptorsFromAasIds( + aasIds: string[], + sync: { syncAasDescriptor: boolean; syncSubmodelDescriptor: boolean; syncDiscovery: boolean } + ): Promise { const warnings: string[] = []; for (const aasId of aasIds) { try { - const href = aasRepositoryUrl.value + '/' + base64Encode(aasId); + const href = getAasEndpointById(aasId); const fetchedShell = await fetchAas(href); if (!fetchedShell || !fetchedShell.id) { @@ -328,26 +377,43 @@ continue; } - const aasEndpoints = createEndpoints(href, 'AAS-3.0'); - const aasDescriptor = createDescriptorFromAAS(fetchedShell, aasEndpoints); - - const submodelRefs = Array.isArray(fetchedShell.submodels) ? fetchedShell.submodels : []; - for (const submodelRef of submodelRefs) { - const submodelId = submodelRef?.keys?.[0]?.value; - if (!submodelId) continue; + if (sync.syncSubmodelDescriptor) { + const submodelRefs = Array.isArray(fetchedShell.submodels) ? fetchedShell.submodels : []; + for (const submodelRef of submodelRefs) { + const submodelId = submodelRef?.keys?.[0]?.value; + if (!submodelId) continue; + + const submodelDescriptor = await createSubmodelDescriptor(submodelId); + const smSuccess = + (await postSubmodelDescriptor(submodelDescriptor)) || + (await putSubmodelDescriptor(submodelDescriptor)); + if (!smSuccess) { + warnings.push(`Failed to create Submodel Descriptor '${submodelId}'.`); + } + } + } - const submodelDescriptor = await createSubmodelDescriptor(submodelId); - const smSuccess = - (await postSubmodelDescriptor(submodelDescriptor)) || - (await putSubmodelDescriptor(submodelDescriptor)); - if (!smSuccess) { - warnings.push(`Failed to create Submodel Descriptor '${submodelId}'.`); + if (sync.syncAasDescriptor) { + const aasEndpoints = createEndpoints(href, 'AAS-3.0'); + const aasDescriptor = createDescriptorFromAAS(fetchedShell, aasEndpoints); + const aasSuccess = + (await postAasDescriptor(aasDescriptor)) || (await putAasDescriptor(aasDescriptor)); + if (!aasSuccess) { + warnings.push(`Failed to create AAS Descriptor '${aasId}'.`); } } - const aasSuccess = (await postAasDescriptor(aasDescriptor)) || (await putAasDescriptor(aasDescriptor)); - if (!aasSuccess) { - warnings.push(`Failed to create AAS Descriptor '${aasId}'.`); + if (sync.syncDiscovery) { + const links = createAssetLinksFromAssetInformation( + fetchedShell?.assetInformation?.globalAssetId, + fetchedShell?.assetInformation?.specificAssetIds + ); + if (links.length > 0) { + const discoverySuccess = await upsertAssetLinksForAas(aasId, links); + if (!discoverySuccess) { + warnings.push(`Failed to synchronize discovery asset links for '${aasId}'.`); + } + } } } catch (error) { warnings.push(`Error while creating descriptors for AAS '${aasId}': ${stringifyUnknown(error)}`); @@ -359,7 +425,8 @@ async function createAndPostDescriptorsFromPayload( aasList: Array>, - submodels: Array> + submodels: Array>, + sync: { syncAasDescriptor: boolean; syncSubmodelDescriptor: boolean; syncDiscovery: boolean } ): Promise { const warnings: string[] = []; const submodelById = new Map>(); @@ -374,39 +441,60 @@ const aasId = asString(aas.id).trim(); if (aasId === '') continue; - const aasHref = aasRepositoryUrl.value + '/' + base64Encode(aasId); - const aasDescriptor = createDescriptorFromAAS( - aas as unknown as jsonization.JsonObject, - createEndpoints(aasHref, 'AAS-3.0') - ); - - const submodelRefs = Array.isArray(aas.submodels) ? aas.submodels : []; - for (const submodelRef of submodelRefs) { - const submodelId = submodelRef?.keys?.[0]?.value; - if (!submodelId) continue; - - const submodel = submodelById.get(submodelId); - if (!submodel) { - warnings.push(`Submodel '${submodelId}' not found for descriptor creation.`); - continue; + const aasHref = getAasEndpointById(aasId); + + if (sync.syncSubmodelDescriptor) { + const submodelRefs = Array.isArray(aas.submodels) ? aas.submodels : []; + for (const submodelRef of submodelRefs) { + const submodelId = submodelRef?.keys?.[0]?.value; + if (!submodelId) continue; + + const submodel = submodelById.get(submodelId); + if (!submodel) { + warnings.push(`Submodel '${submodelId}' not found for descriptor creation.`); + continue; + } + + const submodelHref = getSmEndpointById(submodelId); + const submodelDescriptor = createDescriptorFromSubmodel( + submodel as unknown as jsonization.JsonObject, + createEndpoints(submodelHref, 'SUBMODEL-3.0') + ); + const smSuccess = + (await postSubmodelDescriptor(submodelDescriptor)) || + (await putSubmodelDescriptor(submodelDescriptor)); + if (!smSuccess) { + warnings.push(`Failed to create Submodel Descriptor '${submodelId}'.`); + } } + } - const submodelHref = smRepositoryUrl.value + '/' + base64Encode(submodelId); - const submodelDescriptor = createDescriptorFromSubmodel( - submodel as unknown as jsonization.JsonObject, - createEndpoints(submodelHref, 'SUBMODEL-3.0') + if (sync.syncAasDescriptor) { + const aasDescriptor = createDescriptorFromAAS( + aas as unknown as jsonization.JsonObject, + createEndpoints(aasHref, 'AAS-3.0') ); - const smSuccess = - (await postSubmodelDescriptor(submodelDescriptor)) || - (await putSubmodelDescriptor(submodelDescriptor)); - if (!smSuccess) { - warnings.push(`Failed to create Submodel Descriptor '${submodelId}'.`); + const aasSuccess = + (await postAasDescriptor(aasDescriptor)) || (await putAasDescriptor(aasDescriptor)); + if (!aasSuccess) { + warnings.push(`Failed to create AAS Descriptor '${aasId}'.`); } } - const aasSuccess = (await postAasDescriptor(aasDescriptor)) || (await putAasDescriptor(aasDescriptor)); - if (!aasSuccess) { - warnings.push(`Failed to create AAS Descriptor '${aasId}'.`); + if (sync.syncDiscovery) { + const assetInformation = aas.assetInformation as + | { globalAssetId?: string; specificAssetIds?: Array<{ name?: string; value?: string }> } + | undefined; + const links = createAssetLinksFromAssetInformation( + assetInformation?.globalAssetId, + assetInformation?.specificAssetIds + ); + if (links.length > 0) { + const discoverySuccess = await upsertAssetLinksForAas(aasId, links); + if (!discoverySuccess) { + warnings.push(`Failed to synchronize discovery asset links for '${aasId}'.`); + } + } } } catch (error) { warnings.push(`Error while creating descriptors from imported payload: ${stringifyUnknown(error)}`); @@ -418,9 +506,8 @@ async function createSubmodelDescriptor(submodelId: string): Promise { try { - let submodelId64 = base64Encode(submodelId); - let href = smRepositoryUrl.value + '/' + submodelId64; - let submodel = await fetchSm(href); + const href = getSmEndpointById(submodelId); + const submodel = await fetchSm(href); const endpoints = createEndpoints(href, 'SUBMODEL-3.0'); diff --git a/aas-web-ui/src/components/EditorComponents/AASForm.vue b/aas-web-ui/src/components/EditorComponents/AASForm.vue index 949fd0dd8..599ba0f57 100644 --- a/aas-web-ui/src/components/EditorComponents/AASForm.vue +++ b/aas-web-ui/src/components/EditorComponents/AASForm.vue @@ -191,12 +191,15 @@ import { computed, ref, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useAASHandling } from '@/composables/AAS/AASHandling'; + import { useAASDiscoveryClient } from '@/composables/Client/AASDiscoveryClient'; import { useAASRegistryClient } from '@/composables/Client/AASRegistryClient'; import { useAASRepositoryClient } from '@/composables/Client/AASRepositoryClient'; import { useIDUtils } from '@/composables/IDUtils'; import { buildVerificationSummary, verifyForEditor } from '@/composables/MetamodelVerification'; import { useAASStore } from '@/store/AASDataStore'; + import { useInfrastructureStore } from '@/store/InfrastructureStore'; import { useNavigationStore } from '@/store/NavigationStore'; + import { Endpoint, ProtocolInformation } from '@/types/Descriptors'; const props = defineProps<{ modelValue: boolean; @@ -214,14 +217,24 @@ // Stores const aasStore = useAASStore(); + const infrastructureStore = useInfrastructureStore(); const navigationStore = useNavigationStore(); const emit = defineEmits<{ (event: 'update:modelValue', value: boolean): void; }>(); - const { fetchAasById, postAas, putAas, putThumbnail } = useAASRepositoryClient(); - const { fetchAasDescriptorById, putAasDescriptor, createDescriptorFromAAS } = useAASRegistryClient(); + const { + fetchAasById, + postAas, + putAas, + putThumbnail, + getAasEndpointById: getAasRepoEndpointById, + } = useAASRepositoryClient(); + const { fetchAasDescriptorById, postAasDescriptor, putAasDescriptor, createDescriptorFromAAS } = + useAASRegistryClient(); + const { createAssetLinksFromAssetInformation, upsertAssetLinksForAas, deleteAssetLinksForAas } = + useAASDiscoveryClient(); const editAASDialog = ref(false); const AASObject = ref(undefined); @@ -247,9 +260,17 @@ const embeddedDataSpecifications = ref | null>(null); const fileThumbnail = ref(undefined); + const initialDiscoveryAssetLinks = ref>([]); // Computed Properties const selectedAAS = computed(() => aasStore.getSelectedAAS); // Get the selected AAS from Store + const selectedInfrastructure = computed(() => infrastructureStore.getSelectedInfrastructure); + const aasRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.AASRepo?.hasRegistryIntegration ?? true + ); + const aasRegistryHasDiscoveryIntegration = computed( + () => selectedInfrastructure.value?.components?.AASRegistry?.hasDiscoveryIntegration ?? true + ); const bordersToShow = computed(() => (panel: number) => { let border = ''; switch (panel) { @@ -344,6 +365,12 @@ defaultThumbnail.value = AASObject.value.assetInformation.defaultThumbnail ?? null; } embeddedDataSpecifications.value = AASObject.value.embeddedDataSpecifications ?? null; + + // Keep a stable snapshot from initial load so update detection is not affected by in-place form mutations. + initialDiscoveryAssetLinks.value = createAssetLinksFromAssetInformation( + AASObject.value.assetInformation?.globalAssetId, + AASObject.value.assetInformation?.specificAssetIds + ); } } @@ -401,6 +428,8 @@ async function saveAAS(): Promise { if (AASId.value === null) return; + const previousAssetLinks = props.newShell ? [] : [...initialDiscoveryAssetLinks.value]; + const assetInformation = createAssetInformation(); const administrativeInformation = createAdministrativeInformation(); @@ -463,6 +492,7 @@ if (props.newShell) { // Create new AAS await postAas(AASObject.value); + await syncAasDescriptorAndDiscovery(AASObject.value, true, previousAssetLinks); // Upload default thumbnail if (fileThumbnail.value !== undefined) { await putThumbnail(fileThumbnail.value, AASObject.value.id); @@ -472,18 +502,12 @@ query.aas = await getAasEndpointById(AASObject.value.id); if (Object.hasOwn(query, 'path')) delete query.path; - router.push({ query: query }); + await router.push({ query: query }); navigationStore.dispatchTriggerAASListReload(); // Reload AAS List } else { // Update existing AAS await putAas(AASObject.value); - // Update AAS Descriptor - const jsonAAS = jsonization.toJsonable(AASObject.value); - // Fetch existing descriptor to preserve endpoints - const existingDescriptor = await fetchAasDescriptorById(AASObject.value.id); - const endpoints = existingDescriptor?.endpoints ?? []; - const descriptor = createDescriptorFromAAS(jsonAAS, endpoints); - await putAasDescriptor(descriptor); + await syncAasDescriptorAndDiscovery(AASObject.value, false, previousAssetLinks); // Upload default thumbnail if (fileThumbnail.value !== undefined) { await putThumbnail(fileThumbnail.value, AASObject.value.id); @@ -497,6 +521,115 @@ editAASDialog.value = false; } + async function syncAasDescriptorAndDiscovery( + aas: aasTypes.AssetAdministrationShell, + isCreate: boolean, + previousAssetLinks: Array<{ name: string; value: string }> + ): Promise { + const warnings: string[] = []; + + if (!aasRepoHasRegistryIntegration.value) { + try { + const jsonAAS = jsonization.toJsonable(aas); + const existingDescriptor = await fetchAasDescriptorById(aas.id); + const fallbackAasEndpoint = getAasRepoEndpointById(aas.id); + const endpoints = + Array.isArray(existingDescriptor?.endpoints) && existingDescriptor.endpoints.length > 0 + ? existingDescriptor.endpoints + : createEndpoints(fallbackAasEndpoint, 'AAS-3.0'); + const descriptor = createDescriptorFromAAS(jsonAAS, endpoints); + const success = isCreate + ? (await postAasDescriptor(descriptor)) || (await putAasDescriptor(descriptor)) + : (await putAasDescriptor(descriptor)) || (await postAasDescriptor(descriptor)); + + if (!success) { + warnings.push(`Failed to synchronize AAS descriptor for '${aas.id}'.`); + } + } catch (error) { + warnings.push( + `Failed to synchronize AAS descriptor for '${aas.id}': ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + if (!aasRegistryHasDiscoveryIntegration.value) { + try { + const assetLinks = createAssetLinksFromAssetInformation( + aas.assetInformation?.globalAssetId, + aas.assetInformation?.specificAssetIds + ); + + if (isCreate) { + if (assetLinks.length > 0) { + const success = await upsertAssetLinksForAas(aas.id, assetLinks); + if (!success) { + warnings.push(`Failed to synchronize discovery asset links for '${aas.id}'.`); + } + } + } else { + const linksChanged = !areAssetLinksEqual(previousAssetLinks, assetLinks); + + if (linksChanged) { + if (assetLinks.length === 0) { + const deleteSuccess = await deleteAssetLinksForAas(aas.id); + if (!deleteSuccess) { + warnings.push(`Failed to remove discovery asset links for '${aas.id}'.`); + } + } else { + const upsertSuccess = await upsertAssetLinksForAas(aas.id, assetLinks); + if (!upsertSuccess) { + warnings.push(`Failed to synchronize discovery asset links for '${aas.id}'.`); + } + } + } + } + } catch (error) { + warnings.push( + `Failed to synchronize discovery asset links for '${aas.id}': ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + if (warnings.length > 0) { + navigationStore.dispatchSnackbar({ + status: true, + timeout: 10000, + color: 'warning', + btnColor: 'buttonText', + baseError: 'AAS saved with synchronization warnings.', + extendedError: warnings.join('\n'), + }); + } + } + + function areAssetLinksEqual( + left: Array<{ name: string; value: string }>, + right: Array<{ name: string; value: string }> + ): boolean { + if (left.length !== right.length) return false; + + const normalize = (links: Array<{ name: string; value: string }>): Array => + links.map((link) => `${link.name}\u0000${link.value}`).sort((a, b) => a.localeCompare(b)); + + const leftNormalized = normalize(left); + const rightNormalized = normalize(right); + + return leftNormalized.every((entry, index) => entry === rightNormalized[index]); + } + + function createEndpoints(href: string, type: string): Array { + let protocol: string | null = null; + try { + const url = new URL(href); + protocol = url.protocol.replace(/:$/, ''); + } catch { + // If href is not a valid absolute URL, keep protocol null. + } + + const protocolInformation = new ProtocolInformation(href, null, protocol); + return [new Endpoint(type, protocolInformation)]; + } + function closeDialog(): void { clearForm(); editAASDialog.value = false; @@ -520,6 +653,7 @@ assetType.value = null; defaultThumbnail.value = null; embeddedDataSpecifications.value = null; + initialDiscoveryAssetLinks.value = []; // Reset state of expansion panels openPanels.value = [0, 3]; } diff --git a/aas-web-ui/src/components/EditorComponents/DeleteDialog.vue b/aas-web-ui/src/components/EditorComponents/DeleteDialog.vue index 3512231f6..44c7fed42 100644 --- a/aas-web-ui/src/components/EditorComponents/DeleteDialog.vue +++ b/aas-web-ui/src/components/EditorComponents/DeleteDialog.vue @@ -34,12 +34,16 @@ import { useRoute, useRouter } from 'vue-router'; import { useSMHandling } from '@/composables/AAS/SMHandling'; import { useAASRepositoryClient } from '@/composables/Client/AASRepositoryClient'; + import { useSMRegistryClient } from '@/composables/Client/SMRegistryClient'; + import { useSMRepositoryClient } from '@/composables/Client/SMRepositoryClient'; import { useRequestHandling } from '@/composables/RequestHandling'; import { useAASStore } from '@/store/AASDataStore'; + import { useInfrastructureStore } from '@/store/InfrastructureStore'; import { useNavigationStore } from '@/store/NavigationStore'; import { extractEndpointHref } from '@/utils/AAS/DescriptorUtils'; const aasStore = useAASStore(); + const infrastructureStore = useInfrastructureStore(); const navigationStore = useNavigationStore(); const router = useRouter(); @@ -47,7 +51,9 @@ const { deleteRequest } = useRequestHandling(); const { fetchSmDescriptor } = useSMHandling(); - const { deleteSubmodelRef } = useAASRepositoryClient(); + const { deleteSubmodelDescriptor } = useSMRegistryClient(); + const { deleteSubmodelRef, getAasEndpointById } = useAASRepositoryClient(); + const { getSmEndpointById } = useSMRepositoryClient(); const props = defineProps<{ modelValue: boolean; @@ -62,6 +68,10 @@ const deleteLoading = ref(false); // Variable to store if the AAS is being deleted const selectedAAS = computed(() => aasStore.getSelectedAAS); // get selected AAS from Store + const selectedInfrastructure = computed(() => infrastructureStore.getSelectedInfrastructure); + const submodelRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.SubmodelRepo?.hasRegistryIntegration ?? true + ); watch( () => props.modelValue, @@ -80,20 +90,54 @@ async function confirmDelete(): Promise { deleteLoading.value = true; if (props.element.modelType === 'Submodel') { - // Fetch the submodel from the submodel registry - const smDescriptor = await fetchSmDescriptor(props.element.id); - // extract the submodel endpoint - const smEndpoint = extractEndpointHref(smDescriptor, 'SUBMODEL-3.0'); + let smEndpoint = ''; + if (submodelRepoHasRegistryIntegration.value) { + const smDescriptor = await fetchSmDescriptor(props.element.id); + smEndpoint = extractEndpointHref(smDescriptor, 'SUBMODEL-3.0'); + if (!smEndpoint) { + smEndpoint = getSmEndpointById(props.element.id); + } + } else { + smEndpoint = getSmEndpointById(props.element.id); + } + + if (!smEndpoint) { + navigationStore.dispatchSnackbar({ + status: true, + timeout: 6000, + color: 'error', + btnColor: 'buttonText', + text: 'Unable to resolve Submodel endpoint for deletion.', + }); + deleteLoading.value = false; + return; + } + try { // delete the submodel await deleteRequest(smEndpoint, 'removing Submodel', false); // extract the AAS endpoint - const aasEndpoint = extractEndpointHref(selectedAAS.value, 'AAS-3.0'); + const aasEndpoint = + extractEndpointHref(selectedAAS.value, 'AAS-3.0') || getAasEndpointById(selectedAAS.value.id); // delete the submodel reference from the AAS await deleteSubmodelRef(aasEndpoint, props.element.id); + if (!submodelRepoHasRegistryIntegration.value) { + const descriptorDeleted = await deleteSubmodelDescriptor(props.element.id); + if (!descriptorDeleted) { + navigationStore.dispatchSnackbar({ + status: true, + timeout: 6000, + color: 'warning', + btnColor: 'buttonText', + baseError: 'Submodel deleted with synchronization warning.', + extendedError: `Failed to delete Submodel descriptor '${props.element.id}'.`, + }); + } + } + // delete the submodel reference from the local AAS const localAAS = { ...selectedAAS.value }; const submodelRefs = localAAS.submodels; diff --git a/aas-web-ui/src/components/EditorComponents/JsonInsert.vue b/aas-web-ui/src/components/EditorComponents/JsonInsert.vue index 9d4638d0b..9205cfa74 100644 --- a/aas-web-ui/src/components/EditorComponents/JsonInsert.vue +++ b/aas-web-ui/src/components/EditorComponents/JsonInsert.vue @@ -30,10 +30,12 @@ import { computed, ref, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useAASRepositoryClient } from '@/composables/Client/AASRepositoryClient'; + import { useSMRegistryClient } from '@/composables/Client/SMRegistryClient'; import { useSMRepositoryClient } from '@/composables/Client/SMRepositoryClient'; import { useAASStore } from '@/store/AASDataStore'; import { useInfrastructureStore } from '@/store/InfrastructureStore'; import { useNavigationStore } from '@/store/NavigationStore'; + import { Endpoint, ProtocolInformation } from '@/types/Descriptors'; import { getCreatedSubmodelElementPath, isDataElementModelType } from '@/utils/AAS/SubmodelElementPathUtils'; import { base64Decode, base64Encode } from '@/utils/EncodeDecodeUtils'; @@ -58,6 +60,7 @@ // Composables const { postSubmodel, postSubmodelElement } = useSMRepositoryClient(); + const { postSubmodelDescriptor, putSubmodelDescriptor, createDescriptorFromSubmodel } = useSMRegistryClient(); const { putAas } = useAASRepositoryClient(); // Data @@ -67,7 +70,11 @@ // Computed Properties const selectedAAS = computed(() => aasStore.getSelectedAAS); // Get the selected AAS from Store + const selectedInfrastructure = computed(() => infrastructureStore.getSelectedInfrastructure); const submodelRepoUrl = computed(() => infrastructureStore.getSubmodelRepoURL); + const submodelRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.SubmodelRepo?.hasRegistryIntegration ?? true + ); watch( () => props.modelValue, @@ -117,6 +124,7 @@ await postSubmodel(submodel); // Add Submodel Reference to AAS await addSubmodelReferenceToAas(submodel); + await syncSubmodelDescriptor(submodel); // Fetch and dispatch Submodel const query = structuredClone(route.query); query.path = submodelRepoUrl.value + '/' + base64Encode(submodel.id); @@ -218,6 +226,43 @@ aasStore.dispatchSelectedAAS(localAAS); } + async function syncSubmodelDescriptor(submodel: aasTypes.Submodel): Promise { + if (submodelRepoHasRegistryIntegration.value) { + return; + } + + const submodelHref = `${submodelRepoUrl.value}/submodels/${base64Encode(submodel.id)}`; + const descriptor = createDescriptorFromSubmodel( + jsonization.toJsonable(submodel), + createEndpoints(submodelHref, 'SUBMODEL-3.0') + ); + + const success = (await putSubmodelDescriptor(descriptor)) || (await postSubmodelDescriptor(descriptor)); + if (!success) { + navigationStore.dispatchSnackbar({ + status: true, + timeout: 8000, + color: 'warning', + btnColor: 'buttonText', + baseError: 'Submodel inserted with synchronization warning.', + extendedError: `Failed to synchronize Submodel descriptor for '${submodel.id}'.`, + }); + } + } + + function createEndpoints(href: string, type: string): Array { + let protocol: string | null = null; + try { + const url = new URL(href); + protocol = url.protocol.replace(/:$/, ''); + } catch { + // If href is not a valid absolute URL, keep protocol null. + } + + const protocolInformation = new ProtocolInformation(href, null, protocol); + return [new Endpoint(type, protocolInformation)]; + } + function isValidJson(jsonString: string): boolean { try { JSON.parse(jsonString); diff --git a/aas-web-ui/src/components/EditorComponents/SubmodelForm.vue b/aas-web-ui/src/components/EditorComponents/SubmodelForm.vue index 64b2ede55..94fa6cb49 100644 --- a/aas-web-ui/src/components/EditorComponents/SubmodelForm.vue +++ b/aas-web-ui/src/components/EditorComponents/SubmodelForm.vue @@ -170,6 +170,7 @@ import { useAASStore } from '@/store/AASDataStore'; import { useInfrastructureStore } from '@/store/InfrastructureStore'; import { useNavigationStore } from '@/store/NavigationStore'; + import { Endpoint, ProtocolInformation } from '@/types/Descriptors'; import { base64Encode } from '@/utils/EncodeDecodeUtils'; const props = defineProps<{ @@ -195,7 +196,7 @@ }>(); const { postSubmodel, putSubmodel } = useSMRepositoryClient(); - const { putSubmodelDescriptor, createDescriptorFromSubmodel } = useSMRegistryClient(); + const { postSubmodelDescriptor, putSubmodelDescriptor, createDescriptorFromSubmodel } = useSMRegistryClient(); const { fetchSmById, fetchSmDescriptor, fetchAndDispatchSm } = useSMHandling(); const { putAas } = useAASRepositoryClient(); @@ -222,7 +223,11 @@ // Computed Properties const selectedNode = computed(() => aasStore.getSelectedNode); // Get the selected AAS from Store const selectedAAS = computed(() => aasStore.getSelectedAAS); // Get the selected AAS from Store + const selectedInfrastructure = computed(() => infrastructureStore.getSelectedInfrastructure); const submodelRepoUrl = computed(() => infrastructureStore.getSubmodelRepoURL); + const submodelRepoHasRegistryIntegration = computed( + () => selectedInfrastructure.value?.components?.SubmodelRepo?.hasRegistryIntegration ?? true + ); const bordersToShow = computed(() => (panel: number) => { let border = ''; switch (panel) { @@ -414,6 +419,7 @@ await postSubmodel(submodelObject.value); // Add Submodel Reference to AAS await addSubmodelReferenceToAas(submodelObject.value); + await syncSubmodelDescriptor(submodelObject.value); // Fetch and dispatch Submodel const query = structuredClone(route.query); query.path = submodelRepoUrl.value + '/submodels/' + base64Encode(submodelObject.value.id); @@ -422,12 +428,7 @@ } else { // Update existing Submodel await putSubmodel(submodelObject.value); - const jsonSubmodel = jsonization.toJsonable(submodelObject.value); - // Fetch the current desciptor from the registry - const fetchedDescriptor = await fetchSmDescriptor(submodelObject.value.id); - const descriptor = createDescriptorFromSubmodel(jsonSubmodel, fetchedDescriptor.endpoints); - // Update AAS Descriptor - await putSubmodelDescriptor(descriptor); + await syncSubmodelDescriptor(submodelObject.value); if (submodelObject.value.id === selectedNode.value.id) { const path = submodelRepoUrl.value + '/submodels/' + base64Encode(submodelObject.value.id); fetchAndDispatchSm(path); @@ -438,6 +439,52 @@ editSMDialog.value = false; } + async function syncSubmodelDescriptor(submodel: aasTypes.Submodel): Promise { + if (submodelRepoHasRegistryIntegration.value) { + return; + } + + const jsonSubmodel = jsonization.toJsonable(submodel); + let descriptorSuccess = false; + + try { + const fetchedDescriptor = await fetchSmDescriptor(submodel.id); + const fallbackEndpoint = `${submodelRepoUrl.value}/submodels/${base64Encode(submodel.id)}`; + const endpoints = + Array.isArray(fetchedDescriptor?.endpoints) && fetchedDescriptor.endpoints.length > 0 + ? fetchedDescriptor.endpoints + : createEndpoints(fallbackEndpoint, 'SUBMODEL-3.0'); + const descriptor = createDescriptorFromSubmodel(jsonSubmodel, endpoints); + descriptorSuccess = (await putSubmodelDescriptor(descriptor)) || (await postSubmodelDescriptor(descriptor)); + } catch (error) { + console.error('Failed to synchronize Submodel descriptor:', error); + } + + if (!descriptorSuccess) { + navigationStore.dispatchSnackbar({ + status: true, + timeout: 8000, + color: 'warning', + btnColor: 'buttonText', + baseError: 'Submodel saved with synchronization warning.', + extendedError: `Failed to synchronize Submodel descriptor for '${submodel.id}'.`, + }); + } + } + + function createEndpoints(href: string, type: string): Array { + let protocol: string | null = null; + try { + const url = new URL(href); + protocol = url.protocol.replace(/:$/, ''); + } catch { + // If href is not a valid absolute URL, keep protocol null. + } + + const protocolInformation = new ProtocolInformation(href, null, protocol); + return [new Endpoint(type, protocolInformation)]; + } + async function addSubmodelReferenceToAas(submodel: aasTypes.Submodel): Promise { if (selectedAAS.value === null) return; const localAAS = { ...selectedAAS.value }; diff --git a/aas-web-ui/src/composables/Client/AASDiscoveryClient.ts b/aas-web-ui/src/composables/Client/AASDiscoveryClient.ts index 894813e0e..d933c88f4 100644 --- a/aas-web-ui/src/composables/Client/AASDiscoveryClient.ts +++ b/aas-web-ui/src/composables/Client/AASDiscoveryClient.ts @@ -9,7 +9,7 @@ export function useAASDiscoveryClient() { const infrastructureStore = useInfrastructureStore(); // Composables - const { getRequest } = useRequestHandling(); + const { getRequest, postRequest, deleteRequest } = useRequestHandling(); const endpointPath = '/lookup/shells'; @@ -68,6 +68,85 @@ export function useAASDiscoveryClient() { return failResponse; } + function resolveDiscoveryBaseUrl(endpoint?: string): string { + let aasDiscUrl = endpoint ? endpoint.trim() : aasDiscoveryUrl.value.trim(); + if (aasDiscUrl === '') return ''; + if (aasDiscUrl.endsWith('/')) aasDiscUrl = stripLastCharacter(aasDiscUrl); + if (!aasDiscUrl.endsWith(endpointPath)) aasDiscUrl += endpointPath; + + return aasDiscUrl; + } + + function createAssetLinksFromAssetInformation( + globalAssetId?: string | null, + specificAssetIds?: Array<{ name?: string; value?: string }> | null + ): Array<{ name: string; value: string }> { + const links: Array<{ name: string; value: string }> = []; + + if (typeof globalAssetId === 'string' && globalAssetId.trim() !== '') { + links.push({ name: 'globalAssetId', value: globalAssetId.trim() }); + } + + if (Array.isArray(specificAssetIds)) { + for (const specificAssetId of specificAssetIds) { + const name = typeof specificAssetId?.name === 'string' ? specificAssetId.name.trim() : ''; + const value = typeof specificAssetId?.value === 'string' ? specificAssetId.value.trim() : ''; + if (name !== '' && value !== '') { + links.push({ name, value }); + } + } + } + + return links; + } + + async function upsertAssetLinksForAas( + aasId: string, + assetLinks: Array<{ name: string; value: string }>, + endpoint?: string + ): Promise { + const failResponse = false; + + if (!aasId) return failResponse; + + aasId = aasId.trim(); + + if (aasId === '' || !Array.isArray(assetLinks) || assetLinks.length === 0) return failResponse; + + const discoveryBaseUrl = resolveDiscoveryBaseUrl(endpoint); + if (discoveryBaseUrl === '') return failResponse; + + const path = `${discoveryBaseUrl}/${base64Encode(aasId)}`; + const context = 'updating AAS discovery asset links'; + const disableMessage = false; + const headers = new Headers(); + headers.append('Content-Type', 'application/json'); + const body = JSON.stringify(assetLinks); + + const response = await postRequest(path, body, headers, context, disableMessage); + return response.success; + } + + async function deleteAssetLinksForAas(aasId: string, endpoint?: string): Promise { + const failResponse = false; + + if (!aasId) return failResponse; + + aasId = aasId.trim(); + + if (aasId === '') return failResponse; + + const discoveryBaseUrl = resolveDiscoveryBaseUrl(endpoint); + if (discoveryBaseUrl === '') return failResponse; + + const path = `${discoveryBaseUrl}/${base64Encode(aasId)}`; + const context = 'deleting AAS discovery asset links'; + const disableMessage = false; + const response = await deleteRequest(path, context, disableMessage); + + return response.success; + } + /** * Checks the availability of a global asset by its ID. * @@ -98,6 +177,9 @@ export function useAASDiscoveryClient() { endpointPath, getAasId, getAasIds, + createAssetLinksFromAssetInformation, + upsertAssetLinksForAas, + deleteAssetLinksForAas, isAvailableById, }; } diff --git a/aas-web-ui/src/composables/Client/AASRegistryClient.ts b/aas-web-ui/src/composables/Client/AASRegistryClient.ts index 38af5be62..dabf6c486 100644 --- a/aas-web-ui/src/composables/Client/AASRegistryClient.ts +++ b/aas-web-ui/src/composables/Client/AASRegistryClient.ts @@ -13,7 +13,7 @@ export function useAASRegistryClient() { const infrastructureStore = useInfrastructureStore(); //Composables - const { getRequest, postRequest, putRequest } = useRequestHandling(); + const { getRequest, postRequest, putRequest, deleteRequest } = useRequestHandling(); const endpointPath = '/shell-descriptors'; @@ -181,6 +181,28 @@ export function useAASRegistryClient() { return response.success; } + async function deleteAasDescriptor(aasId: string): Promise { + const failResponse = false; + + if (!aasId) return failResponse; + + aasId = aasId.trim(); + + if (aasId === '') return failResponse; + + let aasRegUrl = aasRegistryUrl.value.trim(); + if (aasRegUrl === '') return failResponse; + if (aasRegUrl.endsWith('/')) aasRegUrl = stripLastCharacter(aasRegUrl); + if (!aasRegUrl.endsWith(endpointPath)) aasRegUrl += endpointPath; + + const context = 'deleting AAS Descriptor'; + const disableMessage = false; + const path = aasRegUrl + '/' + base64Encode(aasId); + const response = await deleteRequest(path, context, disableMessage); + + return response.success; + } + function createDescriptorFromAAS( aas: jsonization.JsonObject, endpoints: Array @@ -212,6 +234,7 @@ export function useAASRegistryClient() { aasDescriptorIsAvailableById, putAasDescriptor, postAasDescriptor, + deleteAasDescriptor, createDescriptorFromAAS, }; } diff --git a/aas-web-ui/src/composables/Client/SMRegistryClient.ts b/aas-web-ui/src/composables/Client/SMRegistryClient.ts index 6574e625c..ae12a6c0f 100644 --- a/aas-web-ui/src/composables/Client/SMRegistryClient.ts +++ b/aas-web-ui/src/composables/Client/SMRegistryClient.ts @@ -13,7 +13,7 @@ export function useSMRegistryClient() { const infrastructureStore = useInfrastructureStore(); // Composables - const { getRequest, postRequest, putRequest } = useRequestHandling(); + const { getRequest, postRequest, putRequest, deleteRequest } = useRequestHandling(); const endpointPath = '/submodel-descriptors'; @@ -181,6 +181,28 @@ export function useSMRegistryClient() { return response.success; } + async function deleteSubmodelDescriptor(smId: string): Promise { + const failResponse = false; + + if (!smId) return failResponse; + + smId = smId.trim(); + + if (smId === '') return failResponse; + + let smRegistryUrl = submodelRegistryUrl.value.trim(); + if (smRegistryUrl === '') return failResponse; + if (smRegistryUrl.endsWith('/')) smRegistryUrl = stripLastCharacter(smRegistryUrl); + if (!smRegistryUrl.endsWith(endpointPath)) smRegistryUrl += endpointPath; + + const context = 'deleting Submodel Descriptor'; + const disableMessage = false; + const path = smRegistryUrl + '/' + base64Encode(smId); + const response = await deleteRequest(path, context, disableMessage); + + return response.success; + } + function createDescriptorFromSubmodel( submodel: jsonization.JsonObject, endpoints: Array @@ -194,7 +216,9 @@ export function useSMRegistryClient() { parsedSubmodel.description, parsedSubmodel.displayName, parsedSubmodel.extensions, - parsedSubmodel.idShort + parsedSubmodel.idShort, + parsedSubmodel.semanticId, + parsedSubmodel.supplementalSemanticIds ); descriptor = removeNullValues(descriptor); return descriptor; @@ -208,6 +232,7 @@ export function useSMRegistryClient() { isAvailableById, postSubmodelDescriptor, putSubmodelDescriptor, + deleteSubmodelDescriptor, createDescriptorFromSubmodel, }; } diff --git a/aas-web-ui/src/composables/Infrastructure/useInfrastructureStorage.ts b/aas-web-ui/src/composables/Infrastructure/useInfrastructureStorage.ts index dfe97be71..344702265 100644 --- a/aas-web-ui/src/composables/Infrastructure/useInfrastructureStorage.ts +++ b/aas-web-ui/src/composables/Infrastructure/useInfrastructureStorage.ts @@ -168,15 +168,18 @@ export function useInfrastructureStorage(): { }, AASRegistry: { url: '', + hasDiscoveryIntegration: true, }, SubmodelRegistry: { url: '', }, AASRepo: { url: '', + hasRegistryIntegration: true, }, SubmodelRepo: { url: '', + hasRegistryIntegration: true, }, ConceptDescriptionRepo: { url: '', diff --git a/aas-web-ui/src/composables/Infrastructure/useInfrastructureYamlParser.ts b/aas-web-ui/src/composables/Infrastructure/useInfrastructureYamlParser.ts index a6911be43..2160d89c5 100644 --- a/aas-web-ui/src/composables/Infrastructure/useInfrastructureYamlParser.ts +++ b/aas-web-ui/src/composables/Infrastructure/useInfrastructureYamlParser.ts @@ -155,6 +155,14 @@ export function useInfrastructureYamlParser(): { const yamlComponent = yamlConfig.components[yamlKey as keyof typeof yamlConfig.components]; if (yamlComponent?.baseUrl) { infrastructure.components[internalKey].url = yamlComponent.baseUrl; + if (yamlComponent.hasRegistryIntegration !== undefined) { + infrastructure.components[internalKey].hasRegistryIntegration = + yamlComponent.hasRegistryIntegration; + } + if (yamlComponent.hasDiscoveryIntegration !== undefined) { + infrastructure.components[internalKey].hasDiscoveryIntegration = + yamlComponent.hasDiscoveryIntegration; + } } } diff --git a/aas-web-ui/src/composables/RequestHandling.ts b/aas-web-ui/src/composables/RequestHandling.ts index f89230eff..668643d81 100644 --- a/aas-web-ui/src/composables/RequestHandling.ts +++ b/aas-web-ui/src/composables/RequestHandling.ts @@ -68,6 +68,12 @@ export function useRequestHandling() { return { success: false }; } + async function parseJsonIfPresent(response: Response): Promise { + const bodyText = await response.text(); + if (bodyText.trim() === '') return undefined; + return JSON.parse(bodyText); + } + function getRequest(path: string, context: string, disableMessage: boolean, headers: Headers = new Headers()): any { if (shouldAddAuthorizationHeader(path)) { // No Authorization needed for the /description endpoint. @@ -80,7 +86,7 @@ export function useRequestHandling() { response.headers.get('Content-Type')?.split(';')[0] === 'application/json' && response.headers.get('Content-Length') !== '0' ) { - return { response: response, data: await response.json() }; // Return the response as JSON + return { response: response, data: await parseJsonIfPresent(response) }; // Return the response as JSON } else if ( response.headers.get('Content-Type')?.split(';')[0] === 'application/asset-administration-shell-package+xml' && @@ -126,6 +132,9 @@ export function useRequestHandling() { } else if (data) { // Successful response from the server return { success: true, data: data, status: response.status, raw: response }; + } else if (response.ok && response.status >= 200 && response.status < 300) { + // Empty successful response + return { success: true, data: {}, status: response.status, raw: response }; } else { // Unexpected response format throw new Error('Unexpected response format'); @@ -152,7 +161,7 @@ export function useRequestHandling() { response.headers.get('Content-Type')?.split(';')[0] === 'application/json' && response.headers.get('Content-Length') !== '0' ) { - return response.json(); // Return the response as JSON + return parseJsonIfPresent(response); // Return the response as JSON } else if ( response.headers.get('Content-Type')?.split(';')[0] === 'text/csv' && response.headers.get('Content-Length') !== '0' @@ -200,7 +209,7 @@ export function useRequestHandling() { response.headers.get('Content-Type')?.split(';')[0] === 'application/json' && response.headers.get('Content-Length') !== '0' ) { - return response.json(); // Return the response as JSON + return parseJsonIfPresent(response); // Return the response as JSON } else if (!response.ok) { // No content but received an HTTP error status throw new Error('Error status: ' + response.status); @@ -237,7 +246,7 @@ export function useRequestHandling() { response.headers.get('Content-Type')?.split(';')[0] === 'application/json' && response.headers.get('Content-Length') !== '0' ) { - return response.json(); // Return the response as JSON + return parseJsonIfPresent(response); // Return the response as JSON } else if (!response.ok) { // No content but received an HTTP error status throw new Error('Error status: ' + response.status); @@ -273,7 +282,7 @@ export function useRequestHandling() { response.headers.get('Content-Type')?.split(';')[0] === 'application/json' && response.headers.get('Content-Length') !== '0' ) { - return response.json(); // Return the response as JSON + return parseJsonIfPresent(response); // Return the response as JSON } else if (!response.ok) { // No content but received an HTTP error status throw new Error('Error status: ' + response.status); diff --git a/aas-web-ui/src/pages/modules/DPPDemo/index.vue b/aas-web-ui/src/pages/modules/DPPDemo/index.vue index 96fd9288e..870b28c89 100644 --- a/aas-web-ui/src/pages/modules/DPPDemo/index.vue +++ b/aas-web-ui/src/pages/modules/DPPDemo/index.vue @@ -15,7 +15,7 @@ :aspect-ratio="productImageAspectRatio" class="w-100 mx-auto border rounded-lg" style="max-width: 300px" - cover + contain @error="onProductImageError" /> | null; globalAssetId?: string | null; idShort?: string | null; - specificAssetId?: Array | null; + specificAssetIds?: Array | null; submodelDescriptors?: Array | null; constructor( @@ -27,7 +27,7 @@ export class AASDescriptor { extensions?: Array | null, globalAssetId?: string | null, idShort?: string | null, - specificAssetId?: Array | null, + specificAssetIds?: Array | null, submodelDescriptors?: Array | null ) { this.id = id; @@ -40,7 +40,7 @@ export class AASDescriptor { this.extensions = extensions; this.globalAssetId = globalAssetId; this.idShort = idShort; - this.specificAssetId = specificAssetId; + this.specificAssetIds = specificAssetIds; this.submodelDescriptors = submodelDescriptors; } } diff --git a/aas-web-ui/src/types/Infrastructure.ts b/aas-web-ui/src/types/Infrastructure.ts index 4bb5838f8..486f907d8 100644 --- a/aas-web-ui/src/types/Infrastructure.ts +++ b/aas-web-ui/src/types/Infrastructure.ts @@ -58,6 +58,8 @@ export interface TokenData { */ export interface ComponentConfig { url: string; + hasRegistryIntegration?: boolean; + hasDiscoveryIntegration?: boolean; } /** @@ -152,6 +154,8 @@ export type ComponentTestingLoading = Record; */ export interface YamlComponentConfig { baseUrl: string; + hasRegistryIntegration?: boolean; + hasDiscoveryIntegration?: boolean; } /**