diff --git a/src/app/[locale]/asset/_components/RedirectToViewer.tsx b/src/app/[locale]/asset/_components/RedirectToViewer.tsx index 36e3e65..89b5766 100644 --- a/src/app/[locale]/asset/_components/RedirectToViewer.tsx +++ b/src/app/[locale]/asset/_components/RedirectToViewer.tsx @@ -11,7 +11,6 @@ import { NotFoundError } from 'lib/errors/NotFoundError'; import { useState } from 'react'; import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner'; import { useSearchParams, useRouter } from 'next/navigation'; -import { useEnv } from 'app/env/provider'; export const RedirectToViewer = () => { const { discoveryServiceClient } = useApis(); @@ -19,7 +18,6 @@ export const RedirectToViewer = () => { const searchParams = useSearchParams(); const assetIdParam = searchParams.get('assetId')?.toString(); const notificationSpawner = useNotificationSpawner(); - const env = useEnv(); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); diff --git a/src/app/[locale]/list/_components/AASListView.tsx b/src/app/[locale]/list/_components/AASListView.tsx deleted file mode 100644 index 4614d27..0000000 --- a/src/app/[locale]/list/_components/AASListView.tsx +++ /dev/null @@ -1,474 +0,0 @@ -'use client'; -import { useRouter } from 'next/navigation'; -import { useApis } from 'components/azureAuthentication/ApiProvider'; -import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner'; -import { useEnv } from 'app/env/provider'; -import { useEffect, useState } from 'react'; -import { AasListEntry } from 'lib/api/generated-api/clients.g'; -import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { getProductClassId, parseProductClassFromString, ProductClass } from 'lib/util/ProductClassResolverUtil'; -import { - Box, - Button, - Checkbox, - Chip, - FormControl, - IconButton, - InputLabel, - MenuItem, - Paper, - Select, - SelectChangeEvent, - styled, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Tooltip, - Typography, - useTheme, -} from '@mui/material'; -import { ShellIcon } from 'components/custom-icons/ShellIcon'; -import { encodeBase64 } from 'lib/util/Base64Util'; -import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; -import CloseIcon from '@mui/icons-material/Close'; -import TireRepairIcon from '@mui/icons-material/TireRepair'; -import FireHydrantAltIcon from '@mui/icons-material/FireHydrantAlt'; -import LabelOffIcon from '@mui/icons-material/LabelOff'; -import LabelIcon from '@mui/icons-material/Label'; -import { messages } from 'lib/i18n/localization'; -import { showError } from 'lib/util/ErrorHandlerUtil'; -import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect'; -import { useAasState } from 'components/contexts/CurrentAasContext'; - -const StyledImage = styled('img')(() => ({ - maxHeight: '88px', - maxWidth: '88px', - width: '100%', - objectFit: 'scale-down', -})); - -export const AASListView = () => { - const { aasListClient } = useApis(); - const [isLoadingList, setIsLoadingList] = useState(false); - const [aasList, setAasList] = useState(); - const [aasListFiltered, setAasListFiltered] = useState(); - const [productClassFilterValue, setProductClassFilterValue] = useState(''); - const [selectedAasList, setSelectedAasList] = useState(); - const [productClass, setProductClass] = useState([]); - const notificationSpawner = useNotificationSpawner(); - const intl = useIntl(); - const navigate = useRouter(); - const theme = useTheme(); - const env = useEnv(); - const [, setAas] = useAasState(); - const MAX_SELECTED_ITEMS = 3; - - useAsyncEffect(async () => { - try { - setIsLoadingList(true); - const list = await aasListClient.getAasListEntries(); - setAasList(list); - if (!productClassFilterValue) setAasListFiltered(list); - } catch (e) { - showError(e, notificationSpawner); - } finally { - setIsLoadingList(false); - } - }, []); - - /** - * Creates the ProductClass Filter values. - */ - useEffect(() => { - const productClasses: ProductClass[] = []; - if (aasList) { - aasList.forEach((aas) => { - if (!aas.productGroup) return; - const productClassId = getProductClassId(aas.productGroup); - let productClassString; - try { - productClassString = intl.formatMessage(messages.mnestix.aasList.productClasses[productClassId]); - } catch (e) { - console.warn('Invalid product type', e); - } - if (!productClassString) { - productClassString = getProductClassId(aas.productGroup); - } - const productClass = parseProductClassFromString(productClassId, productClassString); - if (!productClasses.find((element) => element.id === productClass.id)) { - productClasses.push(productClass); - } - }); - } - setProductClass(productClasses); - }, [aasList]); - - const translateListText = (property: { [key: string]: string } | undefined) => { - if (!property) return ''; - return property[intl.locale] ?? Object.values(property)[0] ?? ''; - }; - - const navigateToAas = (listEntry: AasListEntry) => { - setAas(null); - if (listEntry.aasId) navigate.push(`/viewer/${encodeBase64(listEntry.aasId)}`); - }; - - const navigateToCompare = () => { - const encodedAasList = selectedAasList?.map((aasId) => { - return encodeURIComponent(aasId); - }); - const searchString = encodedAasList?.join('&aasId='); - navigate.push(`/compare?aasId=${searchString}`); - }; - - /** - * Decides if the current checkbox should be disabled or not. - */ - const checkBoxDisabled = (aasId: string | undefined) => { - if (!aasId) return false; - return selectedAasList && selectedAasList.length >= MAX_SELECTED_ITEMS && !selectedAasList.includes(aasId); - }; - - /** - * Shows a warning, indicating that no more aas can be selected. - */ - const showMaxElementsNotification = () => { - notificationSpawner.spawn({ - message: ( - - - - ), - severity: 'warning', - }); - }; - - /** - * Shortens the property text and provides the full text in a tooltip. - */ - const tooltipText = (property: string | undefined, maxChars: number) => { - if (!property) return ''; - else { - return property.length > maxChars ? ( - - {`${property.slice(0, maxChars)} (...)`} - - ) : ( - <>{property} - ); - } - }; - - /** - * Returns an icon component based on the provided product class type. - * @param productClassType - product class type - */ - const getProductClassIcon = (productClassType: string) => { - switch (productClassType) { - case 'pneumatics': - return ; - case 'hydraulics': - return ; - default: - return ; - } - }; - - /** - * Returns a chip component adjusted for product class element - */ - const productClassValue = (productClassId: string | null, maxChars: number) => { - if (!productClassId) return ''; - let productClass; - try { - productClass = parseProductClassFromString( - productClassId, - intl.formatMessage(messages.mnestix.aasList.productClasses[productClassId]), - ); - } catch (e) { - console.warn('Invalid product type', e); - } - if (!productClass) { - productClass = parseProductClassFromString(productClassId, productClassId); - } - return ( - - ); - }; - - /** - * Applies product filter change to the list. - * @param event - */ - const handleFilterChange = (event: SelectChangeEvent) => { - setProductClassFilterValue(event.target.value); - if (!aasList) return; - if (event.target.value === '') { - setAasListFiltered(aasList); - } else { - const filteredList = aasList.filter((aas) => { - return aas.productGroup && aas.productGroup.startsWith(event.target.value); - }); - setAasListFiltered(filteredList); - } - }; - const SelectProductType = () => { - return ( - - - - - - - ); - }; - - const tableBodyText = { - lineHeight: '150%', - fontSize: '16px', - color: 'text.primary', - }; - - /** - * Update the list of currently selected aas - */ - const updateSelectedAasList = (isChecked: boolean, aasId: string | undefined) => { - if (!aasId) return; - let selected: string[] = []; - - if (isChecked) { - selected = selected.concat(selectedAasList ? selectedAasList : [], [aasId]); - selected = [...new Set(selected)]; - } else if (!isChecked && selectedAasList) { - selected = selectedAasList.filter((aas) => { - return aas !== aasId; - }); - } else { - return; - } - - setSelectedAasList(selected); - }; - - return ( - <> - {env.COMPARISON_FEATURE_FLAG && ( - <> - - - - - {selectedAasList?.map((selectedAas) => ( - - - {tooltipText(selectedAas, 15)} - - updateSelectedAasList(false, selectedAas)}> - - - - ))} - - - - - - - )} - {isLoadingList && } - {!isLoadingList && aasListFiltered && ( - - - - - {env.COMPARISON_FEATURE_FLAG && ( - - } - arrow - > - - - - )} - - - - - - - - - - - - - - - - - - /{' '} - - - - - - - - - - - - {aasListFiltered?.map((aasListEntry) => ( - - {env.COMPARISON_FEATURE_FLAG && ( - - { - if (checkBoxDisabled(aasListEntry.aasId)) - showMaxElementsNotification(); - }} - > - el == aasListEntry.aasId) - ) - } - disabled={checkBoxDisabled(aasListEntry.aasId)} - onChange={(evt) => - updateSelectedAasList(evt.target.checked, aasListEntry.aasId) - } - data-testid="list-checkbox" - /> - - - )} - - navigateToAas(aasListEntry)} - sx={{ - width: '88px', - height: '88px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - '&:hover': { - boxShadow: 6, - cursor: 'pointer', - }, - }} - data-testid="list-thumbnail" - > - {aasListEntry.thumbnailUrl ? ( - - ) : ( - - )} - - - - {translateListText(aasListEntry.manufacturerName)} - - - {tooltipText( - translateListText(aasListEntry.manufacturerProductDesignation), - 100, - )} - - - - - - {tooltipText(aasListEntry.assetId, 100)}
- - - - {tooltipText(aasListEntry.aasId, 100)} -
- - {aasListEntry.productGroup ? ( - productClassValue(getProductClassId(aasListEntry.productGroup), 25) - ) : ( - } - variant="outlined" - icon={} - data-testid="product-class-chip" - /> - )} - -
- ))} -
-
-
- )} - - ); -}; diff --git a/src/app/[locale]/list/_components/AasList.tsx b/src/app/[locale]/list/_components/AasList.tsx new file mode 100644 index 0000000..93501c4 --- /dev/null +++ b/src/app/[locale]/list/_components/AasList.tsx @@ -0,0 +1,94 @@ +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; +import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; +import { AasListEntry } from 'lib/api/generated-api/clients.g'; +import { AasListTableRow } from 'app/[locale]/list/_components/AasListTableRow'; + +type AasListProps = { + shells: AasListEntry[]; + tableHeaders: { label: string }[]; + comparisonFeatureFlag?: boolean; + selectedAasList: string[] | undefined; + updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void; +}; + +export default function AasList(props: AasListProps) { + const { shells, tableHeaders, selectedAasList, updateSelectedAasList, comparisonFeatureFlag } = props; + const theme = useTheme(); + const MAX_SELECTED_ITEMS = 3; + + /** + * Decides if the current checkbox should be disabled or not. + */ + const checkBoxDisabled = (aasId: string | undefined) => { + if (!aasId) return false; + return selectedAasList && selectedAasList.length >= MAX_SELECTED_ITEMS && !selectedAasList.includes(aasId); + }; + + return ( + + + + + {comparisonFeatureFlag && ( + + } + arrow + > + + + + )} + {!!tableHeaders && + tableHeaders.map((header: { label: string }, index) => ( + + {header.label} + + ))} + + + + {shells?.map((aasListEntry) => ( + + + + ))} + +
+
+ ); +} diff --git a/src/app/[locale]/list/_components/AasListComparisonHeader.tsx b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx new file mode 100644 index 0000000..1f78188 --- /dev/null +++ b/src/app/[locale]/list/_components/AasListComparisonHeader.tsx @@ -0,0 +1,50 @@ +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; +import CloseIcon from '@mui/icons-material/Close'; +import { useRouter } from 'next/navigation'; +import { tooltipText } from 'lib/util/ToolTipText'; + +type CompareAasListBarType = { + selectedAasList: string[] | undefined; + updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void; +}; + +export const AasListComparisonHeader = (props: CompareAasListBarType) => { + const { selectedAasList, updateSelectedAasList } = props; + + const navigate = useRouter(); + const navigateToCompare = () => { + const encodedAasList = selectedAasList?.map((aasId) => { + return encodeURIComponent(aasId); + }); + const searchString = encodedAasList?.join('&aasId='); + navigate.push(`/compare?aasId=${searchString}`); + }; + + return ( + <> + + + + + {selectedAasList?.map((selectedAas) => ( + + {tooltipText(selectedAas, 15)} + updateSelectedAasList(false, selectedAas)}> + + + + ))} + + + + ); +}; diff --git a/src/app/[locale]/list/_components/AasListHeader.tsx b/src/app/[locale]/list/_components/AasListHeader.tsx new file mode 100644 index 0000000..40de5e5 --- /dev/null +++ b/src/app/[locale]/list/_components/AasListHeader.tsx @@ -0,0 +1,12 @@ +import { getTranslations } from 'next-intl/server'; +import { Typography } from '@mui/material'; + +export default async function AasListHeader() { + const t = await getTranslations('aas-list'); + + return ( + + {t('header')} + + ); +} diff --git a/src/app/[locale]/list/_components/AasListTableRow.tsx b/src/app/[locale]/list/_components/AasListTableRow.tsx new file mode 100644 index 0000000..ef0facf --- /dev/null +++ b/src/app/[locale]/list/_components/AasListTableRow.tsx @@ -0,0 +1,133 @@ +import { Box, Checkbox, Chip, Paper, TableCell, Typography } from '@mui/material'; +import { ShellIcon } from 'components/custom-icons/ShellIcon'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; +import { getProductClassId } from 'lib/util/ProductClassResolverUtil'; +import LabelOffIcon from '@mui/icons-material/LabelOff'; +import { AasListEntry } from 'lib/api/generated-api/clients.g'; +import { encodeBase64 } from 'lib/util/Base64Util'; +import { useRouter } from 'next/navigation'; +import { useAasState } from 'components/contexts/CurrentAasContext'; +import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner'; +import { ImageWithFallback } from './StyledImageWithFallBack'; +import { ProductClassChip } from 'app/[locale]/list/_components/ProductClassChip'; +import { tooltipText } from 'lib/util/ToolTipText'; + +type AasTableRow = { + aasListEntry: AasListEntry; + comparisonFeatureFlag: boolean | undefined; + checkBoxDisabled: (aasId: string | undefined) => boolean | undefined; + selectedAasList: string[] | undefined; + updateSelectedAasList: (isChecked: boolean, aasId: string | undefined) => void; +}; + +const tableBodyText = { + lineHeight: '150%', + fontSize: '16px', + color: 'text.primary', +}; +export const AasListTableRow = (props: AasTableRow) => { + const { aasListEntry, comparisonFeatureFlag, checkBoxDisabled, selectedAasList, updateSelectedAasList } = props; + const navigate = useRouter(); + const intl = useIntl(); + const [, setAas] = useAasState(); + const notificationSpawner = useNotificationSpawner(); + const navigateToAas = (listEntry: AasListEntry) => { + setAas(null); + if (listEntry.aasId) navigate.push(`/viewer/${encodeBase64(listEntry.aasId)}`); + }; + + const translateListText = (property: { [key: string]: string } | undefined) => { + if (!property) return ''; + return property[intl.locale] ?? Object.values(property)[0] ?? ''; + }; + + const showMaxElementsNotification = () => { + notificationSpawner.spawn({ + message: ( + + + + ), + severity: 'warning', + }); + }; + + return ( + <> + {comparisonFeatureFlag && ( + + { + if (checkBoxDisabled(aasListEntry.aasId)) showMaxElementsNotification(); + }} + > + el == aasListEntry.aasId))} + disabled={checkBoxDisabled(aasListEntry.aasId)} + onChange={(evt) => updateSelectedAasList(evt.target.checked, aasListEntry.aasId)} + data-testid="list-checkbox" + /> + + + )} + + navigateToAas(aasListEntry)} + sx={{ + width: '88px', + height: '88px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + '&:hover': { + boxShadow: 6, + cursor: 'pointer', + }, + }} + data-testid="list-thumbnail" + > + {aasListEntry.thumbnailUrl ? ( + + ) : ( + + )} + + + + {translateListText(aasListEntry.manufacturerName)} + + + {tooltipText(translateListText(aasListEntry.manufacturerProductDesignation), 80)} + + + + + + {tooltipText(aasListEntry.assetId, 80)}
+ + + + {tooltipText(aasListEntry.aasId, 80)} +
+ + {aasListEntry.productGroup ? ( + + ) : ( + } + variant="outlined" + icon={} + data-testid="product-class-chip" + /> + )} + + + ); +}; diff --git a/src/app/[locale]/list/_components/AasListView.tsx b/src/app/[locale]/list/_components/AasListView.tsx new file mode 100644 index 0000000..4a4c2ed --- /dev/null +++ b/src/app/[locale]/list/_components/AasListView.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { useApis } from 'components/azureAuthentication/ApiProvider'; +import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner'; +import { useEnv } from 'app/env/provider'; +import { useState } from 'react'; +import { AasListEntry } from 'lib/api/generated-api/clients.g'; +import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner'; +import { Box } from '@mui/material'; +import { showError } from 'lib/util/ErrorHandlerUtil'; +import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect'; +import { SelectProductType } from 'app/[locale]/list/_components/SelectProductType'; +import { AasListComparisonHeader } from 'app/[locale]/list/_components/AasListComparisonHeader'; +import AasList from 'app/[locale]/list/_components/AasList'; +import { useIntl } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; + +export const AasListView = () => { + const { aasListClient } = useApis(); + const [isLoadingList, setIsLoadingList] = useState(false); + const [aasList, setAasList] = useState(); + const [aasListFiltered, setAasListFiltered] = useState(); + const [selectedAasList, setSelectedAasList] = useState(); + const notificationSpawner = useNotificationSpawner(); + const env = useEnv(); + const intl = useIntl(); + + useAsyncEffect(async () => { + try { + setIsLoadingList(true); + const list = await aasListClient.getAasListEntries(); + setAasList(list); + setAasListFiltered(list); + } catch (e) { + showError(e, notificationSpawner); + } finally { + setIsLoadingList(false); + } + }, []); + + const tableHeaders = [ + { label: intl.formatMessage(messages.mnestix.aasList.picture) }, + { label: intl.formatMessage(messages.mnestix.aasList.manufacturerHeading) }, + { label: intl.formatMessage(messages.mnestix.aasList.productDesignationHeading) }, + { + label: + intl.formatMessage(messages.mnestix.aasList.assetIdHeading) + + ' / ' + + intl.formatMessage(messages.mnestix.aasList.aasIdHeading), + }, + { label: intl.formatMessage(messages.mnestix.aasList.productClassHeading) }, + ]; + + /** + * Update the list of currently selected aas + */ + const updateSelectedAasList = (isChecked: boolean, aasId: string | undefined) => { + if (!aasId) return; + let selected: string[] = []; + + if (isChecked) { + selected = selected.concat(selectedAasList ? selectedAasList : [], [aasId]); + selected = [...new Set(selected)]; + } else if (!isChecked && selectedAasList) { + selected = selectedAasList.filter((aas) => { + return aas !== aasId; + }); + } else { + return; + } + + setSelectedAasList(selected); + }; + + return ( + <> + {env.COMPARISON_FEATURE_FLAG && ( + + )} + {isLoadingList && } + {!isLoadingList && aasListFiltered && ( + <> + + + + + + )} + + ); +}; diff --git a/src/app/[locale]/list/_components/GetProductClassIcon.tsx b/src/app/[locale]/list/_components/GetProductClassIcon.tsx new file mode 100644 index 0000000..02899dd --- /dev/null +++ b/src/app/[locale]/list/_components/GetProductClassIcon.tsx @@ -0,0 +1,21 @@ +import dynamic from 'next/dynamic'; + +const TireRepairIcon = dynamic(() => import('@mui/icons-material/TireRepair')); +const FireHydrantAltIcon = dynamic(() => import('@mui/icons-material/FireHydrantAlt')); +const LabelIcon = dynamic(() => import('@mui/icons-material/Label')); + +/** + * Returns an icon component based on the provided product class type. + * @param props + */ +export const GetProductClassIcon = (props: { productClassType: string }) => { + const { productClassType } = props; + switch (productClassType) { + case 'pneumatics': + return ; + case 'hydraulics': + return ; + default: + return ; + } +}; diff --git a/src/app/[locale]/list/_components/ProductClassChip.tsx b/src/app/[locale]/list/_components/ProductClassChip.tsx new file mode 100644 index 0000000..ae4aa64 --- /dev/null +++ b/src/app/[locale]/list/_components/ProductClassChip.tsx @@ -0,0 +1,42 @@ +import { Chip } from '@mui/material'; +import { parseProductClassFromString } from 'lib/util/ProductClassResolverUtil'; +import { useIntl } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; +import { tooltipText } from 'lib/util/ToolTipText'; +import { GetProductClassIcon } from 'app/[locale]/list/_components/GetProductClassIcon'; + +type ProductClassChipProps = { + productClassId: string | null; + maxChars: number; +}; + +/** + * Returns a chip component adjusted for product class element + */ +export const ProductClassChip = (props: ProductClassChipProps) => { + const { productClassId, maxChars } = props; + const intl = useIntl(); + if (!productClassId) return ''; + let productClass; + try { + productClass = parseProductClassFromString( + productClassId, + intl.formatMessage(messages.mnestix.aasList.productClasses[productClassId]), + ); + } catch (e) { + console.warn('Invalid product type', e); + } + if (!productClass) { + productClass = parseProductClassFromString(productClassId, productClassId); + } + return ( + } + data-testid="product-class-chip" + /> + ); +}; diff --git a/src/app/[locale]/list/_components/SelectProductType.tsx b/src/app/[locale]/list/_components/SelectProductType.tsx new file mode 100644 index 0000000..cb47f13 --- /dev/null +++ b/src/app/[locale]/list/_components/SelectProductType.tsx @@ -0,0 +1,98 @@ +import { Box, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { messages } from 'lib/i18n/localization'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { getProductClassId, parseProductClassFromString, ProductClass } from 'lib/util/ProductClassResolverUtil'; +import { AasListEntry } from 'lib/api/generated-api/clients.g'; +import { GetProductClassIcon } from './GetProductClassIcon'; +import { tooltipText } from 'lib/util/ToolTipText'; + +type SelectProductTypeProps = { + aasList: AasListEntry[] | undefined; + setAasListFiltered: Dispatch>; +}; + +export const SelectProductType = (props: SelectProductTypeProps) => { + const { aasList, setAasListFiltered } = props; + const [productClassFilterValue, setProductClassFilterValue] = useState(''); + const [productClass, setProductClass] = useState([]); + const intl = useIntl(); + /** + * Creates the ProductClass Filter values. + */ + useEffect(() => { + const productClasses: ProductClass[] = []; + if (aasList) { + aasList.forEach((aas) => { + if (!aas.productGroup) return; + const productClassId = getProductClassId(aas.productGroup); + let productClassString; + try { + productClassString = intl.formatMessage(messages.mnestix.aasList.productClasses[productClassId]); + } catch (e) { + console.warn('Invalid product type', e); + } + if (!productClassString) { + productClassString = getProductClassId(aas.productGroup); + } + const productClass = parseProductClassFromString(productClassId, productClassString); + if (!productClasses.find((element) => element.id === productClass.id)) { + productClasses.push(productClass); + } + }); + } + setProductClass(productClasses); + }, [aasList]); + + /** + * Applies product filter change to the list. + * @param event + */ + const handleFilterChange = (event: SelectChangeEvent) => { + setProductClassFilterValue(event.target.value); + if (!aasList) return; + if (event.target.value === '') { + setAasListFiltered(aasList); + } else { + const filteredList = aasList.filter((aas) => { + return aas.productGroup && aas.productGroup.startsWith(event.target.value); + }); + setAasListFiltered(filteredList); + } + }; + + return ( + + + + + + + ); +}; diff --git a/src/app/[locale]/list/_components/StyledImageWithFallBack.tsx b/src/app/[locale]/list/_components/StyledImageWithFallBack.tsx new file mode 100644 index 0000000..5e38777 --- /dev/null +++ b/src/app/[locale]/list/_components/StyledImageWithFallBack.tsx @@ -0,0 +1,34 @@ +import { styled } from '@mui/material'; +import { useState } from 'react'; +import { ShellIcon } from 'components/custom-icons/ShellIcon'; + +const StyledImage = styled('img')(() => ({ + maxHeight: '88px', + maxWidth: '88px', + width: '100%', + objectFit: 'scale-down', +})); + +type StyledImageWithFallBackProps = { + src: string; + alt: string; +}; + +export const ImageWithFallback = (props: StyledImageWithFallBackProps) => { + const { src, alt } = props; + const [hasError, setHasError] = useState(false); + + const handleError = () => { + setHasError(true); + }; + + return ( + <> + {!hasError ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/app/[locale]/list/page.tsx b/src/app/[locale]/list/page.tsx index 69aa4f2..bb3c60a 100644 --- a/src/app/[locale]/list/page.tsx +++ b/src/app/[locale]/list/page.tsx @@ -1,16 +1,13 @@ -import { Box, Typography } from '@mui/material'; -import { AASListView } from './_components/AASListView'; -import { getTranslations } from 'next-intl/server'; +import { Box } from '@mui/material'; +import { AasListView } from 'app/[locale]/list/_components/AasListView'; +import AasListHeader from 'app/[locale]/list/_components/AasListHeader'; -export default async function page() { - const t = await getTranslations('aas-list'); +export default function Page() { return ( - - {t('header')} - - + + ); diff --git a/src/app/[locale]/viewer/_components/AASOverviewCard.tsx b/src/app/[locale]/viewer/_components/AASOverviewCard.tsx index 6c585e9..a1f4fe9 100644 --- a/src/app/[locale]/viewer/_components/AASOverviewCard.tsx +++ b/src/app/[locale]/viewer/_components/AASOverviewCard.tsx @@ -69,15 +69,17 @@ export function AASOverviewCard(props: AASOverviewCardProps) { const navigate = useRouter(); const [productImageUrl, setProductImageUrl] = useState(''); const { repositoryClient } = useApis(); - const [ registryAasData] = useRegistryAasState(); - + const [registryAasData] = useRegistryAasState(); + useAsyncEffect(async () => { if (!props.productImage) return; if (!isValidUrl(props.productImage!) && props.aas) { try { - if(registryAasData) { - const registryRepository = new AssetAdministrationShellRepositoryApi({basePath: registryAasData.aasRegistryRepositoryOrigin}); + if (registryAasData) { + const registryRepository = new AssetAdministrationShellRepositoryApi({ + basePath: registryAasData.aasRegistryRepositoryOrigin, + }); const image = await registryRepository.getThumbnailFromShell(props.aas.id); setProductImageUrl(URL.createObjectURL(image)); } else { diff --git a/src/app/[locale]/viewer/_components/ManualAasViewerInput.tsx b/src/app/[locale]/viewer/_components/ManualAasViewerInput.tsx index 606edd2..a6cfe82 100644 --- a/src/app/[locale]/viewer/_components/ManualAasViewerInput.tsx +++ b/src/app/[locale]/viewer/_components/ManualAasViewerInput.tsx @@ -45,7 +45,7 @@ export function ManualAASViewerInput(props: { focus: boolean }) { try { setIsLoading(true); - const { registrySearchResult, aasId} = await getAasFromExternalServices(val); + const { registrySearchResult, aasId } = await getAasFromExternalServices(val); const aas = registrySearchResult != null ? registrySearchResult.registryAas diff --git a/src/app/[locale]/viewer/_components/submodel/coffee-consumption/CoffeeConsumptionVisualizations.tsx b/src/app/[locale]/viewer/_components/submodel/coffee-consumption/CoffeeConsumptionVisualizations.tsx index 2af106f..1fd5731 100644 --- a/src/app/[locale]/viewer/_components/submodel/coffee-consumption/CoffeeConsumptionVisualizations.tsx +++ b/src/app/[locale]/viewer/_components/submodel/coffee-consumption/CoffeeConsumptionVisualizations.tsx @@ -258,7 +258,7 @@ export function CoffeeConsumptionVisualizations(props: { submodel: Submodel }) { )}

