diff --git a/package.json b/package.json index 68f4bab0b..04816ef23 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,13 @@ "@fontsource/noto-sans": "^5.2.10", "@fontsource/saira": "^5.2.10", "@influxdata/influxdb-client-browser": "^1.33.2", - "@mui/icons-material": "^7.3.7", - "@mui/material": "^7.3.7", - "@mui/material-nextjs": "^7.3.7", - "@mui/styled-engine": "^7.3.7", - "@mui/system": "^7.1.0", - "@mui/x-date-pickers": "^8.26.0", - "@mui/x-tree-view": "^8.10.2", + "@mui/icons-material": "^9.0.0", + "@mui/material": "^9.0.0", + "@mui/material-nextjs": "^9.0.0", + "@mui/styled-engine": "^9.0.0", + "@mui/system": "^9.0.0", + "@mui/x-date-pickers": "^9.0.2", + "@mui/x-tree-view": "^9.0.2", "@prisma/adapter-better-sqlite3": "^7.8.0", "@prisma/client": "^7.8.0", "@svgr/webpack": "^8.1.0", diff --git a/src/app/[locale]/_components/FindOutMoreCard.tsx b/src/app/[locale]/_components/FindOutMoreCard.tsx index 5535620d9..b203a6366 100644 --- a/src/app/[locale]/_components/FindOutMoreCard.tsx +++ b/src/app/[locale]/_components/FindOutMoreCard.tsx @@ -10,13 +10,18 @@ export function FindOutMoreCard() { return ( + sx={{ + display: 'flex', + flex: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-start', + px: 3, + py: 4, + gap: 1, + height: '100%', + width: '100%' + }}> {t('findOutMoreHeader')} - + {t('findOutMoreText')} diff --git a/src/app/[locale]/_components/GoToListCard.tsx b/src/app/[locale]/_components/GoToListCard.tsx index a693fcfb6..1dc22f0fe 100644 --- a/src/app/[locale]/_components/GoToListCard.tsx +++ b/src/app/[locale]/_components/GoToListCard.tsx @@ -9,13 +9,18 @@ export function GoToListCard() { return ( + sx={{ + display: 'flex', + flex: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-start', + px: 3, + py: 4, + gap: 1, + height: '100%', + width: '100%' + }}> - + {t('listCardHeader')} - + {t('listBtnLabel')} diff --git a/src/app/[locale]/_components/ManualAasInput.tsx b/src/app/[locale]/_components/ManualAasInput.tsx index bad7ec85f..feeab776e 100644 --- a/src/app/[locale]/_components/ManualAasInput.tsx +++ b/src/app/[locale]/_components/ManualAasInput.tsx @@ -76,7 +76,13 @@ export function ManualAasInput(props: { }; return ( - + {t('pages.dashboard.enterManuallyLabel')} - + - { - setInputValue(''); - }} - > - - - - ), + slotProps={{ + input: { + endAdornment: ( + + { + setInputValue(''); + }} + > + + + + ), + } }} /> + sx={{ + position: 'relative', + margin: 'auto', + height: size, + width: size + }}> {state === State.Stopped && ( setState(State.LoadScanner)} - display="flex" - flexDirection="column" - alignItems="center" - justifyContent="center" - height={size} - width={size} - gap={1} - color="white" data-testid="scanner-start" - > + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: size, + width: size, + gap: 1, + color: 'white' + }}> Scan Code - + @@ -145,7 +149,13 @@ export function QrScanner(props: { )} {(state === State.LoadScanner || state === State.HandleQr) && ( - + diff --git a/src/app/[locale]/clientLayout.tsx b/src/app/[locale]/clientLayout.tsx index 66130daa2..8a4270228 100644 --- a/src/app/[locale]/clientLayout.tsx +++ b/src/app/[locale]/clientLayout.tsx @@ -36,7 +36,9 @@ export const ClientLayout = ({ children }: Readonly) => { - + {children} diff --git a/src/app/[locale]/compare/_components/CompareSubmodelElement.tsx b/src/app/[locale]/compare/_components/CompareSubmodelElement.tsx index aba0a3620..f904070f6 100644 --- a/src/app/[locale]/compare/_components/CompareSubmodelElement.tsx +++ b/src/app/[locale]/compare/_components/CompareSubmodelElement.tsx @@ -26,9 +26,15 @@ export function CompareSubmodelElement(props: CompareSubmodelElementProps) { case KeyTypes.Property: return ( <> - + {isMarked && } - + @@ -43,9 +49,15 @@ export function CompareSubmodelElement(props: CompareSubmodelElementProps) { case KeyTypes.MultiLanguageProperty: return ( <> - + {isMarked && } - + diff --git a/src/app/[locale]/compare/_components/CompareView.tsx b/src/app/[locale]/compare/_components/CompareView.tsx index 319f6c97b..f7c38e2b0 100644 --- a/src/app/[locale]/compare/_components/CompareView.tsx +++ b/src/app/[locale]/compare/_components/CompareView.tsx @@ -78,15 +78,41 @@ export function CompareView() { return ( <> - - + + {t('title')} {compareAas.length !== 0 || isLoadingAas ? ( - - + + {compareAas.map((compareAas, index) => ( - + handleDeleteAas(compareAas.aas.id)} dataTestId={`delete-compare-aas-${index}`} @@ -104,7 +130,9 @@ export function CompareView() { ))} {compareAas.length < 3 && } - + diff --git a/src/app/[locale]/compare/_components/add-aas/AddAasToCompareCard.tsx b/src/app/[locale]/compare/_components/add-aas/AddAasToCompareCard.tsx index 860fb1f36..c55f655f3 100644 --- a/src/app/[locale]/compare/_components/add-aas/AddAasToCompareCard.tsx +++ b/src/app/[locale]/compare/_components/add-aas/AddAasToCompareCard.tsx @@ -15,20 +15,31 @@ export function AddAasToCompareCard(props: AddAasToCompareCardProps) { return ( + sx={{ + width: '33%', + display: 'flex', + cursor: 'pointer' + }}> {isFirst ? ( - + {t('addFirstAasButton')} ) : ( - + {t('addButton')} )} diff --git a/src/app/[locale]/compare/_components/add-aas/CompareAasAddDialog.tsx b/src/app/[locale]/compare/_components/add-aas/CompareAasAddDialog.tsx index e5d7b415e..3a3b3c1cc 100644 --- a/src/app/[locale]/compare/_components/add-aas/CompareAasAddDialog.tsx +++ b/src/app/[locale]/compare/_components/add-aas/CompareAasAddDialog.tsx @@ -23,21 +23,43 @@ export function CompareAasAddDialog(props: AddAasModalProps) { > - - + + {t('compare.addAnother')}: - + {t('dashboard.scanIdLabel')} - + {t('dashboard.enterManuallyLabel')} - + diff --git a/src/app/[locale]/compare/_components/add-aas/CompareRecordValueRow.tsx b/src/app/[locale]/compare/_components/add-aas/CompareRecordValueRow.tsx index 23fc0cc1d..89d1b88a1 100644 --- a/src/app/[locale]/compare/_components/add-aas/CompareRecordValueRow.tsx +++ b/src/app/[locale]/compare/_components/add-aas/CompareRecordValueRow.tsx @@ -12,7 +12,13 @@ export function CompareRecordValueRow(props: { data: SubmodelCompareDataRecord; const markedIndexes = compareRowValues(dataRecord.submodelElements, locale); return ( - + {dataRecord.submodelElements?.map((subElement, valueIndex) => { return ( + sx={{ + letterSpacing: 0.16, + fontWeight: 700 + }}> {header.label} diff --git a/src/app/[locale]/list/_components/AasListComparisonHeader.tsx b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx index 51e1489b6..5c20b001c 100644 --- a/src/app/[locale]/list/_components/AasListComparisonHeader.tsx +++ b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx @@ -24,9 +24,21 @@ export const AasListComparisonHeader = (props: CompareAasListBarType) => { return ( <> - + {selectedAasList?.map((selectedAas) => ( - + {tooltipText(selectedAas, 15)} updateSelectedAasList(false, selectedAas)}> diff --git a/src/app/[locale]/list/_components/AasListDataWrapper.tsx b/src/app/[locale]/list/_components/AasListDataWrapper.tsx index 10a58e91f..66ed63789 100644 --- a/src/app/[locale]/list/_components/AasListDataWrapper.tsx +++ b/src/app/[locale]/list/_components/AasListDataWrapper.tsx @@ -118,8 +118,18 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra }; const pagination = ( - - + + {t('page') + ' ' + (currentPage + 1)} @@ -135,8 +145,18 @@ export default function AasListDataWrapper({ hideRepoSelection }: AasListDataWra {!hideRepoSelection && ( - - + + - + {t('header')} diff --git a/src/app/[locale]/list/_components/filter/SelectListSource.tsx b/src/app/[locale]/list/_components/filter/SelectListSource.tsx index 7d51d205f..16acdc894 100644 --- a/src/app/[locale]/list/_components/filter/SelectListSource.tsx +++ b/src/app/[locale]/list/_components/filter/SelectListSource.tsx @@ -100,9 +100,10 @@ export function SelectListSource(props: { + sx={{ + color: 'text.secondary', + ml: '5px' + }}> ({conn.infrastructureName}) @@ -121,9 +122,10 @@ export function SelectListSource(props: { + sx={{ + color: 'text.secondary', + ml: '5px' + }}> ({conn.infrastructureName}) diff --git a/src/app/[locale]/list/page.tsx b/src/app/[locale]/list/page.tsx index 441e8fae7..f2ed531c5 100644 --- a/src/app/[locale]/list/page.tsx +++ b/src/app/[locale]/list/page.tsx @@ -8,9 +8,24 @@ import { useTranslations } from 'next-intl'; export default function Page() { const t = useTranslations('pages.aasList'); return ( - - - + + + diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 1d5601ed6..71ddb2dff 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -72,7 +72,9 @@ export default function () { {t('digitalTwinMadeEasyText')} - + diff --git a/src/app/[locale]/product/_components/KeyFactsBox.tsx b/src/app/[locale]/product/_components/KeyFactsBox.tsx index 063224524..786bd83ee 100644 --- a/src/app/[locale]/product/_components/KeyFactsBox.tsx +++ b/src/app/[locale]/product/_components/KeyFactsBox.tsx @@ -55,9 +55,15 @@ export function KeyFactsBox({ return ( - + - + {t('summary')} diff --git a/src/app/[locale]/product/_components/ProductClassificationInfoBox.tsx b/src/app/[locale]/product/_components/ProductClassificationInfoBox.tsx index 51a800600..979449115 100644 --- a/src/app/[locale]/product/_components/ProductClassificationInfoBox.tsx +++ b/src/app/[locale]/product/_components/ProductClassificationInfoBox.tsx @@ -56,13 +56,15 @@ export function ProductClassificationInfoBox({ productClassifications }: Product return ( - + + sx={{ + fontWeight: 'bold', + borderBottom: '2px solid', + borderColor: 'primary' + }}> {t('summary')} @@ -73,7 +75,9 @@ export function ProductClassificationInfoBox({ productClassifications }: Product label={classification.ProductClassificationSystem || 'Classification'} icon={classification.ProductClassificationSystem === 'ECLASS' ? eClassIcon : vecIcon} > - + {tooltipText(classification.ProductClassId, 35) || '-'} diff --git a/src/app/[locale]/product/_components/ProductOverviewCard.tsx b/src/app/[locale]/product/_components/ProductOverviewCard.tsx index 915da0487..af53f0ce7 100644 --- a/src/app/[locale]/product/_components/ProductOverviewCard.tsx +++ b/src/app/[locale]/product/_components/ProductOverviewCard.tsx @@ -1,525 +1,531 @@ -import { Box, Card, CardContent, Skeleton, Typography, Tooltip, Divider } from '@mui/material'; -import React, { useMemo, useCallback } from 'react'; -import { DataRow } from 'components/basics/DataRow'; -import { IconCircleWrapper } from 'components/basics/IconCircleWrapper'; -import { AssetIcon } from 'components/custom-icons/AssetIcon'; -import { encodeBase64 } from 'lib/util/Base64Util'; -import { useRouter } from 'next/navigation'; -import { SubmodelOrIdReference } from 'components/contexts/CurrentAasContext'; -import { ImageWithFallback } from 'components/basics/StyledImageWithFallBack'; -import { useTranslations } from 'next-intl'; -import { SubmodelSemanticIdEnum } from 'lib/enums/SubmodelSemanticId.enum'; -import { findSubmodelByIdOrSemanticId, findSubmodelElementByIdShort } from 'lib/util/SubmodelResolverUtil'; -import { MobileAccordion } from 'components/basics/detailViewBasics/MobileAccordion'; -import { KeyFactsBox } from 'app/[locale]/product/_components/KeyFactsBox'; -import { SubmodelElementSemanticIdEnum } from 'lib/enums/SubmodelElementSemanticId.enum'; -import { useProductImageUrl } from 'lib/hooks/UseProductImageUrl'; -import { useFindValueByIdShort } from 'lib/hooks/useFindValueByIdShort'; -import { ActionMenu } from './ProductActionMenu'; -import LinkIcon from '@mui/icons-material/Link'; -import { MnestixConnection } from '../../../../../prisma/generated/client'; -import { - AssetAdministrationShell, - Property, - SubmodelElementChoice, - SubmodelElementCollection, -} from 'lib/api/aas/models'; -import { useAasStore } from 'stores/AasStore'; - -type ProductOverviewCardProps = { - readonly aas: AssetAdministrationShell | null; - readonly submodels: SubmodelOrIdReference[] | null; - readonly productImage?: string; - readonly isLoading?: boolean; - readonly isAccordion: boolean; - readonly imageLinksToDetail?: boolean; - readonly repositoryURL?: string; - readonly displayName: string | null; - readonly catalogConfig?: MnestixConnection; - readonly infrastructureName?: string; -}; - -type ProductClassification = { - ProductClassificationSystem?: string; - ProductClassId?: string; -}; - -type OverviewData = { - readonly manufacturerName?: string; - readonly manufacturerProductDesignation?: string; - readonly manufacturerProductRoot?: string; - readonly manufacturerProductFamily?: string; - readonly manufacturerProductType?: string; - readonly manufacturerArticleNumber?: string; - readonly manufacturerOrderCode?: string; - readonly manufacturerLogo: SubmodelElementChoice | null; - readonly companyLogo: SubmodelElementChoice | null; - readonly markings: string[] | null; - readonly productClassifications?: ProductClassification[]; - readonly URIOfTheProduct?: string | null; -}; - -export function ProductOverviewCard(props: ProductOverviewCardProps) { - const isAccordion = props.isAccordion; - const navigate = useRouter(); - const t = useTranslations('pages.productViewer'); - const findValue = useFindValueByIdShort(); - const productImageUrl = useProductImageUrl(props.aas, props.repositoryURL ?? undefined, props.productImage); - const { addAasData } = useAasStore(); - - const prepareTechnicalDataSubmodel = useCallback( - (technicalDataSubmodelElements: Array): Partial => { - const manufacturerName = findValue( - technicalDataSubmodelElements, - 'ManufacturerName', - SubmodelElementSemanticIdEnum.ManufacturerName, - ); - const manufacturerProductDesignation = findValue( - technicalDataSubmodelElements, - 'ManufacturerProductDesignation', - SubmodelElementSemanticIdEnum.ManufacturerProductDesignation, - ); - const manufacturerArticleNumber = findValue( - technicalDataSubmodelElements, - 'ManufacturerArticleNumber', - SubmodelElementSemanticIdEnum.ManufacturerArticleNumber, - ); - const manufacturerOrderCode = findValue( - technicalDataSubmodelElements, - 'ManufacturerOrderCode', - SubmodelElementSemanticIdEnum.ManufacturerOrderCode, - ); - const manufacturerLogo = findSubmodelElementByIdShort( - technicalDataSubmodelElements, - 'ManufacturerLogo', - SubmodelElementSemanticIdEnum.ManufacturerLogo, - ); - const productClassifications = findSubmodelElementByIdShort( - technicalDataSubmodelElements, - 'ProductClassifications', - SubmodelElementSemanticIdEnum.ProductClassifications, - ) as SubmodelElementCollection; - const classifications: ProductClassification[] = []; - - // If ProductClassifications is a flat list, we can directly use it - // This is not standard conform but we support it for now. - if ( - productClassifications?.value && - productClassifications.value.find((element) => element.idShort === 'ProductClassificationSystem') - ) { - const classification = { - ProductClassificationSystem: - findValue( - productClassifications.value, - 'ProductClassificationSystem', - SubmodelElementSemanticIdEnum.ProductClassificationSystem, - ) || undefined, - ProductClassId: - findValue( - productClassifications.value, - 'ProductClassId', - SubmodelElementSemanticIdEnum.ProductClassId, - ) || undefined, - }; - classifications.push(classification); - } else { - productClassifications?.value?.forEach((productClassification) => { - const submodelClassification = productClassification as SubmodelElementCollection; - if (submodelClassification?.value) { - const classification = { - ProductClassificationSystem: - findValue( - submodelClassification.value, - 'ProductClassificationSystem', - SubmodelElementSemanticIdEnum.ProductClassificationSystem, - ) || undefined, - ProductClassId: - findValue( - submodelClassification.value, - 'ProductClassId', - SubmodelElementSemanticIdEnum.ProductClassId, - ) || undefined, - }; - // Filter out classifications without a ProductClassId - if (!classification.ProductClassId) { - return; - } - classifications.push(classification); - } - }); - } - return { - manufacturerName: !manufacturerName || manufacturerName.trim() === '' ? undefined : manufacturerName, - manufacturerProductDesignation: - !manufacturerProductDesignation || manufacturerProductDesignation.trim() === '' - ? undefined - : manufacturerProductDesignation, - productClassifications: classifications, - manufacturerArticleNumber: - !manufacturerArticleNumber || manufacturerArticleNumber.trim() === '' - ? undefined - : manufacturerArticleNumber, - manufacturerOrderCode: - !manufacturerOrderCode || manufacturerOrderCode.trim() === '' ? undefined : manufacturerOrderCode, - companyLogo: null, - markings: null, - manufacturerLogo: manufacturerLogo, - }; - }, - [findValue], - ); - - /** - * Prepare marking texts from the SubmodelElementCollection by extracting the 'MarkingName' properties. - * @param markings - */ - const prepareMarkingTexts = (markings: SubmodelElementCollection | null): string[] => { - if (!markings?.value) return []; - - const result: string[] = []; - markings.value.forEach((el) => { - Object.values(el || {}).forEach((marking) => { - Object.values(marking || {}).forEach((markingProperty: SubmodelElementChoice) => { - if (markingProperty?.idShort === 'MarkingName' && (markingProperty as Property).value) { - result.push((markingProperty as Property).value || ''); - } - }); - }); - }); - return result; - }; - - const prepareNameplateData = useCallback( - ( - nameplateSubmodelElements: Array, - prevData?: Partial, - ): Partial => { - const manufacturerProductRoot = findValue( - nameplateSubmodelElements, - 'ManufacturerProductRoot', - SubmodelElementSemanticIdEnum.ManufacturerProductRoot, - ); - const manufacturerProductFamily = findValue( - nameplateSubmodelElements, - 'ManufacturerProductFamily', - SubmodelElementSemanticIdEnum.ManufacturerProductFamily, - ); - const manufacturerProductType = findValue( - nameplateSubmodelElements, - 'ManufacturerProductType', - SubmodelElementSemanticIdEnum.ManufacturerProductType, - ); - const markingsElement = findSubmodelElementByIdShort( - nameplateSubmodelElements, - 'Markings', - SubmodelElementSemanticIdEnum.MarkingsV3, - ) as SubmodelElementCollection; - - const markings = prepareMarkingTexts(markingsElement || null); - - const companyLogo = findSubmodelElementByIdShort( - nameplateSubmodelElements, - 'CompanyLogo', - SubmodelElementSemanticIdEnum.CompanyLogo, - ); - const URIOfTheProduct = findValue(nameplateSubmodelElements, 'URIOfTheProducts', [ - SubmodelElementSemanticIdEnum.URIOfTheProductV2, - SubmodelElementSemanticIdEnum.URIOfTheProductV3, - ]); - const manufacturerName = findValue( - nameplateSubmodelElements, - 'ManufacturerName', - SubmodelElementSemanticIdEnum.ManufacturerName, - ); - const manufacturerProductDesignation = findValue( - nameplateSubmodelElements, - 'ManufacturerProductDesignation', - SubmodelElementSemanticIdEnum.ManufacturerProductDesignation, - ); - const manufacturerArticleNumber = findValue( - nameplateSubmodelElements, - 'ProductArticleNumberOfManufacturer', - SubmodelElementSemanticIdEnum.ManufacturerArticleNumber, - ); - const manufacturerOrderCode = findValue( - nameplateSubmodelElements, - 'OrderCodeOfManufacturer', - SubmodelElementSemanticIdEnum.ManufacturerOrderCode, - ); - return { - ...prevData, - manufacturerName: prevData?.manufacturerName ? prevData.manufacturerName : (manufacturerName ?? '-'), - manufacturerProductDesignation: prevData?.manufacturerProductDesignation - ? prevData.manufacturerProductDesignation - : (manufacturerProductDesignation ?? '-'), - manufacturerArticleNumber: prevData?.manufacturerArticleNumber - ? prevData.manufacturerArticleNumber - : (manufacturerArticleNumber ?? '-'), - manufacturerOrderCode: prevData?.manufacturerOrderCode - ? prevData.manufacturerOrderCode - : (manufacturerOrderCode ?? '-'), - manufacturerProductRoot: manufacturerProductRoot ?? '-', - manufacturerProductFamily: manufacturerProductFamily ?? '-', - manufacturerProductType: manufacturerProductType ?? '-', - markings: markings, - manufacturerLogo: prevData?.manufacturerLogo || null, - companyLogo: companyLogo || null, - URIOfTheProduct: URIOfTheProduct || null, - }; - }, - [findValue, prepareMarkingTexts], - ); - - const overviewData = useMemo((): OverviewData | undefined => { - if (!props.submodels || props.submodels.length === 0) return undefined; - - const technicalData = findSubmodelByIdOrSemanticId( - props.submodels, - SubmodelSemanticIdEnum.TechnicalDataV11, - 'TechnicalData', - ); - let data: Partial | undefined; - if (technicalData?.submodelElements) { - data = prepareTechnicalDataSubmodel(technicalData.submodelElements); - } - - const nameplate = findSubmodelByIdOrSemanticId( - props.submodels, - SubmodelSemanticIdEnum.NameplateV2, - 'Nameplate', - ); - if (nameplate?.submodelElements) { - data = prepareNameplateData(nameplate.submodelElements, data); - } - - return data as OverviewData | undefined; - }, [props.submodels, prepareTechnicalDataSubmodel, prepareNameplateData]); - - const infoBoxStyle = { - display: 'flex', - flexDirection: 'column', - width: '100%', - flexGrow: '1', - flexBasis: '0', - }; - - const titleStyle = { - marginBottom: '15px', - }; - - const cardContentStyle = { - display: 'flex', - alignItems: isAccordion ? 'center' : 'unset', - gap: isAccordion ? '10px' : '40px', - flexDirection: isAccordion ? 'column' : 'row', - }; - - const navigateToAas = () => { - if (props.imageLinksToDetail && props.aas && props.infrastructureName) { - addAasData({ - aas: props.aas, - aasData: { - aasRepositoryOrigin: props.repositoryURL, - submodelDescriptors: undefined, - infrastructureName: props.infrastructureName, - }, - }); - const url = `/product/${encodeBase64(props.aas.id)}`; - navigate.push(url); - } - }; - - const navigateToProduct = () => { - if (overviewData?.URIOfTheProduct) { - const url = overviewData.URIOfTheProduct; - window.open(url, '_blank', 'noopener,noreferrer'); - } - }; - - const productInfo = ( - - - - - - - - - - - ); - - const showKeyFactsBox = - (overviewData?.productClassifications && overviewData.productClassifications.length > 0) || - (overviewData?.markings && overviewData.markings.length > 0); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const classificationInfo = ( - - {!isAccordion && ( - - - - - - {t('classification')} - - - )} - - - - - ); - - return ( - - - {props.isLoading ? ( - <> - - - {isAccordion ? ( - - - - - ) : ( - <> - - - - - - - - - - - - - - )} - - - ) : ( - <> - - {isAccordion ? ( - - ) : ( - <> - - - - {props.displayName || t('title')} - {overviewData?.URIOfTheProduct && ( - - - - )} - - - - - {productInfo} - - - )} - - )} - - {showKeyFactsBox && ( - - )} - - ); -} +import { Box, Card, CardContent, Skeleton, Typography, Tooltip, Divider } from '@mui/material'; +import React, { useMemo, useCallback } from 'react'; +import { DataRow } from 'components/basics/DataRow'; +import { IconCircleWrapper } from 'components/basics/IconCircleWrapper'; +import { AssetIcon } from 'components/custom-icons/AssetIcon'; +import { encodeBase64 } from 'lib/util/Base64Util'; +import { useRouter } from 'next/navigation'; +import { SubmodelOrIdReference } from 'components/contexts/CurrentAasContext'; +import { ImageWithFallback } from 'components/basics/StyledImageWithFallBack'; +import { useTranslations } from 'next-intl'; +import { SubmodelSemanticIdEnum } from 'lib/enums/SubmodelSemanticId.enum'; +import { findSubmodelByIdOrSemanticId, findSubmodelElementByIdShort } from 'lib/util/SubmodelResolverUtil'; +import { MobileAccordion } from 'components/basics/detailViewBasics/MobileAccordion'; +import { KeyFactsBox } from 'app/[locale]/product/_components/KeyFactsBox'; +import { SubmodelElementSemanticIdEnum } from 'lib/enums/SubmodelElementSemanticId.enum'; +import { useProductImageUrl } from 'lib/hooks/UseProductImageUrl'; +import { useFindValueByIdShort } from 'lib/hooks/useFindValueByIdShort'; +import { ActionMenu } from './ProductActionMenu'; +import LinkIcon from '@mui/icons-material/Link'; +import { MnestixConnection } from '../../../../../prisma/generated/client'; +import { + AssetAdministrationShell, + Property, + SubmodelElementChoice, + SubmodelElementCollection, +} from 'lib/api/aas/models'; +import { useAasStore } from 'stores/AasStore'; + +type ProductOverviewCardProps = { + readonly aas: AssetAdministrationShell | null; + readonly submodels: SubmodelOrIdReference[] | null; + readonly productImage?: string; + readonly isLoading?: boolean; + readonly isAccordion: boolean; + readonly imageLinksToDetail?: boolean; + readonly repositoryURL?: string; + readonly displayName: string | null; + readonly catalogConfig?: MnestixConnection; + readonly infrastructureName?: string; +}; + +type ProductClassification = { + ProductClassificationSystem?: string; + ProductClassId?: string; +}; + +type OverviewData = { + readonly manufacturerName?: string; + readonly manufacturerProductDesignation?: string; + readonly manufacturerProductRoot?: string; + readonly manufacturerProductFamily?: string; + readonly manufacturerProductType?: string; + readonly manufacturerArticleNumber?: string; + readonly manufacturerOrderCode?: string; + readonly manufacturerLogo: SubmodelElementChoice | null; + readonly companyLogo: SubmodelElementChoice | null; + readonly markings: string[] | null; + readonly productClassifications?: ProductClassification[]; + readonly URIOfTheProduct?: string | null; +}; + +export function ProductOverviewCard(props: ProductOverviewCardProps) { + const isAccordion = props.isAccordion; + const navigate = useRouter(); + const t = useTranslations('pages.productViewer'); + const findValue = useFindValueByIdShort(); + const productImageUrl = useProductImageUrl(props.aas, props.repositoryURL ?? undefined, props.productImage); + const { addAasData } = useAasStore(); + + const prepareTechnicalDataSubmodel = useCallback( + (technicalDataSubmodelElements: Array): Partial => { + const manufacturerName = findValue( + technicalDataSubmodelElements, + 'ManufacturerName', + SubmodelElementSemanticIdEnum.ManufacturerName, + ); + const manufacturerProductDesignation = findValue( + technicalDataSubmodelElements, + 'ManufacturerProductDesignation', + SubmodelElementSemanticIdEnum.ManufacturerProductDesignation, + ); + const manufacturerArticleNumber = findValue( + technicalDataSubmodelElements, + 'ManufacturerArticleNumber', + SubmodelElementSemanticIdEnum.ManufacturerArticleNumber, + ); + const manufacturerOrderCode = findValue( + technicalDataSubmodelElements, + 'ManufacturerOrderCode', + SubmodelElementSemanticIdEnum.ManufacturerOrderCode, + ); + const manufacturerLogo = findSubmodelElementByIdShort( + technicalDataSubmodelElements, + 'ManufacturerLogo', + SubmodelElementSemanticIdEnum.ManufacturerLogo, + ); + const productClassifications = findSubmodelElementByIdShort( + technicalDataSubmodelElements, + 'ProductClassifications', + SubmodelElementSemanticIdEnum.ProductClassifications, + ) as SubmodelElementCollection; + const classifications: ProductClassification[] = []; + + // If ProductClassifications is a flat list, we can directly use it + // This is not standard conform but we support it for now. + if ( + productClassifications?.value && + productClassifications.value.find((element) => element.idShort === 'ProductClassificationSystem') + ) { + const classification = { + ProductClassificationSystem: + findValue( + productClassifications.value, + 'ProductClassificationSystem', + SubmodelElementSemanticIdEnum.ProductClassificationSystem, + ) || undefined, + ProductClassId: + findValue( + productClassifications.value, + 'ProductClassId', + SubmodelElementSemanticIdEnum.ProductClassId, + ) || undefined, + }; + classifications.push(classification); + } else { + productClassifications?.value?.forEach((productClassification) => { + const submodelClassification = productClassification as SubmodelElementCollection; + if (submodelClassification?.value) { + const classification = { + ProductClassificationSystem: + findValue( + submodelClassification.value, + 'ProductClassificationSystem', + SubmodelElementSemanticIdEnum.ProductClassificationSystem, + ) || undefined, + ProductClassId: + findValue( + submodelClassification.value, + 'ProductClassId', + SubmodelElementSemanticIdEnum.ProductClassId, + ) || undefined, + }; + // Filter out classifications without a ProductClassId + if (!classification.ProductClassId) { + return; + } + classifications.push(classification); + } + }); + } + return { + manufacturerName: !manufacturerName || manufacturerName.trim() === '' ? undefined : manufacturerName, + manufacturerProductDesignation: + !manufacturerProductDesignation || manufacturerProductDesignation.trim() === '' + ? undefined + : manufacturerProductDesignation, + productClassifications: classifications, + manufacturerArticleNumber: + !manufacturerArticleNumber || manufacturerArticleNumber.trim() === '' + ? undefined + : manufacturerArticleNumber, + manufacturerOrderCode: + !manufacturerOrderCode || manufacturerOrderCode.trim() === '' ? undefined : manufacturerOrderCode, + companyLogo: null, + markings: null, + manufacturerLogo: manufacturerLogo, + }; + }, + [findValue], + ); + + /** + * Prepare marking texts from the SubmodelElementCollection by extracting the 'MarkingName' properties. + * @param markings + */ + const prepareMarkingTexts = (markings: SubmodelElementCollection | null): string[] => { + if (!markings?.value) return []; + + const result: string[] = []; + markings.value.forEach((el) => { + Object.values(el || {}).forEach((marking) => { + Object.values(marking || {}).forEach((markingProperty: SubmodelElementChoice) => { + if (markingProperty?.idShort === 'MarkingName' && (markingProperty as Property).value) { + result.push((markingProperty as Property).value || ''); + } + }); + }); + }); + return result; + }; + + const prepareNameplateData = useCallback( + ( + nameplateSubmodelElements: Array, + prevData?: Partial, + ): Partial => { + const manufacturerProductRoot = findValue( + nameplateSubmodelElements, + 'ManufacturerProductRoot', + SubmodelElementSemanticIdEnum.ManufacturerProductRoot, + ); + const manufacturerProductFamily = findValue( + nameplateSubmodelElements, + 'ManufacturerProductFamily', + SubmodelElementSemanticIdEnum.ManufacturerProductFamily, + ); + const manufacturerProductType = findValue( + nameplateSubmodelElements, + 'ManufacturerProductType', + SubmodelElementSemanticIdEnum.ManufacturerProductType, + ); + const markingsElement = findSubmodelElementByIdShort( + nameplateSubmodelElements, + 'Markings', + SubmodelElementSemanticIdEnum.MarkingsV3, + ) as SubmodelElementCollection; + + const markings = prepareMarkingTexts(markingsElement || null); + + const companyLogo = findSubmodelElementByIdShort( + nameplateSubmodelElements, + 'CompanyLogo', + SubmodelElementSemanticIdEnum.CompanyLogo, + ); + const URIOfTheProduct = findValue(nameplateSubmodelElements, 'URIOfTheProducts', [ + SubmodelElementSemanticIdEnum.URIOfTheProductV2, + SubmodelElementSemanticIdEnum.URIOfTheProductV3, + ]); + const manufacturerName = findValue( + nameplateSubmodelElements, + 'ManufacturerName', + SubmodelElementSemanticIdEnum.ManufacturerName, + ); + const manufacturerProductDesignation = findValue( + nameplateSubmodelElements, + 'ManufacturerProductDesignation', + SubmodelElementSemanticIdEnum.ManufacturerProductDesignation, + ); + const manufacturerArticleNumber = findValue( + nameplateSubmodelElements, + 'ProductArticleNumberOfManufacturer', + SubmodelElementSemanticIdEnum.ManufacturerArticleNumber, + ); + const manufacturerOrderCode = findValue( + nameplateSubmodelElements, + 'OrderCodeOfManufacturer', + SubmodelElementSemanticIdEnum.ManufacturerOrderCode, + ); + return { + ...prevData, + manufacturerName: prevData?.manufacturerName ? prevData.manufacturerName : (manufacturerName ?? '-'), + manufacturerProductDesignation: prevData?.manufacturerProductDesignation + ? prevData.manufacturerProductDesignation + : (manufacturerProductDesignation ?? '-'), + manufacturerArticleNumber: prevData?.manufacturerArticleNumber + ? prevData.manufacturerArticleNumber + : (manufacturerArticleNumber ?? '-'), + manufacturerOrderCode: prevData?.manufacturerOrderCode + ? prevData.manufacturerOrderCode + : (manufacturerOrderCode ?? '-'), + manufacturerProductRoot: manufacturerProductRoot ?? '-', + manufacturerProductFamily: manufacturerProductFamily ?? '-', + manufacturerProductType: manufacturerProductType ?? '-', + markings: markings, + manufacturerLogo: prevData?.manufacturerLogo || null, + companyLogo: companyLogo || null, + URIOfTheProduct: URIOfTheProduct || null, + }; + }, + [findValue, prepareMarkingTexts], + ); + + const overviewData = useMemo((): OverviewData | undefined => { + if (!props.submodels || props.submodels.length === 0) return undefined; + + const technicalData = findSubmodelByIdOrSemanticId( + props.submodels, + SubmodelSemanticIdEnum.TechnicalDataV11, + 'TechnicalData', + ); + let data: Partial | undefined; + if (technicalData?.submodelElements) { + data = prepareTechnicalDataSubmodel(technicalData.submodelElements); + } + + const nameplate = findSubmodelByIdOrSemanticId( + props.submodels, + SubmodelSemanticIdEnum.NameplateV2, + 'Nameplate', + ); + if (nameplate?.submodelElements) { + data = prepareNameplateData(nameplate.submodelElements, data); + } + + return data as OverviewData | undefined; + }, [props.submodels, prepareTechnicalDataSubmodel, prepareNameplateData]); + + const infoBoxStyle = { + display: 'flex', + flexDirection: 'column', + width: '100%', + flexGrow: '1', + flexBasis: '0', + }; + + const titleStyle = { + marginBottom: '15px', + }; + + const cardContentStyle = { + display: 'flex', + alignItems: isAccordion ? 'center' : 'unset', + gap: isAccordion ? '10px' : '40px', + flexDirection: isAccordion ? 'column' : 'row', + }; + + const navigateToAas = () => { + if (props.imageLinksToDetail && props.aas && props.infrastructureName) { + addAasData({ + aas: props.aas, + aasData: { + aasRepositoryOrigin: props.repositoryURL, + submodelDescriptors: undefined, + infrastructureName: props.infrastructureName, + }, + }); + const url = `/product/${encodeBase64(props.aas.id)}`; + navigate.push(url); + } + }; + + const navigateToProduct = () => { + if (overviewData?.URIOfTheProduct) { + const url = overviewData.URIOfTheProduct; + window.open(url, '_blank', 'noopener,noreferrer'); + } + }; + + const productInfo = ( + + + + + + + + + + + ); + + const showKeyFactsBox = + (overviewData?.productClassifications && overviewData.productClassifications.length > 0) || + (overviewData?.markings && overviewData.markings.length > 0); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const classificationInfo = ( + + {!isAccordion && ( + + + + + + {t('classification')} + + + )} + + + + + ); + + return ( + + + {props.isLoading ? ( + <> + + + {isAccordion ? ( + + + + + ) : ( + <> + + + + + + + + + + + + + + )} + + + ) : ( + <> + + {isAccordion ? ( + + ) : ( + <> + + + + {props.displayName || t('title')} + {overviewData?.URIOfTheProduct && ( + + + + )} + + + + + {productInfo} + + + )} + + )} + + {showKeyFactsBox && ( + + )} + + ); +} diff --git a/src/app/[locale]/settings/_components/SettingsCardHeader.tsx b/src/app/[locale]/settings/_components/SettingsCardHeader.tsx index e4ab31e7a..de156eafd 100644 --- a/src/app/[locale]/settings/_components/SettingsCardHeader.tsx +++ b/src/app/[locale]/settings/_components/SettingsCardHeader.tsx @@ -18,9 +18,21 @@ export type SettingsCardHeaderProps = { export function SettingsCardHeader(props: SettingsCardHeaderProps) { const t = useTranslations('pages.settings'); return ( - + - + {props.isEditMode ? ( <> - + {Object.entries(groupedRules).map(([roleName, rules]) => ( { )} - setRuleDetailDialogOpen(false)} reloadRules={loadRbacData} diff --git a/src/app/[locale]/settings/_components/role-settings/target-information/TargetInformationForm.tsx b/src/app/[locale]/settings/_components/role-settings/target-information/TargetInformationForm.tsx index a7ca3e83f..e5543409b 100644 --- a/src/app/[locale]/settings/_components/role-settings/target-information/TargetInformationForm.tsx +++ b/src/app/[locale]/settings/_components/role-settings/target-information/TargetInformationForm.tsx @@ -31,7 +31,9 @@ export const TargetInformationForm = (props: TargetInformationProps) => { }, []); return ( - + {t('rules.tableHeader.type')} { } const ids = element as string[]; permissions.push( - + {key} {ids.join(', ')} , diff --git a/src/app/[locale]/settings/_components/role-settings/target-information/WildcardOrStringArrayInput.tsx b/src/app/[locale]/settings/_components/role-settings/target-information/WildcardOrStringArrayInput.tsx index 15dd2d361..c6f2cf94d 100644 --- a/src/app/[locale]/settings/_components/role-settings/target-information/WildcardOrStringArrayInput.tsx +++ b/src/app/[locale]/settings/_components/role-settings/target-information/WildcardOrStringArrayInput.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Box, Button, Checkbox, FormControlLabel, IconButton, TextField, Typography } from '@mui/material'; import ControlPointIcon from '@mui/icons-material/ControlPoint'; -import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutlineOutlined'; import { Control, Controller, useFieldArray, UseFormGetValues, UseFormSetValue } from 'react-hook-form'; import { useTranslations } from 'next-intl'; import { RuleFormModel } from 'app/[locale]/settings/_components/role-settings/RuleForm'; @@ -38,9 +38,10 @@ export const WildcardOrStringArrayInput = (props: WildcardOrStringArrayInputProp }; return ( - + {props.rule} - ( - + - + : blueprintDisplayName || ''} - + {isLoading ? : templateSemanticId} @@ -254,9 +256,17 @@ function TemplateBuilderContent() { - + - + {t('healthCheck.label')} + sx={{ + color: 'text.secondary', + mt: 1 + }}> {t('docsLink')} diff --git a/src/app/[locale]/templates/_components/BlueprintItem.tsx b/src/app/[locale]/templates/_components/BlueprintItem.tsx index 9cf800d5b..04f7c8c33 100644 --- a/src/app/[locale]/templates/_components/BlueprintItem.tsx +++ b/src/app/[locale]/templates/_components/BlueprintItem.tsx @@ -97,7 +97,9 @@ export function BlueprintItem(props: BlueprintItemProps) { {props.item.displayName} - + {props.item.basedOnTemplate} diff --git a/src/app/[locale]/templates/_components/ChooseTemplateItem.tsx b/src/app/[locale]/templates/_components/ChooseTemplateItem.tsx index 80c161490..46ae9dfb5 100644 --- a/src/app/[locale]/templates/_components/ChooseTemplateItem.tsx +++ b/src/app/[locale]/templates/_components/ChooseTemplateItem.tsx @@ -34,15 +34,14 @@ export function ChooseTemplateItem(props: ChooseTemplateItemProps) { {!!subLabel && ( + display: 'inline-block' + }}> {subLabel} )} diff --git a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditFields.tsx b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditFields.tsx index a92c882dc..acb5f6a6c 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditFields.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditFields.tsx @@ -77,11 +77,23 @@ export function BlueprintEditFields() { fullWidth /> {selectedElement?.data?.modelType && ( - - + + {t('labels.modelType')} - + {selectedElement.data.modelType} @@ -90,7 +102,6 @@ export function BlueprintEditFields() { )} {/* Render the SME-Fields */} {getRenderFields()} - {/* Render the Mapping-Qualifiers */} {selectedElement?.data && ( <> diff --git a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading.tsx b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading.tsx index fbae65d8a..0639b2f3b 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading.tsx @@ -68,8 +68,19 @@ export function BlueprintEditSectionHeading(props: BlueprintEditSectionHeadingPr return ( <> - - + + {getIcon()} {getTitle()} diff --git a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditTreeItem.tsx b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditTreeItem.tsx index a52ce806c..e0cf185cd 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditTreeItem.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/BlueprintEditTreeItem.tsx @@ -99,7 +99,9 @@ const CustomContent = React.forwardRef(function CustomContent( }} onClick={handleSelectionClick} > - + {label} diff --git a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/SubmodelEditComponent.tsx b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/SubmodelEditComponent.tsx index bbd98234a..31609efe7 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/SubmodelEditComponent.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/SubmodelEditComponent.tsx @@ -33,7 +33,11 @@ export function SubmodelEditComponent(props: SubmodelEditComponentProps) { return ( <> - + onTextChange(e)} fullWidth /> - ) : ( - )} diff --git a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/mapping-info/MappingInfoEditComponent.tsx b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/mapping-info/MappingInfoEditComponent.tsx index 32181b08e..54e3be6b1 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/mapping-info/MappingInfoEditComponent.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/mapping-info/MappingInfoEditComponent.tsx @@ -1,4 +1,4 @@ -import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; +import { AddCircleOutlineOutlined, RemoveCircleOutlineOutlined } from '@mui/icons-material'; import { Box, Button, IconButton, TextField } from '@mui/material'; import { BlueprintEditSectionHeading } from 'app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading'; import { Qualifier, Submodel, SubmodelElementChoice } from 'lib/api/aas/models'; @@ -64,7 +64,11 @@ export function MappingInfoEditComponent(props: MappingInfoEditComponentProps) { <> {mappingInfo ? ( - + - + ) : ( - )} diff --git a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multi-lang/MultiLangEditComponent.tsx b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multi-lang/MultiLangEditComponent.tsx index 73133156e..52a90852d 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multi-lang/MultiLangEditComponent.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multi-lang/MultiLangEditComponent.tsx @@ -1,4 +1,4 @@ -import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; +import { AddCircleOutlineOutlined, RemoveCircleOutlineOutlined } from '@mui/icons-material'; import { Autocomplete, Box, Button, IconButton, TextField } from '@mui/material'; import { BlueprintEditSectionHeading } from 'app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading'; import options from './language-suggestions.json'; @@ -49,7 +49,13 @@ export function MultiLangEditComponent(props: MultiLangEditComponentProps) { {Array.isArray(langStrings) && langStrings.map((langString, i) => ( - + } @@ -67,11 +73,11 @@ export function MultiLangEditComponent(props: MultiLangEditComponentProps) { fullWidth /> onRemove(i)}> - + ))} - diff --git a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multiplicity/MultiplicityEditComponent.tsx b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multiplicity/MultiplicityEditComponent.tsx index f6205c211..db6dfb55b 100644 --- a/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multiplicity/MultiplicityEditComponent.tsx +++ b/src/app/[locale]/templates/_components/blueprint-edit/edit-components/multiplicity/MultiplicityEditComponent.tsx @@ -1,4 +1,4 @@ -import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'; +import { AddCircleOutlineOutlined, RemoveCircleOutlineOutlined } from '@mui/icons-material'; import { Box, Button, FormControl, IconButton, InputLabel, MenuItem, Select } from '@mui/material'; import { useState } from 'react'; import { BlueprintEditSectionHeading } from 'app/[locale]/templates/_components/blueprint-edit/BlueprintEditSectionHeading'; @@ -85,7 +85,11 @@ export function MultiplicityEditComponent(props: MultiplicityEditComponentProps) {valueEnabled ? ( <> {allowMultiplicityToBeSet ? ( - + Multiplicity