From 030e374a56a0254ca49f6c9b871fb1daf5b0d013 Mon Sep 17 00:00:00 2001 From: Nils Rothamel Date: Tue, 28 Oct 2025 11:11:12 +0100 Subject: [PATCH 01/12] feat: add support for AAS registries in AasListDataWrapper and related components --- .../list/_components/AasListDataWrapper.tsx | 8 +- .../list/_components/AasListTableRow.tsx | 2 +- .../_components/filter/SelectRepository.tsx | 92 +++++++++++++++---- .../registryServiceApi.ts | 36 ++++++++ .../registryServiceApiInMemory.ts | 25 +++++ .../registryServiceApiInterface.ts | 7 ++ .../services/database/ConnectionTypeEnum.ts | 3 + .../database/infrastructureDatabaseActions.ts | 17 ++++ src/lib/services/list-service/ListService.ts | 65 +++++++++++-- .../list-service/aasListApiActions.ts | 3 +- .../apiResponseWrapper/apiResponseWrapper.ts | 45 ++++++--- 11 files changed, 259 insertions(+), 44 deletions(-) diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.tsx index 10d0e92c6..709648ef6 100644 --- a/src/app/[locale]/list/_components/AasListDataWrapper.tsx +++ b/src/app/[locale]/list/_components/AasListDataWrapper.tsx @@ -28,6 +28,7 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra const [, setAasListFiltered] = useState(); const [selectedAasList, setSelectedAasList] = useState(); const [selectedRepository, setSelectedRepository] = useState(); + const [selectedType, setSelectedType] = useState<'repository' | 'registry' | undefined>(); const env = useEnv(); const t = useTranslations('pages.aasList'); const { showError } = useShowError(); @@ -56,7 +57,7 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra setIsLoadingList(true); clearResults(); - const response = await getAasListEntities(selectedRepository, 10, newCursor); + const response = await getAasListEntities(selectedRepository, 10, newCursor, selectedType); if (response.success) { setAasList(response); @@ -166,7 +167,10 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra {!hideRepoSelection && ( - + {env.COMPARISON_FEATURE_FLAG && ( { // this can happen if the property is not a MultiLanguageValueOnly type // e.g. if the property is a AAS Property type (incorrect by specification but possible) string or an error occurs if (typeof property === 'string') return property; - console.error('Error translating property:', e); + console.error('Error translating property:', e, 'Property:', property); return ''; } }; diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.tsx b/src/app/[locale]/list/_components/filter/SelectRepository.tsx index cf7336b88..71d06fae7 100644 --- a/src/app/[locale]/list/_components/filter/SelectRepository.tsx +++ b/src/app/[locale]/list/_components/filter/SelectRepository.tsx @@ -1,16 +1,32 @@ import { Dispatch, SetStateAction, useState } from 'react'; -import { Box, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Skeleton, Typography } from '@mui/material'; +import { + Box, + FormControl, + InputLabel, + ListSubheader, + MenuItem, + Select, + SelectChangeEvent, + Skeleton, + Typography, +} from '@mui/material'; import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect'; -import { getAasRepositoriesIncludingDefault } from 'lib/services/database/infrastructureDatabaseActions'; +import { + getAasRegistriesIncludingDefault, + getAasRepositoriesIncludingDefault, +} from 'lib/services/database/infrastructureDatabaseActions'; import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner'; import { useTranslations } from 'next-intl'; import { RepositoryWithInfrastructure } from 'lib/services/database/InfrastructureMappedTypes'; +type ConnectionWithType = RepositoryWithInfrastructure & { type: 'repository' | 'registry' }; + export function SelectRepository(props: { onSelectedRepositoryChanged: Dispatch>; + onSelectedTypeChanged?: Dispatch>; }) { - const [aasRepositories, setAasRepositories] = useState([]); - const [selectedRepository, setSelectedRepository] = useState(''); + const [aasConnections, setAasConnections] = useState([]); + const [selectedConnectionIndex, setSelectedConnectionIndex] = useState(0); const notificationSpawner = useNotificationSpawner(); const [isLoading, setIsLoading] = useState(false); const t = useTranslations('pages.aasList'); @@ -19,10 +35,18 @@ export function SelectRepository(props: { try { setIsLoading(true); const aasRepositories: RepositoryWithInfrastructure[] = await getAasRepositoriesIncludingDefault(); - setAasRepositories(aasRepositories); - if (aasRepositories.length > 0 && aasRepositories[0].id) { - setSelectedRepository(aasRepositories[0].id); - props.onSelectedRepositoryChanged(aasRepositories[0]); + const aasRegistries: RepositoryWithInfrastructure[] = await getAasRegistriesIncludingDefault(); + + const connections: ConnectionWithType[] = [ + ...aasRepositories.map((repo) => ({ ...repo, type: 'repository' as const })), + ...aasRegistries.map((registry) => ({ ...registry, type: 'registry' as const })), + ]; + + setAasConnections(connections); + if (connections.length > 0) { + setSelectedConnectionIndex(0); + props.onSelectedRepositoryChanged(connections[0]); + props.onSelectedTypeChanged?.(connections[0].type); } } catch (error) { notificationSpawner.spawn({ @@ -35,9 +59,12 @@ export function SelectRepository(props: { } }, []); - const onRepositoryChanged = (event: SelectChangeEvent) => { - setSelectedRepository(event.target.value); - props.onSelectedRepositoryChanged(aasRepositories.find((repo) => repo.id === event.target.value)); + const onRepositoryChanged = (event: SelectChangeEvent) => { + const index = event.target.value as number; + setSelectedConnectionIndex(index); + const selected = aasConnections[index]; + props.onSelectedRepositoryChanged(selected); + props.onSelectedTypeChanged?.(selected?.type); }; return ( @@ -53,25 +80,52 @@ export function SelectRepository(props: { data-testid="repository-select" labelId="aas-repository-select" variant="standard" - value={selectedRepository} + value={selectedConnectionIndex} label={t('repositoryDropdownLabel')} onChange={onRepositoryChanged} > - {aasRepositories.map((repo, index) => { - return ( - - {repo.url}{' '} + Repositories + {aasConnections + .map((conn, index) => (conn.type === 'repository' ? { conn, index } : null)) + .filter((item): item is { conn: ConnectionWithType; index: number } => item !== null) + .map(({ conn, index }) => ( + + {conn.url}{' '} + + ({conn.infrastructureName}) + + + ))} + Registries + {aasConnections + .map((conn, index) => (conn.type === 'registry' ? { conn, index } : null)) + .filter((item): item is { conn: ConnectionWithType; index: number } => item !== null) + .map(({ conn, index }) => ( + + {conn.url}{' '} - ({repo.infrastructureName}) + ({conn.infrastructureName}) - ); - })} + ))} )} diff --git a/src/lib/api/registry-service-api/registryServiceApi.ts b/src/lib/api/registry-service-api/registryServiceApi.ts index f228395e7..ffd01dd3c 100644 --- a/src/lib/api/registry-service-api/registryServiceApi.ts +++ b/src/lib/api/registry-service-api/registryServiceApi.ts @@ -10,6 +10,7 @@ import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapp import path from 'node:path'; import ServiceReachable from 'test-utils/TestUtils'; import logger, { logResponseDebug } from 'lib/util/Logger'; +import { PaginationData } from 'lib/api/basyx-v3/types'; export class RegistryServiceApi implements IRegistryServiceApi { constructor( @@ -44,6 +45,41 @@ export class RegistryServiceApi implements IRegistryServiceApi { return this.baseUrl; } + async getAllAssetAdministrationShellDescriptors( + limit?: number, + cursor?: string, + ): Promise>> { + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + const url = new URL(this.baseUrl + `/shell-descriptors?limit=${limit}&cursor=${cursor ?? ''}`); + + const response = await this.http.fetch>(url, { + method: 'GET', + headers, + }); + + if (!response.isSuccess) { + logResponseDebug( + this.log, + 'getAllAssetAdministrationShellDescriptors', + 'Registry fetch failed, no AAS descriptors found', + response, + { message: response.message }, + ); + return response; + } + logResponseDebug( + this.log, + 'getAllAssetAdministrationShellDescriptors', + 'Registry fetch successful, AAS descriptors found', + response, + ); + return response; + } + async getAssetAdministrationShellDescriptorById( aasId: string, ): Promise> { diff --git a/src/lib/api/registry-service-api/registryServiceApiInMemory.ts b/src/lib/api/registry-service-api/registryServiceApiInMemory.ts index 2c760a608..78323c1c8 100644 --- a/src/lib/api/registry-service-api/registryServiceApiInMemory.ts +++ b/src/lib/api/registry-service-api/registryServiceApiInMemory.ts @@ -9,6 +9,7 @@ import { } from 'lib/util/apiResponseWrapper/apiResponseWrapper'; import ServiceReachable from 'test-utils/TestUtils'; import { ApiResultStatus } from 'lib/util/apiResponseWrapper/apiResultStatus'; +import { PaginationData } from 'lib/api/basyx-v3/types'; export type AasRegistryEndpointEntryInMemory = { endpoint: URL | string; @@ -31,6 +32,30 @@ export class RegistryServiceApiInMemory implements IRegistryServiceApi { return this.baseUrl; } + async getAllAssetAdministrationShellDescriptors( + limit?: number, + cursor?: string, + ): Promise>> { + if (this.reachable !== ServiceReachable.Yes) + return wrapErrorCode(ApiResultStatus.UNKNOWN_ERROR, 'Service not reachable'); + + const startIndex = cursor ? parseInt(cursor, 10) : 0; + const endIndex = limit ? startIndex + limit : this.registryShellDescriptors.length; + const descriptors = this.registryShellDescriptors.slice(startIndex, endIndex); + const nextCursor = endIndex < this.registryShellDescriptors.length ? endIndex.toString() : ''; + + const paginationData: PaginationData = { + result: descriptors, + paging_metadata: { + cursor: nextCursor, + }, + }; + + const response = new Response(JSON.stringify(paginationData), options); + const value = await wrapResponse>(response); + return Promise.resolve(value); + } + async postAssetAdministrationShellDescriptor( shellDescriptor: AssetAdministrationShellDescriptor, ): Promise> { diff --git a/src/lib/api/registry-service-api/registryServiceApiInterface.ts b/src/lib/api/registry-service-api/registryServiceApiInterface.ts index d197f055b..3b0e564f5 100644 --- a/src/lib/api/registry-service-api/registryServiceApiInterface.ts +++ b/src/lib/api/registry-service-api/registryServiceApiInterface.ts @@ -1,6 +1,7 @@ import { AssetAdministrationShellDescriptor } from 'lib/types/registryServiceTypes'; import { AssetAdministrationShell } from 'lib/api/aas/models'; import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper'; +import { PaginationData } from 'lib/api/basyx-v3/types'; export interface IRegistryServiceApi { /** @@ -8,6 +9,12 @@ export interface IRegistryServiceApi { */ getBaseUrl(): string; + getAllAssetAdministrationShellDescriptors( + limit?: number, + cursor?: string, + options?: object, + ): Promise>>; + getAssetAdministrationShellDescriptorById( aasId: string, ): Promise>; diff --git a/src/lib/services/database/ConnectionTypeEnum.ts b/src/lib/services/database/ConnectionTypeEnum.ts index aca80112f..d244289cd 100644 --- a/src/lib/services/database/ConnectionTypeEnum.ts +++ b/src/lib/services/database/ConnectionTypeEnum.ts @@ -1,5 +1,6 @@ export enum ConnectionTypeEnum { AAS_REPOSITORY = 'AAS_REPOSITORY', + AAS_REGISTRY = 'AAS_REGISTRY', SUBMODEL_REPOSITORY = 'SUBMODEL_REPOSITORY', } @@ -7,6 +8,8 @@ export function getTypeAction(type: ConnectionTypeEnum): { id: string; typeName: switch (type) { case ConnectionTypeEnum.AAS_REPOSITORY: return { id: '0', typeName: ConnectionTypeEnum.AAS_REPOSITORY }; + case ConnectionTypeEnum.AAS_REGISTRY: + return { id: '1', typeName: ConnectionTypeEnum.AAS_REGISTRY }; case ConnectionTypeEnum.SUBMODEL_REPOSITORY: return { id: '2', typeName: ConnectionTypeEnum.SUBMODEL_REPOSITORY }; default: diff --git a/src/lib/services/database/infrastructureDatabaseActions.ts b/src/lib/services/database/infrastructureDatabaseActions.ts index f495a2f54..bbf03db90 100644 --- a/src/lib/services/database/infrastructureDatabaseActions.ts +++ b/src/lib/services/database/infrastructureDatabaseActions.ts @@ -89,6 +89,23 @@ export async function getAasRepositoriesIncludingDefault() { } } +export async function getAasRegistriesIncludingDefault() { + const defaultAasRegistry = { + id: 'default', + url: envs.REGISTRY_API_URL || '', + infrastructureName: (await getDefaultInfrastructure()).name, + isDefault: true, + }; + try { + const aasRegistriesDb = await getConnectionDataByTypeAction(getTypeAction(ConnectionTypeEnum.AAS_REGISTRY)); + + return [defaultAasRegistry, ...aasRegistriesDb]; + } catch (error) { + logger.error('Failed to fetch AAS registries', error); + return []; + } +} + export async function getSubmodelRepositoriesIncludingDefault() { const submodelRepositoriesDb = await getConnectionDataByTypeAction( getTypeAction(ConnectionTypeEnum.SUBMODEL_REPOSITORY), diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index 16572e991..be0529fd8 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -1,5 +1,6 @@ import { IAssetAdministrationShellRepositoryApi, ISubmodelRepositoryApi } from 'lib/api/basyx-v3/apiInterface'; import { AssetAdministrationShellRepositoryApi, SubmodelRepositoryApi } from 'lib/api/basyx-v3/api'; +import { RegistryServiceApi } from 'lib/api/registry-service-api/registryServiceApi'; import { mnestixFetch } from 'lib/api/infrastructure'; import { AssetAdministrationShell, Submodel } from 'lib/api/aas/models'; import ServiceReachable from 'test-utils/TestUtils'; @@ -10,6 +11,8 @@ import { getInfrastructureByName } from '../database/infrastructureDatabaseActio import { createSecurityHeaders } from 'lib/util/securityHelpers/SecurityConfiguration'; import logger, { logInfo } from 'lib/util/Logger'; import { RepositoryWithInfrastructure } from '../database/InfrastructureMappedTypes'; +import { IRegistryServiceApi } from 'lib/api/registry-service-api/registryServiceApiInterface'; +import { AssetAdministrationShellDescriptor } from 'lib/types/registryServiceTypes'; export type ListEntityDto = { aasId: string; @@ -35,6 +38,7 @@ export class ListService { private constructor( protected readonly getTargetAasRepositoryClient: () => IAssetAdministrationShellRepositoryApi, protected readonly getTargetSubmodelRepositoryClient: () => ISubmodelRepositoryApi, + protected readonly getTargetAasRegistryClient: () => IRegistryServiceApi, private readonly log: typeof logger = logger, ) {} @@ -55,6 +59,7 @@ export class ListService { () => AssetAdministrationShellRepositoryApi.create(targetAasRepository.url, mnestixFetch(securityHeader)), // For now, we only use the same repository. () => SubmodelRepositoryApi.create(targetAasRepository.url, mnestixFetch(securityHeader)), + () => RegistryServiceApi.create(targetAasRepository.url, mnestixFetch(securityHeader), listServiceLogger), listServiceLogger, ); } @@ -74,9 +79,16 @@ export class ListService { submodelInRepositories, targetAasRepository, ); + const targetAasRegistryClient = RegistryServiceApi.createNull( + 'https://targetAasRegistryClient.com', + shellsInRepositories, + [], + targetAasRepository, + ); return new ListService( () => targetAasRepositoryClient, () => targetSubmodelRepositoryClient, + () => targetAasRegistryClient, ); } @@ -87,18 +99,53 @@ export class ListService { * This logic is needed to hide the configuration AASs created by the mnestix-api. * @param limit * @param cursor + * @param type */ - async getAasListEntities(limit: number, cursor?: string): Promise { - logInfo(this.log, 'getAasListEntities', 'Fetching aas list from repository'); - const targetAasRepositoryClient = this.getTargetAasRepositoryClient(); - const response = await targetAasRepositoryClient.getAllAssetAdministrationShells(limit, cursor); + async getAasListEntities(limit: number, cursor?: string, type?: 'repository' | 'registry'): Promise { + let assetAdministrationShells: AssetAdministrationShell[] = []; + let nextCursor: string | undefined; - if (!response.isSuccess) { - return { success: false, error: response }; - } + if (type === 'registry') { + logInfo(this.log, 'getAasListEntities', 'Fetching aas list from registry'); + const targetAasRegistryClient = this.getTargetAasRegistryClient(); + const descriptorsResponse = await targetAasRegistryClient.getAllAssetAdministrationShellDescriptors( + limit, + cursor, + ); + + if (!descriptorsResponse.isSuccess) { + return { success: false, error: descriptorsResponse }; + } - const { result: assetAdministrationShells, paging_metadata } = response.result; - const nextCursor = paging_metadata.cursor; + const { result: descriptors, paging_metadata } = descriptorsResponse.result; + nextCursor = paging_metadata?.cursor; + + // Fetch all AAS from their endpoints in parallel + const aasPromises = descriptors.map(async (descriptor: AssetAdministrationShellDescriptor) => { + if (!descriptor.endpoints || descriptor.endpoints.length === 0) { + this.log?.warn(`Descriptor ${descriptor.id} has no endpoints`); + return null; + } + const endpoint = new URL(descriptor.endpoints[0].protocolInformation.href); + const aasResponse = await targetAasRegistryClient.getAssetAdministrationShellFromEndpoint(endpoint); + return aasResponse.isSuccess ? aasResponse.result : null; + }); + + const aasResults = await Promise.all(aasPromises); + assetAdministrationShells = aasResults.filter((aas): aas is AssetAdministrationShell => aas !== null); + } else { + logInfo(this.log, 'getAasListEntities', 'Fetching aas list from repository'); + const targetAasRepositoryClient = this.getTargetAasRepositoryClient(); + const response = await targetAasRepositoryClient.getAllAssetAdministrationShells(limit, cursor); + + if (!response.isSuccess) { + return { success: false, error: response }; + } + + const { result: shells, paging_metadata } = response.result; + assetAdministrationShells = shells; + nextCursor = paging_metadata?.cursor; + } const aasListDtos = assetAdministrationShells .filter((aas) => { diff --git a/src/lib/services/list-service/aasListApiActions.ts b/src/lib/services/list-service/aasListApiActions.ts index bee27991e..8ef2d21f3 100644 --- a/src/lib/services/list-service/aasListApiActions.ts +++ b/src/lib/services/list-service/aasListApiActions.ts @@ -8,10 +8,11 @@ export async function getAasListEntities( targetRepository: RepositoryWithInfrastructure, limit: number, cursor?: string, + type?: 'repository' | 'registry', ) { const logger = createRequestLogger(await headers()); const listService = await ListService.create(targetRepository, logger); - return listService.getAasListEntities(limit, cursor); + return listService.getAasListEntities(limit, cursor, type); } export async function getNameplateValuesForAAS(targetRepository: RepositoryWithInfrastructure, aasId: string) { diff --git a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts index aca0ab528..10f69e786 100644 --- a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts +++ b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts @@ -78,19 +78,42 @@ export async function wrapResponse(response: Response): Promise console.warn(e.message)); - return wrapSuccess(result, response.status, getStatus(response.status)); + // Check if it's a binary content type (images, etc.) + const binaryContentTypes = ['image/', 'application/octet-stream', 'application/pdf', 'video/', 'audio/']; + const isBinaryContent = binaryContentTypes.some((type) => contentType?.includes(type)); + + if (isBinaryContent) { + const fileFromResponse = await response.blob(); + return wrapSuccess(fileFromResponse as T, response.status, getStatus(response.status)); + } + + // For all other content types, read as text + const text = await response.text(); + if (!text?.trim()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return wrapSuccess(undefined as any, response.status, getStatus(response.status)); } - // Default to Blob for all other content types - const fileFromResponse = await response.blob(); - return wrapSuccess(fileFromResponse as T, response.status, getStatus(response.status)); + // Try to parse as JSON if Content-Type says so or if it looks like JSON + const isJsonContentType = contentType?.includes('application/json'); + const trimmed = text.trim(); + const looksLikeJson = /^[\[{"']|^(-?\d+\.?\d*|true|false|null)$/.test(trimmed); + + if (isJsonContentType || looksLikeJson) { + try { + const jsonResult = JSON.parse(text); + return wrapSuccess(jsonResult as T, response.status, getStatus(response.status)); + } catch (error) { + if (isJsonContentType) { + console.warn('Failed to parse JSON response despite application/json Content-Type:', error); + } + } + } + + // Default to Blob for plain text + const blob = new Blob([text], { type: contentType || 'text/plain' }); + return wrapSuccess(blob as T, response.status, getStatus(response.status)); } export function mapFileDtoToBlob(fileDto: ApiFileDto): Blob { @@ -103,5 +126,3 @@ export async function mapBlobToFileDto(content: Blob): Promise { fileType: content.type, }; } - - From 7de2f8fe7eed5eb02783610b8f8562ae33c46b08 Mon Sep 17 00:00:00 2001 From: Nils Rothamel Date: Tue, 28 Oct 2025 11:42:43 +0100 Subject: [PATCH 02/12] feat: enhance value extraction for manufacturer details in getNameplateValuesForAAS --- src/lib/services/list-service/ListService.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index be0529fd8..14ded43ad 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -199,10 +199,22 @@ export class ListService { 'ManufacturerProductDesignation', ); + // The API might return the value directly or wrapped in an object with the property name as key + const extractValue = (response: any): MultiLanguageValueOnly | undefined => { + if (!response) return undefined; + if (Array.isArray(response)) return response; + // If response is an object with a single key, extract that value + const keys = Object.keys(response); + if (keys.length === 1 && Array.isArray(response[keys[0]])) { + return response[keys[0]]; + } + return response; + }; + return { success: true, - manufacturerName: manufacturerName.result, - manufacturerProductDesignation: manufacturerProduct.result, + manufacturerName: extractValue(manufacturerName.result), + manufacturerProductDesignation: extractValue(manufacturerProduct.result), }; } } From 6855686a6088eb211262f3b74f0fe50736c51099 Mon Sep 17 00:00:00 2001 From: Nils Rothamel Date: Tue, 28 Oct 2025 15:57:00 +0100 Subject: [PATCH 03/12] fix: add optional chaining to prevent potential runtime errors in getNameplateValuesForAAS --- src/lib/services/list-service/ListService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index 14ded43ad..3d5eeafa4 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -182,7 +182,7 @@ export class ListService { const submodelRepositoryClient = this.getTargetSubmodelRepositoryClient(); const submodelResponse = await submodelRepositoryClient.getSubmodelMetaData(submodelId); if (submodelResponse.isSuccess) { - const semanticId = submodelResponse.result?.semanticId?.keys[0].value; + const semanticId = submodelResponse.result?.semanticId?.keys[0]?.value; const nameplateKeys = [ SubmodelSemanticIdEnum.NameplateV1, SubmodelSemanticIdEnum.NameplateV2, From 993da18691e7213630542a5aea68ea6966fb6efd Mon Sep 17 00:00:00 2001 From: Nils Rothamel Date: Tue, 28 Oct 2025 17:23:53 +0100 Subject: [PATCH 04/12] feat: update repository dropdown labels to include AAS Registry for improved clarity --- src/locale/de.json | 6 +++--- src/locale/en.json | 6 +++--- src/locale/es.json | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/locale/de.json b/src/locale/de.json index 038ccb860..2adea091e 100644 --- a/src/locale/de.json +++ b/src/locale/de.json @@ -211,9 +211,9 @@ }, "maxElementsWarning": "Maximal 3 Elemente selektierbar", "page": "Seite", - "repositoryDropdownLabel": "AAS Repository (Infrastruktur)", - "selectRepository": "Bitte wählen Sie ein Repository aus", - "subHeader": "Hier finden Sie alle AAS aus Ihrem Repository. Sie können ein AAS-Repository auswählen, um zwischen den Datenquellen zu wechseln.", + "repositoryDropdownLabel": "AAS Repository oder AAS Registry (Infrastruktur)", + "selectRepository": "Bitte wählen Sie ein Repository oder AAS Registry aus", + "subHeader": "Hier finden Sie alle AAS aus Ihrem Repository oder AAS Registry. Sie können ein AAS-Repository oder AAS Registry auswählen, um zwischen den Datenquellen zu wechseln.", "titleComparisonAddButton": "AAS zum Vergleich hinzufügen" }, "aasViewer": { diff --git a/src/locale/en.json b/src/locale/en.json index 76c9f8e36..f97795fdc 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -211,9 +211,9 @@ }, "maxElementsWarning": "Cannot compare more than 3 elements", "page": "Page", - "repositoryDropdownLabel": "AAS Repository (Infrastructure)", - "selectRepository": "Please select a repository", - "subHeader": "You will find all AAS from your repository here. You can select an AAS repository to switch between data sources.", + "repositoryDropdownLabel": "AAS Repository or AAS Registry (Infrastructure)", + "selectRepository": "Please select a repository or AAS registry", + "subHeader": "You will find all AAS from your repository or AAS registry here. You can select an AAS repository or AAS registry to switch between data sources.", "titleComparisonAddButton": "Add AAS to comparison" }, "aasViewer": { diff --git a/src/locale/es.json b/src/locale/es.json index a3f107960..0021190fd 100644 --- a/src/locale/es.json +++ b/src/locale/es.json @@ -211,9 +211,9 @@ }, "maxElementsWarning": "No se pueden comparar más de 3 elementos", "page": "Página", - "repositoryDropdownLabel": "AAS repository (Infrastructure)", - "selectRepository": "Por favor, seleccione un repositorio", - "subHeader": "Todos los AAS de su repositorio se muestran aquí. Puede seleccionar otro repositorio para cambiar la fuente de los datos.", + "repositoryDropdownLabel": "AAS repository o AAS registry (Infrastructure)", + "selectRepository": "Por favor, seleccione un repositorio o AAS registry", + "subHeader": "Todos los AAS de su repositorio o AAS registry se muestran aquí. Puede seleccionar otro repositorio o AAS registry para cambiar la fuente de los datos.", "titleComparisonAddButton": "Añadir AAS a la comparación" }, "aasViewer": { From d2796837a45c704aa11d995a1bd5e39147213613 Mon Sep 17 00:00:00 2001 From: Nils Rothamel Date: Tue, 28 Oct 2025 18:13:43 +0100 Subject: [PATCH 05/12] feat: add connectionType prop to AasList and related components for improved data handling --- src/app/[locale]/list/_components/AasList.tsx | 5 ++++- .../list/_components/AasListDataWrapper.tsx | 1 + .../[locale]/list/_components/AasListTableRow.tsx | 7 +++++-- src/lib/services/list-service/ListService.ts | 15 ++++++++++----- .../util/apiResponseWrapper/apiResponseWrapper.ts | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/app/[locale]/list/_components/AasList.tsx b/src/app/[locale]/list/_components/AasList.tsx index 2051afd8d..b4f674094 100644 --- a/src/app/[locale]/list/_components/AasList.tsx +++ b/src/app/[locale]/list/_components/AasList.tsx @@ -7,6 +7,7 @@ import { RepositoryWithInfrastructure } from 'lib/services/database/Infrastructu type AasListProps = { repositoryUrl: RepositoryWithInfrastructure; + connectionType?: 'repository' | 'registry'; shells: AasListDto | undefined; comparisonFeatureFlag?: boolean; selectedAasList: string[] | undefined; @@ -14,7 +15,8 @@ type AasListProps = { }; export default function AasList(props: AasListProps) { - const { repositoryUrl, shells, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = props; + const { repositoryUrl, connectionType, shells, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = + props; const t = useTranslations('pages.aasList'); const MAX_SELECTED_ITEMS = 3; @@ -76,6 +78,7 @@ export default function AasList(props: AasListProps) { boolean | undefined; @@ -37,6 +38,7 @@ const tableBodyText = { export const AasListTableRow = (props: AasTableRowProps) => { const { repository, + connectionType, aasListEntry, comparisonFeatureFlag, checkBoxDisabled, @@ -66,9 +68,10 @@ export const AasListTableRow = (props: AasTableRowProps) => { const baseUrl = window.location.origin; const pageToGo = env.PRODUCT_VIEW_FEATURE_FLAG ? '/product' : '/viewer'; - const repoUrlParam = repository.url ? `?repoUrl=${repository.url}` : ''; + // Only send repoUrl parameter if it's a repository, not a registry + const repoUrlParam = connectionType === 'repository' && repository.url ? `?repoUrl=${repository.url}` : ''; const infrastructureParam = repository.infrastructureName - ? `&infrastructure=${repository.infrastructureName}` + ? `${repoUrlParam ? '&' : '?'}infrastructure=${repository.infrastructureName}` : ''; window.open( baseUrl + `${pageToGo}/${encodeBase64(listEntry.aasId)}${repoUrlParam}${infrastructureParam}`, diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index 3d5eeafa4..bf947642c 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -200,15 +200,20 @@ export class ListService { ); // The API might return the value directly or wrapped in an object with the property name as key - const extractValue = (response: any): MultiLanguageValueOnly | undefined => { + const extractValue = (response: unknown): MultiLanguageValueOnly | undefined => { if (!response) return undefined; if (Array.isArray(response)) return response; // If response is an object with a single key, extract that value - const keys = Object.keys(response); - if (keys.length === 1 && Array.isArray(response[keys[0]])) { - return response[keys[0]]; + if (typeof response === 'object' && response !== null) { + const keys = Object.keys(response); + if (keys.length === 1) { + const value = (response as Record)[keys[0]]; + if (Array.isArray(value)) { + return value; + } + } } - return response; + return undefined; }; return { diff --git a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts index 10f69e786..815b7ca30 100644 --- a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts +++ b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts @@ -98,7 +98,7 @@ export async function wrapResponse(response: Response): Promise Date: Tue, 28 Oct 2025 18:25:26 +0100 Subject: [PATCH 06/12] fix: refine value extraction logic in extractValue function for improved handling of API responses --- src/lib/services/list-service/ListService.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index bf947642c..3d5eeafa4 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -200,20 +200,15 @@ export class ListService { ); // The API might return the value directly or wrapped in an object with the property name as key - const extractValue = (response: unknown): MultiLanguageValueOnly | undefined => { + const extractValue = (response: any): MultiLanguageValueOnly | undefined => { if (!response) return undefined; if (Array.isArray(response)) return response; // If response is an object with a single key, extract that value - if (typeof response === 'object' && response !== null) { - const keys = Object.keys(response); - if (keys.length === 1) { - const value = (response as Record)[keys[0]]; - if (Array.isArray(value)) { - return value; - } - } + const keys = Object.keys(response); + if (keys.length === 1 && Array.isArray(response[keys[0]])) { + return response[keys[0]]; } - return undefined; + return response; }; return { From 037416194b9bc2d8a46059ab8666048e8c74c771 Mon Sep 17 00:00:00 2001 From: Moritz Hofer Date: Wed, 29 Oct 2025 17:24:25 +0100 Subject: [PATCH 07/12] Fix wrong loading of Registry Descriptors --- .../registry-service-api/registryServiceApi.ts | 11 +++++++++-- src/lib/hooks/UseAasDataLoader.ts | 17 ++++++++++++++++- src/lib/services/list-service/ListService.ts | 18 ++++++++++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/lib/api/registry-service-api/registryServiceApi.ts b/src/lib/api/registry-service-api/registryServiceApi.ts index ffd01dd3c..7cf752990 100644 --- a/src/lib/api/registry-service-api/registryServiceApi.ts +++ b/src/lib/api/registry-service-api/registryServiceApi.ts @@ -53,8 +53,15 @@ export class RegistryServiceApi implements IRegistryServiceApi { Accept: 'application/json', 'Content-Type': 'application/json', }; - - const url = new URL(this.baseUrl + `/shell-descriptors?limit=${limit}&cursor=${cursor ?? ''}`); + + let url = new URL(this.baseUrl + `/shell-descriptors`) + + if (limit) { + url.searchParams.append('limit', limit.toString()); + } + if (cursor) { + url.searchParams.append('cursor', cursor); + } const response = await this.http.fetch>(url, { method: 'GET', diff --git a/src/lib/hooks/UseAasDataLoader.ts b/src/lib/hooks/UseAasDataLoader.ts index c0769cdd0..888b04558 100644 --- a/src/lib/hooks/UseAasDataLoader.ts +++ b/src/lib/hooks/UseAasDataLoader.ts @@ -10,9 +10,15 @@ import { useAasStore } from 'stores/AasStore'; import { performFullAasSearch, performSubmodelSearch, + searchAasInInfrastructure, } from 'lib/services/infrastructure-search-service/infrastructureSearchActions'; +import { + AasSearchResult +} from 'lib/services/infrastructure-search-service/InfrastructureSearchService'; import { getAasFromRepository } from 'lib/services/aas-repository-service/aasRepositoryActions'; import { encodeBase64 } from 'lib/util/Base64Util'; +import { InfrastructureSearchService } from 'lib/services/infrastructure-search-service/InfrastructureSearchService'; +import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper'; /** * Hook to load AAS content and its submodels asynchronously. @@ -91,7 +97,16 @@ export function useAasLoader(context: CurrentAasContextType, aasIdToLoad: string } } - const { isSuccess, result } = await performFullAasSearch(aasIdToLoad); + let response: ApiResponseWrapper | undefined = undefined; + + if (infrastructureName) { + response = await searchAasInInfrastructure(aasIdToLoad, infrastructureName); + } else { + response = await performFullAasSearch(aasIdToLoad); + } + + const { isSuccess, result } = response; + if (!isSuccess) { showError(new LocalizedError('navigation.errors.urlNotFound')); return { success: false }; diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index 3d5eeafa4..338ded36e 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -9,7 +9,7 @@ import { encodeBase64 } from 'lib/util/Base64Util'; import { MultiLanguageValueOnly } from 'lib/api/basyx-v3/types'; import { getInfrastructureByName } from '../database/infrastructureDatabaseActions'; import { createSecurityHeaders } from 'lib/util/securityHelpers/SecurityConfiguration'; -import logger, { logInfo } from 'lib/util/Logger'; +import logger, { logInfo, logWarn } from 'lib/util/Logger'; import { RepositoryWithInfrastructure } from '../database/InfrastructureMappedTypes'; import { IRegistryServiceApi } from 'lib/api/registry-service-api/registryServiceApiInterface'; import { AssetAdministrationShellDescriptor } from 'lib/types/registryServiceTypes'; @@ -36,6 +36,7 @@ export type AasListDto = { export class ListService { private constructor( + protected readonly repositoryWithInfrastructure: RepositoryWithInfrastructure, protected readonly getTargetAasRepositoryClient: () => IAssetAdministrationShellRepositoryApi, protected readonly getTargetSubmodelRepositoryClient: () => ISubmodelRepositoryApi, protected readonly getTargetAasRegistryClient: () => IRegistryServiceApi, @@ -56,6 +57,7 @@ export class ListService { const securityHeader = await createSecurityHeaders(infrastructure || undefined); return new ListService( + targetAasRepository, () => AssetAdministrationShellRepositoryApi.create(targetAasRepository.url, mnestixFetch(securityHeader)), // For now, we only use the same repository. () => SubmodelRepositoryApi.create(targetAasRepository.url, mnestixFetch(securityHeader)), @@ -69,6 +71,10 @@ export class ListService { submodelInRepositories: Submodel[] = [], targetAasRepository = ServiceReachable.Yes, ): ListService { + const repositoryWithInfrastructure = { + infrastructureName: 'null', + url: 'https://targetAasRepositoryClient.com', + }; const targetAasRepositoryClient = AssetAdministrationShellRepositoryApi.createNull( 'https://targetAasRepositoryClient.com', shellsInRepositories, @@ -86,6 +92,7 @@ export class ListService { targetAasRepository, ); return new ListService( + repositoryWithInfrastructure, () => targetAasRepositoryClient, () => targetSubmodelRepositoryClient, () => targetAasRegistryClient, @@ -126,7 +133,14 @@ export class ListService { this.log?.warn(`Descriptor ${descriptor.id} has no endpoints`); return null; } - const endpoint = new URL(descriptor.endpoints[0].protocolInformation.href); + let hrefValue = descriptor.endpoints[0].protocolInformation.href; + if (hrefValue.startsWith("/")) { + const host = new URL(this.repositoryWithInfrastructure.url).origin; + logWarn(this.log, 'getAasListEntities', `Descriptor with id "${descriptor.id}" does not contain a standardconform URL, trying a workaround. Please update your data.`); + hrefValue = host.concat(hrefValue); + } + + const endpoint = new URL(hrefValue); const aasResponse = await targetAasRegistryClient.getAssetAdministrationShellFromEndpoint(endpoint); return aasResponse.isSuccess ? aasResponse.result : null; }); From e82c9eec7f52e974d202a625e6dbd5cf84d147db Mon Sep 17 00:00:00 2001 From: Moritz Hofer Date: Wed, 29 Oct 2025 17:34:54 +0100 Subject: [PATCH 08/12] linting --- src/lib/api/registry-service-api/registryServiceApi.ts | 2 +- src/lib/hooks/UseAasDataLoader.ts | 7 ++----- src/lib/services/list-service/ListService.ts | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lib/api/registry-service-api/registryServiceApi.ts b/src/lib/api/registry-service-api/registryServiceApi.ts index 7cf752990..57c7206b9 100644 --- a/src/lib/api/registry-service-api/registryServiceApi.ts +++ b/src/lib/api/registry-service-api/registryServiceApi.ts @@ -54,7 +54,7 @@ export class RegistryServiceApi implements IRegistryServiceApi { 'Content-Type': 'application/json', }; - let url = new URL(this.baseUrl + `/shell-descriptors`) + const url = new URL(this.baseUrl + '/shell-descriptors') if (limit) { url.searchParams.append('limit', limit.toString()); diff --git a/src/lib/hooks/UseAasDataLoader.ts b/src/lib/hooks/UseAasDataLoader.ts index 888b04558..2ee159f80 100644 --- a/src/lib/hooks/UseAasDataLoader.ts +++ b/src/lib/hooks/UseAasDataLoader.ts @@ -12,12 +12,9 @@ import { performSubmodelSearch, searchAasInInfrastructure, } from 'lib/services/infrastructure-search-service/infrastructureSearchActions'; -import { - AasSearchResult -} from 'lib/services/infrastructure-search-service/InfrastructureSearchService'; +import { AasSearchResult } from 'lib/services/infrastructure-search-service/InfrastructureSearchService'; import { getAasFromRepository } from 'lib/services/aas-repository-service/aasRepositoryActions'; import { encodeBase64 } from 'lib/util/Base64Util'; -import { InfrastructureSearchService } from 'lib/services/infrastructure-search-service/InfrastructureSearchService'; import { ApiResponseWrapper } from 'lib/util/apiResponseWrapper/apiResponseWrapper'; /** @@ -97,7 +94,7 @@ export function useAasLoader(context: CurrentAasContextType, aasIdToLoad: string } } - let response: ApiResponseWrapper | undefined = undefined; + let response: ApiResponseWrapper | undefined = undefined; if (infrastructureName) { response = await searchAasInInfrastructure(aasIdToLoad, infrastructureName); diff --git a/src/lib/services/list-service/ListService.ts b/src/lib/services/list-service/ListService.ts index 338ded36e..2b57b1397 100644 --- a/src/lib/services/list-service/ListService.ts +++ b/src/lib/services/list-service/ListService.ts @@ -134,7 +134,7 @@ export class ListService { return null; } let hrefValue = descriptor.endpoints[0].protocolInformation.href; - if (hrefValue.startsWith("/")) { + if (hrefValue.startsWith('/')) { const host = new URL(this.repositoryWithInfrastructure.url).origin; logWarn(this.log, 'getAasListEntities', `Descriptor with id "${descriptor.id}" does not contain a standardconform URL, trying a workaround. Please update your data.`); hrefValue = host.concat(hrefValue); @@ -214,6 +214,7 @@ export class ListService { ); // The API might return the value directly or wrapped in an object with the property name as key + // eslint-disable-next-line const extractValue = (response: any): MultiLanguageValueOnly | undefined => { if (!response) return undefined; if (Array.isArray(response)) return response; From b50202bcb6e259771cc55b579545abb1d07dca0b Mon Sep 17 00:00:00 2001 From: Moritz Hofer Date: Thu, 30 Oct 2025 10:36:02 +0100 Subject: [PATCH 09/12] Fix tests --- .../_components/AasListDataWrapper.spec.tsx | 38 ++++++++++++++++--- .../filter/SelectRepository.spec.tsx | 8 +++- .../_components/filter/SelectRepository.tsx | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx index 4ae3952c0..d560ce4a0 100644 --- a/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx +++ b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx @@ -9,6 +9,7 @@ import { ListEntityDto } from 'lib/services/list-service/ListService'; import { Internationalization } from 'lib/i18n/Internationalization'; import { AasStoreProvider } from 'stores/AasStore'; import { RepositoryWithInfrastructure } from 'lib/services/database/InfrastructureMappedTypes'; +import { ConnectionWithType } from 'app/[locale]/list/_components/filter/SelectRepository'; jest.mock('./../../../../lib/services/list-service/aasListApiActions'); jest.mock('./../../../../lib/services/database/infrastructureDatabaseActions'); @@ -35,6 +36,12 @@ jest.mock('next-auth', jest.fn()); const REPOSITORY_URL = 'https://test-repository.de'; const FIRST_PAGE_CURSOR = '123123'; const REPOSITORY = { url: REPOSITORY_URL, infrastructureName: 'Test Infrastructure', id: 'test-repo-id' }; +const REPOSITORY_CONNECTION_TYPE = { + url: REPOSITORY.url, + infrastructureName: REPOSITORY.infrastructureName, + id: REPOSITORY.id, + type: 'repository', +}; const mockDB = jest.fn(() => { return [REPOSITORY]; }); @@ -56,7 +63,12 @@ const createTestListEntries = (from = 0, to = 10): ListEntityDto[] => { }; const mockActionFirstPage = jest.fn( - (_repository: RepositoryWithInfrastructure, _count: number, _cursor: string | undefined) => { + ( + _repository: RepositoryWithInfrastructure | ConnectionWithType, + _count: number, + _cursor: string | undefined, + _type: string | undefined, + ) => { return { success: true, entities: createTestListEntries(0, 10), @@ -67,7 +79,12 @@ const mockActionFirstPage = jest.fn( ); const mockActionSecondPage = jest.fn( - (_repository: RepositoryWithInfrastructure, _count: number, _cursor: string | undefined) => { + ( + _repository: RepositoryWithInfrastructure, + _count: number, + _cursor: string | undefined, + _type: string | undefined, + ) => { return { success: true, entities: createTestListEntries(10, 12), @@ -90,6 +107,11 @@ describe('AASListDataWrapper', () => { (serverActions.getAasListEntities as jest.Mock).mockImplementation(mockActionFirstPage); (connectionServerActions.getAasRepositoriesIncludingDefault as jest.Mock).mockImplementation(mockDB); + (connectionServerActions.getAasRegistriesIncludingDefault as jest.Mock).mockImplementation( + jest.fn(() => { + return []; + }), + ); (nameplateDataActions.getNameplateValuesForAAS as jest.Mock).mockImplementation(mockNameplateData); CustomRender( @@ -109,7 +131,8 @@ describe('AASListDataWrapper', () => { await waitFor(() => screen.getByTestId('repository-select')); const select = screen.getAllByRole('combobox')[0]; fireEvent.mouseDown(select); - const firstElement = screen.getAllByRole('option')[0]; + // const firstElement = screen.getAllByRole('option')[0]; + const firstElement = screen.getByTestId('repository-select-item-0'); fireEvent.click(firstElement); await waitFor(() => screen.getByTestId('list-next-button')); @@ -130,7 +153,12 @@ describe('AASListDataWrapper', () => { expect(screen.getByText('assetId10', { exact: false })).toBeInTheDocument(); expect(screen.getByText('Page 2', { exact: false })).toBeInTheDocument(); expect(screen.getByTestId('list-next-button')).toBeDisabled(); - expect(mockActionSecondPage).toHaveBeenCalledWith(REPOSITORY, 10, FIRST_PAGE_CURSOR); + expect(mockActionSecondPage).toHaveBeenCalledWith( + REPOSITORY_CONNECTION_TYPE, + 10, + FIRST_PAGE_CURSOR, + 'repository', + ); }); it('Navigates one page back when clicking on the back button', async () => { @@ -146,7 +174,7 @@ describe('AASListDataWrapper', () => { expect(screen.getByText('assetId3', { exact: false })).toBeInTheDocument(); expect(screen.getByText('Page 1', { exact: false })).toBeInTheDocument(); - expect(mockActionFirstPage).toHaveBeenCalledWith(REPOSITORY, 10, undefined); + expect(mockActionFirstPage).toHaveBeenCalledWith(REPOSITORY_CONNECTION_TYPE, 10, undefined, 'repository'); }); }); }); diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx b/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx index 97941bad6..305b49162 100644 --- a/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx +++ b/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx @@ -19,6 +19,11 @@ describe('SelectRepository', () => { }); const repositoryChanged = jest.fn(); (connectionServerActions.getAasRepositoriesIncludingDefault as jest.Mock).mockImplementation(mockDB); + (connectionServerActions.getAasRegistriesIncludingDefault as jest.Mock).mockImplementation( + jest.fn(() => { + return []; + }), + ); CustomRender( { @@ -31,7 +36,8 @@ describe('SelectRepository', () => { const select = screen.getByRole('combobox'); fireEvent.mouseDown(select); - const firstElement = screen.getAllByRole('option')[0]; + // const firstElement = screen.getAllByRole('option')[0]; + const firstElement = screen.getByTestId('repository-select-item-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 index 71d06fae7..97fb86519 100644 --- a/src/app/[locale]/list/_components/filter/SelectRepository.tsx +++ b/src/app/[locale]/list/_components/filter/SelectRepository.tsx @@ -19,7 +19,7 @@ import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner'; import { useTranslations } from 'next-intl'; import { RepositoryWithInfrastructure } from 'lib/services/database/InfrastructureMappedTypes'; -type ConnectionWithType = RepositoryWithInfrastructure & { type: 'repository' | 'registry' }; +export type ConnectionWithType = RepositoryWithInfrastructure & { type: 'repository' | 'registry' }; export function SelectRepository(props: { onSelectedRepositoryChanged: Dispatch>; From 917f17baece0c4b38a6405067bca0cd477b33521 Mon Sep 17 00:00:00 2001 From: Moritz Hofer Date: Thu, 30 Oct 2025 10:36:18 +0100 Subject: [PATCH 10/12] Add debug configuration for JEST Unit tests --- .vscode/launch.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index ecbf4b4b5..0197141b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,6 +45,20 @@ "sourceMapPathOverrides": { "/turbopack/[project]/*": "${webRoot}/*" } + }, + { + "name": "Debug Jest file", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": ["${fileBasenameNoExtension}", "--runInBand", "--watch", "--coverage=false", "--no-cache"], + "cwd": "${workspaceRoot}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "sourceMaps": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } } ] } From ff699ad873c295538071b72c45fd84dcbec3e49c Mon Sep 17 00:00:00 2001 From: Moritz Hofer Date: Fri, 31 Oct 2025 10:58:42 +0100 Subject: [PATCH 11/12] Add comment regarding the JSON handling in API Response Wrapper --- src/lib/util/apiResponseWrapper/apiResponseWrapper.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts index 815b7ca30..a39143e1d 100644 --- a/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts +++ b/src/lib/util/apiResponseWrapper/apiResponseWrapper.ts @@ -103,6 +103,17 @@ export async function wrapResponse(response: Response): Promise Date: Fri, 31 Oct 2025 11:11:36 +0100 Subject: [PATCH 12/12] Rename SelectRepository in SelectListSource --- .../[locale]/list/_components/AasListDataWrapper.spec.tsx | 2 +- src/app/[locale]/list/_components/AasListDataWrapper.tsx | 6 +++--- ...{SelectRepository.spec.tsx => SelectListSource.spec.tsx} | 4 ++-- .../filter/{SelectRepository.tsx => SelectListSource.tsx} | 2 +- src/locale/de.json | 2 +- src/locale/en.json | 2 +- src/locale/es.json | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename src/app/[locale]/list/_components/filter/{SelectRepository.spec.tsx => SelectListSource.spec.tsx} (93%) rename src/app/[locale]/list/_components/filter/{SelectRepository.tsx => SelectListSource.tsx} (99%) diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx index d560ce4a0..d3da08e12 100644 --- a/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx +++ b/src/app/[locale]/list/_components/AasListDataWrapper.spec.tsx @@ -9,7 +9,7 @@ import { ListEntityDto } from 'lib/services/list-service/ListService'; import { Internationalization } from 'lib/i18n/Internationalization'; import { AasStoreProvider } from 'stores/AasStore'; import { RepositoryWithInfrastructure } from 'lib/services/database/InfrastructureMappedTypes'; -import { ConnectionWithType } from 'app/[locale]/list/_components/filter/SelectRepository'; +import { ConnectionWithType } from 'app/[locale]/list/_components/filter/SelectListSource'; jest.mock('./../../../../lib/services/list-service/aasListApiActions'); jest.mock('./../../../../lib/services/database/infrastructureDatabaseActions'); diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.tsx index 4484f5992..011a1fe06 100644 --- a/src/app/[locale]/list/_components/AasListDataWrapper.tsx +++ b/src/app/[locale]/list/_components/AasListDataWrapper.tsx @@ -10,7 +10,7 @@ import { AasListComparisonHeader } from './AasListComparisonHeader'; import { Box, Card, CardContent, 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 { SelectListSource } from './filter/SelectListSource'; import { useTranslations } from 'next-intl'; import { ApiResponseWrapperError } from 'lib/util/apiResponseWrapper/apiResponseWrapper'; import { AuthenticationPrompt } from 'components/authentication/AuthenticationPrompt'; @@ -137,7 +137,7 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra if (!selectedRepository) { return ( - {t('selectRepository')} + {t('selectListSource')} ); } @@ -168,7 +168,7 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra {!hideRepoSelection && ( - diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx b/src/app/[locale]/list/_components/filter/SelectListSource.spec.tsx similarity index 93% rename from src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx rename to src/app/[locale]/list/_components/filter/SelectListSource.spec.tsx index 305b49162..7a1bb5906 100644 --- a/src/app/[locale]/list/_components/filter/SelectRepository.spec.tsx +++ b/src/app/[locale]/list/_components/filter/SelectListSource.spec.tsx @@ -1,7 +1,7 @@ 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 { SelectListSource } from 'app/[locale]/list/_components/filter/SelectListSource'; import * as connectionServerActions from 'lib/services/database/infrastructureDatabaseActions'; jest.mock('./../../../../../lib/services/database/infrastructureDatabaseActions'); @@ -25,7 +25,7 @@ describe('SelectRepository', () => { }), ); CustomRender( - { repositoryChanged(); }} diff --git a/src/app/[locale]/list/_components/filter/SelectRepository.tsx b/src/app/[locale]/list/_components/filter/SelectListSource.tsx similarity index 99% rename from src/app/[locale]/list/_components/filter/SelectRepository.tsx rename to src/app/[locale]/list/_components/filter/SelectListSource.tsx index 97fb86519..e87f85f50 100644 --- a/src/app/[locale]/list/_components/filter/SelectRepository.tsx +++ b/src/app/[locale]/list/_components/filter/SelectListSource.tsx @@ -21,7 +21,7 @@ import { RepositoryWithInfrastructure } from 'lib/services/database/Infrastructu export type ConnectionWithType = RepositoryWithInfrastructure & { type: 'repository' | 'registry' }; -export function SelectRepository(props: { +export function SelectListSource(props: { onSelectedRepositoryChanged: Dispatch>; onSelectedTypeChanged?: Dispatch>; }) { diff --git a/src/locale/de.json b/src/locale/de.json index 2adea091e..ca9f3b66a 100644 --- a/src/locale/de.json +++ b/src/locale/de.json @@ -212,7 +212,7 @@ "maxElementsWarning": "Maximal 3 Elemente selektierbar", "page": "Seite", "repositoryDropdownLabel": "AAS Repository oder AAS Registry (Infrastruktur)", - "selectRepository": "Bitte wählen Sie ein Repository oder AAS Registry aus", + "selectListSource": "Bitte wählen Sie ein Repository oder AAS Registry aus", "subHeader": "Hier finden Sie alle AAS aus Ihrem Repository oder AAS Registry. Sie können ein AAS-Repository oder AAS Registry auswählen, um zwischen den Datenquellen zu wechseln.", "titleComparisonAddButton": "AAS zum Vergleich hinzufügen" }, diff --git a/src/locale/en.json b/src/locale/en.json index f97795fdc..32a392861 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -212,7 +212,7 @@ "maxElementsWarning": "Cannot compare more than 3 elements", "page": "Page", "repositoryDropdownLabel": "AAS Repository or AAS Registry (Infrastructure)", - "selectRepository": "Please select a repository or AAS registry", + "selectListSource": "Please select a repository or AAS registry", "subHeader": "You will find all AAS from your repository or AAS registry here. You can select an AAS repository or AAS registry to switch between data sources.", "titleComparisonAddButton": "Add AAS to comparison" }, diff --git a/src/locale/es.json b/src/locale/es.json index 0021190fd..dda193072 100644 --- a/src/locale/es.json +++ b/src/locale/es.json @@ -212,7 +212,7 @@ "maxElementsWarning": "No se pueden comparar más de 3 elementos", "page": "Página", "repositoryDropdownLabel": "AAS repository o AAS registry (Infrastructure)", - "selectRepository": "Por favor, seleccione un repositorio o AAS registry", + "selectListSource": "Por favor, seleccione un repositorio o AAS registry", "subHeader": "Todos los AAS de su repositorio o AAS registry se muestran aquí. Puede seleccionar otro repositorio o AAS registry para cambiar la fuente de los datos.", "titleComparisonAddButton": "Añadir AAS a la comparación" },