Skip to content

Commit cb4aaab

Browse files
Experimental(product view) add product view (#501)
* feat: Introduce experimental product view feature - Updated environment variables to replace PRODUCT_VIEW_FEATURE_FLAG with EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG. - Modified relevant components to utilize the new experimental feature flag. - Enhanced navigation logic to support the new product view feature. - Added KeyFactsBox component to display product classifications and markings. - Updated tests to reflect changes in product overview and action menu components. - Documented the new experimental feature in the configuration settings. * fix(env): disable experimental feature flags for stability * fix env variable and build --------- Co-authored-by: Nils Rothamel <nils.rothamel@xitaso.com>
1 parent 6e57551 commit cb4aaab

File tree

18 files changed

+391
-158
lines changed

18 files changed

+391
-158
lines changed

.env.local

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ AAS_LIST_FEATURE_FLAG= "true"
88
TRANSFER_FEATURE_FLAG= "false"
99
BASYX_RBAC_ENABLED= "false"
1010
WHITELIST_FEATURE_FLAG= "false"
11-
PRODUCT_VIEW_FEATURE_FLAG= "false"
1211
MNESTIX_V2_ENABLED= "true"
1312

1413
AAS_REPO_API_URL= "http://localhost:5065/repo"
@@ -35,3 +34,6 @@ BASYX_RBAC_SEC_SM_API_URL= "http://localhost:8089"
3534

3635
IMPRINT_URL= ""
3736
DATA_PRIVACY_URL= ""
37+
38+
EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG= "false"
39+
EXPERIMENTAL_HIGHLIGHT_DATA_FLAG= "false"

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ FROM base AS production
3030
WORKDIR /app
3131

3232
ENV NODE_ENV=production
33-
RUN yarn add prisma
33+
RUN yarn add prisma@6.19.0
3434

3535
RUN addgroup -g 1001 -S nodejs
3636
RUN adduser -S nextjs -u 1001

compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030
AAS_LIST_FEATURE_FLAG: 'true'
3131
TRANSFER_FEATURE_FLAG: 'false'
3232
COMPARISON_FEATURE_FLAG: 'true'
33-
PRODUCT_VIEW_FEATURE_FLAG: 'false'
33+
EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG: 'false'
3434
AUTHENTICATION_FEATURE_FLAG: 'false'
3535
LOCK_TIMESERIES_PERIOD_FEATURE_FLAG: 'true'
3636
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-verySecureNextAuthSecret}