What are the benefits of tracking your products with the AAS?

- + {'take ); } diff --git a/src/app/env/provider.tsx b/src/app/env/provider.tsx index f64d975..0ca1dce 100644 --- a/src/app/env/provider.tsx +++ b/src/app/env/provider.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useState } from 'react'; import { EnvironmentalVariables, getEnv } from './env'; import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect'; -import {CenteredLoadingSpinner} from "../../components/basics/CenteredLoadingSpinner"; +import { CenteredLoadingSpinner } from 'components/basics/CenteredLoadingSpinner'; const initialValues: EnvironmentalVariables = { AAS_LIST_FEATURE_FLAG: false, diff --git a/src/components/basics/MnestixLogo.tsx b/src/components/basics/MnestixLogo.tsx index 5572c11..f5e7ba8 100644 --- a/src/components/basics/MnestixLogo.tsx +++ b/src/components/basics/MnestixLogo.tsx @@ -21,7 +21,7 @@ const StyledLogo = styled(Logo)<{ width?: string; height?: string }>(({ width, h export function MnestixLogo(props: MnestixLogoLogoProps) { // Differentiate between the logo variants const theme = useTheme(); - let color = theme.palette.common.white; + const color = theme.palette.common.white; return ; } diff --git a/src/layout/HeaderLogo.tsx b/src/layout/HeaderLogo.tsx index 9fabc9c..5c5f34a 100644 --- a/src/layout/HeaderLogo.tsx +++ b/src/layout/HeaderLogo.tsx @@ -1,7 +1,6 @@ import { Box, useTheme } from '@mui/material'; import { useRouter } from 'next/navigation'; -import { MnestixLogo } from "components/basics/MnestixLogo"; - +import { MnestixLogo } from 'components/basics/MnestixLogo'; export function HeaderLogo() { const theme = useTheme(); const navigate = useRouter(); diff --git a/src/lib/util/ToolTipText.tsx b/src/lib/util/ToolTipText.tsx new file mode 100644 index 0000000..ad25fed --- /dev/null +++ b/src/lib/util/ToolTipText.tsx @@ -0,0 +1,17 @@ +import { Box, Tooltip } from '@mui/material'; + +/** + * Shortens the property text and provides the full text in a tooltip. + */ +export function tooltipText(property: string | undefined, maxChars: number) { + if (!property) return ''; + else { + return property.length > maxChars ? ( + + {`${property.slice(0, maxChars)} (...)`} + + ) : ( + <>{property} + ); + } +} diff --git a/src/locale/de.json b/src/locale/de.json index 02547c2..2750dd0 100644 --- a/src/locale/de.json +++ b/src/locale/de.json @@ -1,7 +1,13 @@ { "comment": "wir werden hier deutsche Übersetzungs-Keys haben", "aas-list": { - "header": "AAS Liste" + "header": "AAS Liste", + "picture": "Bild", + "manufacturerHeading": "Hersteller Name", + "productDesignationHeading": "Hersteller Produktbezeichnung", + "assetIdHeading": "Asset ID", + "aasIdHeading": "AAS ID", + "productClassHeading": "Produktklasse" }, "dashboard": { "welcome-text": "Willkommen bei Mnestix", diff --git a/src/locale/en.json b/src/locale/en.json index e567128..30e2cb1 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -1,7 +1,13 @@ { "comment": "we will have english translation keys here", "aas-list": { - "header": "AAS List" + "header": "AAS List", + "picture": "Picture", + "manufacturerHeading": "Manufacturer Name", + "productDesignationHeading": "Manufacturer Product Designation", + "assetIdHeading": "Asset ID", + "aasIdHeading": "AAS ID", + "productClassHeading": "Product Class" }, "dashboard": { "welcome-text": "Welcome to Mnestix",