src/app/EnvProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const initialEnvValues: EnvironmentalVariables = {
1010
AAS_LIST_FEATURE_FLAG: false,
1111
COMPARISON_FEATURE_FLAG: false,
1212
TRANSFER_FEATURE_FLAG: false,
13-
PRODUCT_VIEW_FEATURE_FLAG: false,
13+
EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG: false,
1414
KEYCLOAK_ENABLED: false,
1515
LOCK_TIMESERIES_PERIOD_FEATURE_FLAG: false,
1616
DISCOVERY_API_URL: undefined,

src/app/[locale]/list/_components/AasListTableRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const AasListTableRow = (props: AasTableRowProps) => {
6666

6767
const navigateToAas = (listEntry: ListEntityDto) => {
6868
const baseUrl = window.location.origin;
69-
const pageToGo = env.PRODUCT_VIEW_FEATURE_FLAG ? '/product' : '/viewer';
69+
const pageToGo = env.EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG ? '/product' : '/viewer';
7070

7171
// Only send repoUrl parameter if it's a repository, not a registry
7272
const repoUrlParam = connectionType === 'repository' && repository.url ? `?repoUrl=${repository.url}` : '';

src/app/[locale]/product/[base64AasId]/page.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,42 @@
11
'use client';
2-
import { CurrentAasContextProvider } from 'components/contexts/CurrentAasContext';
2+
3+
import { Box } from '@mui/material';
34
import { safeBase64Decode } from 'lib/util/Base64Util';
45
import { useParams, useSearchParams } from 'next/navigation';
5-
import { ProductViewer } from '../_components/ProductViewer';
6-
import { useShowError } from 'lib/hooks/UseShowError';
7-
import { Box } from '@mui/material';
86
import { NoSearchResult } from 'components/basics/detailViewBasics/NoSearchResult';
7+
import { CurrentAasContextProvider } from 'components/contexts/CurrentAasContext';
8+
import { useShowError } from 'lib/hooks/UseShowError';
9+
import { ProductViewer } from '../_components/ProductViewer';
910

10-
export default function () {
11+
export default function Page() {
1112
const { showError } = useShowError();
1213
const params = useParams<{ base64AasId: string }>();
13-
const base64AasId = (params.base64AasId || '').replace(/=+$|[%3D]+$/, '');
14-
const searchParams = useSearchParams();
15-
const encodedRepoUrl = searchParams.get('repoUrl');
14+
const base64AasId = decodeURIComponent(params.base64AasId).replace(/=+$|[%3D]+$/, '');
15+
const encodedRepoUrl = useSearchParams().get('repoUrl');
1616
const repoUrl = encodedRepoUrl ? decodeURI(encodedRepoUrl) : undefined;
17-
17+
const infrastructureName = useSearchParams().get('infrastructure') || undefined;
1818
try {
1919
const aasIdDecoded = safeBase64Decode(base64AasId);
2020

2121
return (
22-
<CurrentAasContextProvider aasId={aasIdDecoded} repoUrl={repoUrl}>
22+
<CurrentAasContextProvider aasId={aasIdDecoded} repoUrl={repoUrl} infrastructureName={infrastructureName}>
2323
<ProductViewer />
2424
</CurrentAasContextProvider>
2525
);
2626
} catch (e) {
2727
showError(e);
2828
return (
29-
<Box sx={{ padding: 2 }}>
29+
<Box
30+
sx={{
31+
display: 'flex',
32+
flexDirection: 'column',
33+
gap: '30px',
34+
alignItems: 'center',
35+
width: '100vw',
36+
marginBottom: '50px',
37+
marginTop: '20px',
38+
}}
39+
>
3040
<NoSearchResult base64AasId={base64AasId} />
3141
</Box>
3242
);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Box, Chip, Tooltip, Typography, styled } from '@mui/material';
2+
import { useTranslations } from 'next-intl';
3+
import EClassIcon from 'assets/product/eclass.svg';
4+
import VecIcon from 'assets/product/vec_classification.svg';
5+
import LabelIcon from '@mui/icons-material/Label';
6+
/**
7+
* Type definition for product classification
8+
*/
9+
export interface ProductClassification {
10+
ProductClassificationSystem?: string;
11+
ProductClassId?: string;
12+
}
13+
14+
interface ProductClassificationInfoBoxProps {
15+
productClassifications: ProductClassification[];
16+
markings: string[];
17+
}
18+
19+
const StyledBox = styled(Box)(({ theme }) => ({
20+
display: 'flex',
21+
alignItems: 'center',
22+
flexWrap: 'wrap',
23+
gap: theme.spacing(2),
24+
padding: theme.spacing(2, 3),
25+
width: '100%',
26+
}));
27+
28+
const LabelContainer = styled(Box)(({ theme }) => ({
29+
display: 'flex',
30+
alignItems: 'center',
31+
marginRight: theme.spacing(4),
32+
}));
33+
34+
const ValueContainer = styled(Box)(({ theme }) => ({
35+
display: 'flex',
36+
alignItems: 'center',
37+
marginTop: theme.spacing(0.5),
38+
marginBottom: theme.spacing(0.5),
39+
}));
40+
41+
export function KeyFactsBox({
42+
productClassifications,
43+
markings,
44+
}: ProductClassificationInfoBoxProps) {
45+
const t = useTranslations('pages.productViewer');
46+
const validClassifications = productClassifications ?? [];
47+
48+
// Return null if there are no valid classifications or markings to show
49+
if (validClassifications.length === 0 && markings.length === 0) {
50+
return null;
51+
}
52+
53+
const vecIcon = <VecIcon color='primary'></VecIcon>;
54+
const eClassIcon = <EClassIcon color='primary'></EClassIcon>;
55+
56+
return (
57+
<Box>
58+
<StyledBox bgcolor={'grey.100'}>
59+
<LabelContainer>
60+
<Typography sx={{ borderBottom: '2px solid', borderColor: 'primary' }} color="primary" fontWeight="bold">
61+
{t('summary')}
62+
</Typography>
63+
</LabelContainer>
64+
{validClassifications && validClassifications.map((classification, index) => (
65+
<ValueContainer key={`classification-${index}`}>
66+
<Tooltip title={classification.ProductClassId}>
67+
<Chip
68+
sx={{ color: 'primary.main', backgroundColor: 'grey.200', borderRadius: 5, padding: 0.5 }}
69+
label={classification.ProductClassificationSystem || 'Classification'}
70+
icon={classification.ProductClassificationSystem === 'ECLASS' ? eClassIcon : vecIcon}>
71+
</Chip>
72+
</Tooltip>
73+
</ValueContainer>
74+
))}
75+
{markings && markings.map((markingText, index) => (
76+
<ValueContainer key={`classification-${index}`} data-testid="markings-element">
77+
<Chip
78+
sx={{ color: 'primary.main', backgroundColor: 'grey.200', borderRadius: 5, padding: 0.5 }}
79+
label={markingText}
80+
icon={<LabelIcon color='primary' />}>
81+
</Chip>
82+
</ValueContainer>
83+
))}
84+
</StyledBox>
85+
</Box>
86+
);
87+
}

src/app/[locale]/product/_components/ProductActionMenu.tsx

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,27 @@ import { useState } from 'react';
55
import { useTranslations } from 'use-intl';
66
import { encodeBase64 } from 'lib/util/Base64Util';
77
import { useEnv } from 'app/EnvProvider';
8-
import { SubmodelOrIdReference, useCurrentAasContext } from 'components/contexts/CurrentAasContext';
8+
import { SubmodelOrIdReference } from 'components/contexts/CurrentAasContext';
99
import { useShowError } from 'lib/hooks/UseShowError';
1010
import { AssetAdministrationShell } from 'lib/api/aas/models';
11-
import {
12-
checkIfInfrastructureHasSerializationEndpoints,
13-
serializeAasFromInfrastructure,
14-
} from 'lib/services/serialization-service/serializationActions';
15-
import { useAsyncEffect } from 'lib/hooks/UseAsyncEffect';
11+
import { serializeAasFromInfrastructure } from 'lib/services/serialization-service/serializationActions';
12+
import { useNotificationSpawner } from 'lib/hooks/UseNotificationSpawner';
1613

1714
type ActionMenuProps = {
1815
readonly aas: AssetAdministrationShell | null;
16+
readonly repositoryUrl?: string;
17+
readonly infrastructureName?: string;
1918
readonly submodels: SubmodelOrIdReference[] | null;
20-
readonly repositoryURL?: string;
2119
readonly className?: string;
2220
};
2321

24-
export function ActionMenu({ aas, submodels, repositoryURL, className }: ActionMenuProps) {
22+
export function ActionMenu({ aas, submodels, className, infrastructureName, repositoryUrl }: ActionMenuProps) {
2523
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
2624
const navigate = useRouter();
2725
const t = useTranslations('pages');
2826
const env = useEnv();
27+
const { spawn } = useNotificationSpawner();
2928
const { showError } = useShowError();
30-
const currentAASContext = useCurrentAasContext();
31-
const [showDownloadButton, setShowDownloadButton] = useState(false);
3229

3330
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
3431
setAnchorEl(event.currentTarget);
@@ -47,55 +44,49 @@ export function ActionMenu({ aas, submodels, repositoryURL, className }: ActionM
4744

4845
const goToAASView = () => {
4946
if (aas?.id) {
50-
navigate.push(`/viewer/${encodeBase64(aas?.id)}`);
47+
navigate.push(
48+
`/viewer/${encodeBase64(aas?.id)}?repoUrl=${encodeURIComponent(repositoryUrl || '')}&infrastructure=${infrastructureName || ''}`,
49+
);
5150
}
5251
handleMenuClose();
5352
};
5453

5554
async function downloadAAS() {
56-
if (!aas?.id || !currentAASContext.infrastructureName) {
57-
handleMenuClose();
58-
return;
59-
}
60-
if (!repositoryURL) {
61-
showError(t('productViewer.actions.downloadErrorNoRepo'));
62-
handleMenuClose();
55+
if (!aas?.id || !infrastructureName) {
56+
showError(t('aasViewer.errors.downloadError'));
6357
return;
6458
}
6559
const submodelIds = Array.isArray(submodels) ? submodels.map((s) => s.id) : [];
6660
try {
67-
const response = await serializeAasFromInfrastructure(
68-
aas?.id,
69-
submodelIds,
70-
currentAASContext.infrastructureName,
71-
);
61+
const response = await serializeAasFromInfrastructure(aas?.id, submodelIds, infrastructureName);
7262
if (response.isSuccess && response.result) {
73-
const url = window.URL.createObjectURL(response.result.blob);
63+
const { blob, endpointUrl, infrastructureName: infra } = response.result;
64+
const url = window.URL.createObjectURL(blob);
7465
const link = document.createElement('a');
7566
link.href = url;
7667
link.setAttribute('download', `${aas?.idShort}.aasx`);
7768
document.body.appendChild(link);
7869
link.click();
7970
link.parentNode?.removeChild(link);
8071
window.URL.revokeObjectURL(url);
72+
73+
// Show success message with endpoint information
74+
spawn({
75+
title: t('aasViewer.actions.download'),
76+
message: t('aasViewer.messages.downloadSuccess', {
77+
endpoint: endpointUrl,
78+
infrastructure: infra,
79+
}),
80+
severity: 'success',
81+
});
8182
} else if (!response.isSuccess) {
8283
showError(response.message);
8384
}
8485
} catch {
85-
showError(t('productViewer.actions.downloadError'));
86+
showError(t('aasViewer.errors.downloadError'));
8687
}
87-
handleMenuClose();
8888
}
8989

90-
useAsyncEffect(async () => {
91-
if (currentAASContext && currentAASContext.infrastructureName) {
92-
const serializationEndpointAvailable = await checkIfInfrastructureHasSerializationEndpoints(
93-
currentAASContext.infrastructureName,
94-
);
95-
setShowDownloadButton(serializationEndpointAvailable.isSuccess);
96-
}
97-
}, []);
98-
9990
return (
10091
<>
10192
<IconButton
@@ -114,16 +105,14 @@ export function ActionMenu({ aas, submodels, repositoryURL, className }: ActionM
114105
{t('productViewer.actions.compareButton')}
115106
</MenuItem>
116107
)}
117-
{env.PRODUCT_VIEW_FEATURE_FLAG && (
108+
{env.EXPERIMENTAL_PRODUCT_VIEW_FEATURE_FLAG && (
118109
<MenuItem onClick={goToAASView} data-testid="detail-aas-view-button">
119110
{t('productViewer.actions.toAasView')}
120111
</MenuItem>
121112
)}
122-
{showDownloadButton && (
123-
<MenuItem onClick={downloadAAS} data-testid="detail-download-button">
124-
{t('productViewer.actions.download')}
125-
</MenuItem>
126-
)}
113+
<MenuItem onClick={downloadAAS} data-testid="detail-download-button">
114+
{t('productViewer.actions.download')}
115+
</MenuItem>
127116
</Menu>
128117
</>
129118
);

0 commit comments

Comments
 (0)