diff --git a/aas-web-ui/src/components/AppNavigation/AppNavigation.vue b/aas-web-ui/src/components/AppNavigation/AppNavigation.vue index 653cab51..8d4fcfe7 100644 --- a/aas-web-ui/src/components/AppNavigation/AppNavigation.vue +++ b/aas-web-ui/src/components/AppNavigation/AppNavigation.vue @@ -311,7 +311,7 @@ ); return filteredAndOrderedModuleRoutes; }); - const showAASList = computed(() => ['AASViewer', 'AASEditor', 'SubmodelViewer'].includes(route.name as string)); + const showAASList = computed(() => ['AASViewer', 'AASEditor', 'AASSubmodelViewer'].includes(route.name as string)); const drawerState = computed(() => navigationStore.getDrawerState); const LogoPath = computed(() => { if (isDark.value && envStore.getEnvLogoDarkPath.trim().length > 0) { @@ -329,7 +329,7 @@ 'ComponentVisualization', 'Visualization', 'AASEditor', - 'SubmodelViewer', + 'AASSubmodelViewer', ].includes(route.name as string); }); diff --git a/aas-web-ui/src/components/AppNavigation/MainMenu.vue b/aas-web-ui/src/components/AppNavigation/MainMenu.vue index d8521561..1789abec 100644 --- a/aas-web-ui/src/components/AppNavigation/MainMenu.vue +++ b/aas-web-ui/src/components/AppNavigation/MainMenu.vue @@ -40,10 +40,10 @@ class="mt-3 py-2" nav :active="false" - :border="isActiveRoutePath('/submodelviewer')" - subtitle="View Submodels" - title="Submodel Viewer" - :to="isActiveRoutePath('/submodelviewer') ? '' : '/submodelviewer'" + :border="isActiveRoutePath('/aassmviewer')" + subtitle="View Submodel Visualizations of Asset Administration Shells" + title="AAS SM Visualizations" + :to="isActiveRoutePath('/aassmviewer') ? '' : '/aassmviewer'" @click="closeMenu"> @@ -51,6 +51,37 @@ + + + + + + + + + + + + + + @@ -76,7 +76,7 @@ const isMobile = computed(() => navigationStore.getIsMobile); const singleAas = computed(() => envStore.getSingleAas); const visualizationMode = computed(() => routesToVisualization.includes(route.name)); - const submodelViewerMode = computed(() => route.name === 'SubmodelViewer'); + const aasSubmodelViewerMode = computed(() => route.name === 'AASSubmodelViewer'); // Watchers watch( diff --git a/aas-web-ui/src/components/SubmodelElementView.vue b/aas-web-ui/src/components/SubmodelElementView.vue index a120ee30..3ea18200 100644 --- a/aas-web-ui/src/components/SubmodelElementView.vue +++ b/aas-web-ui/src/components/SubmodelElementView.vue @@ -3,8 +3,8 @@ + :is-editable="editorMode"> + :is-editable="editorMode"> + :is-editable="editorMode"> + :is-editable="editorMode"> + :is-editable="editorMode"> + :is-editable="editorMode"> @@ -134,7 +134,7 @@ + :is-editable="editorMode"> @@ -180,6 +180,7 @@ import { useSMEHandling } from '@/composables/AAS/SMEHandling'; import { useAASStore } from '@/store/AASDataStore'; import { useNavigationStore } from '@/store/NavigationStore'; + // Vue Router const route = useRoute(); @@ -204,7 +205,7 @@ const selectedAAS = computed(() => aasStore.getSelectedAAS); const selectedNode = computed(() => aasStore.getSelectedNode); const autoSync = computed(() => navigationStore.getAutoSync); - const aasEditorMode = computed(() => route.name === 'AASEditor'); + const editorMode = computed(() => ['AASEditor', 'SMEditor'].includes(route.name as string)); // Watchers watch( diff --git a/aas-web-ui/src/components/SubmodelElementViewAndVisualization.vue b/aas-web-ui/src/components/SubmodelElementViewAndVisualization.vue index 7952655c..5930a58e 100644 --- a/aas-web-ui/src/components/SubmodelElementViewAndVisualization.vue +++ b/aas-web-ui/src/components/SubmodelElementViewAndVisualization.vue @@ -31,8 +31,8 @@ @@ -41,7 +41,10 @@ import { computed, ref } from 'vue'; + import { useRoute } from 'vue-router'; import { useAASStore } from '@/store/AASDataStore'; + // Vue Router + const route = useRoute(); + //Stores const aasStore = useAASStore(); diff --git a/aas-web-ui/src/components/SubmodelElementVisualization.vue b/aas-web-ui/src/components/SubmodelElementVisualization.vue index 4eba96aa..a2239b5b 100644 --- a/aas-web-ui/src/components/SubmodelElementVisualization.vue +++ b/aas-web-ui/src/components/SubmodelElementVisualization.vue @@ -2,8 +2,8 @@ + :style=" + aasSubmodelViewerMode ? 'height: calc(100svh - 137px)' : 'height: calc(100svh - 202px)' + "> @@ -152,7 +154,7 @@ return plugins; }); const visualizationMode = computed(() => routesToVisualization.includes(route.name)); - const submodelViewerMode = computed(() => route.name === 'SubmodelViewer'); + const aasSubmodelViewerMode = computed(() => route.name === 'AASSubmodelViewer'); // Watchers watch( diff --git a/aas-web-ui/src/components/SubmodelTree.vue b/aas-web-ui/src/components/SubmodelTree.vue index e5d82f1b..f93516e5 100644 --- a/aas-web-ui/src/components/SubmodelTree.vue +++ b/aas-web-ui/src/components/SubmodelTree.vue @@ -70,13 +70,13 @@ variant="plain" v-bind="props" class="ml-n3" - :class="editMode ? 'mr-n3' : ''" + :class="editorMode ? 'mr-n3' : ''" @click="collapseTree()"> Collapse Submodel trees - + @@ -118,7 +118,11 @@ - + + - - - + @@ -219,6 +227,7 @@ import { useRoute } from 'vue-router'; import { useAASHandling } from '@/composables/AAS/AASHandling'; import { useReferableUtils } from '@/composables/AAS/ReferableUtils'; + import { useSMHandling } from '@/composables/AAS/SMHandling'; import { useAASStore } from '@/store/AASDataStore'; import { useEnvStore } from '@/store/EnvironmentStore'; import { useNavigationStore } from '@/store/NavigationStore'; @@ -229,6 +238,7 @@ // Composables const { fetchAasSmListById } = useAASHandling(); + const { fetchSmList } = useSMHandling(); const { nameToDisplay, descriptionToDisplay } = useReferableUtils(); // Stores @@ -272,14 +282,16 @@ const submodelRegistryURL = computed(() => navigationStore.getSubmodelRegistryURL); // get Submodel Registry URL from Store const selectedNode = computed(() => aasStore.getSelectedNode); // get the updated Treeview Node from Store const singleAas = computed(() => envStore.getSingleAas); // Get the single AAS state from the Store - const editMode = computed(() => route.name === 'AASEditor'); // Check if the current Route is the AAS Editor + const editorMode = computed(() => ['AASEditor', 'SMEditor'].includes(route.name as string)); const triggerTreeviewReload = computed(() => navigationStore.getTriggerTreeviewReload); // Reload the Treeview // Watchers watch( () => aasRegistryURL.value, () => { - submodelTree.value = []; + if (!['SMViewer', 'SMEditor'].includes(route.name as string)) { + submodelTree.value = []; + } } ); @@ -293,8 +305,10 @@ watch( () => selectedAAS.value, () => { - submodelTree.value = []; - initialize(); + if (!['SMViewer', 'SMEditor'].includes(route.name as string)) { + submodelTree.value = []; + initialize(); + } } ); @@ -312,7 +326,11 @@ }); async function initialize(): Promise { - if (!selectedAAS.value || Object.keys(selectedAAS.value).length === 0) { + // console.log(selectedNode.value); + if ( + !['SMEditor', 'SMViewer'].includes(route.name as string) && + (!selectedAAS.value || Object.keys(selectedAAS.value).length === 0) + ) { submodelTree.value = []; return; } @@ -320,14 +338,24 @@ treeLoading.value = true; try { - const submodels: Array = await fetchAasSmListById(selectedAAS.value.id); + let submodels: Array = []; + + if (['SMEditor', 'SMViewer'].includes(route.name as string)) { + submodels = await fetchSmList(); + } else { + submodels = await fetchAasSmListById(selectedAAS.value.id); + } const sortedSubmodels = submodels.sort((a, b) => a.id.localeCompare(b.id)); let processedList = [] as Array; processedList = sortedSubmodels.map((submodel: any) => { // Assumes submodel.path is already set for top-level nodes - if (Array.isArray(submodel.submodelElements) && submodel.submodelElements.length) { + if ( + submodel.submodelElements && + Array.isArray(submodel.submodelElements) && + submodel.submodelElements.length > 0 + ) { submodel.children = prepareForTree(submodel.submodelElements, submodel); submodel.showChildren = shouldExpandNode(submodel.path); return submodel; diff --git a/aas-web-ui/src/components/UIComponents/Treeview.vue b/aas-web-ui/src/components/UIComponents/Treeview.vue index e8e44d1e..8885d6bc 100644 --- a/aas-web-ui/src/components/UIComponents/Treeview.vue +++ b/aas-web-ui/src/components/UIComponents/Treeview.vue @@ -7,7 +7,7 @@ :style="{ 'padding-left': depth * 22 + 'px' }" density="compact" class="py-0" - :class="editMode ? 'pr-0' : ''" + :class="editorMode ? 'pr-0' : ''" nav slim color="primary" @@ -86,7 +86,7 @@ size="x-small" :style="{ marginRight: - isHovering && (!editMode || canElementAddSubmodelElement(item)) + isHovering && (!editorMode || canElementAddSubmodelElement(item)) ? '8px' : '-24px', transition: 'margin 0.3s ease', @@ -98,7 +98,7 @@ @@ -120,7 +120,7 @@ @@ -143,7 +143,7 @@ - + - + - + @@ -311,7 +311,7 @@ // Computed Properties const selectedNode = computed(() => aasStore.getSelectedNode); - const editMode = computed(() => route.name === 'AASEditor'); + const editorMode = computed(() => ['AASEditor', 'SMEditor'].includes(route.name as string)); const isMobile = computed(() => navigationStore.getIsMobile); const copyIconAsRef = computed(() => copyIcon); diff --git a/aas-web-ui/src/composables/AAS/SMHandling.ts b/aas-web-ui/src/composables/AAS/SMHandling.ts index 5008b4b4..92ec9e54 100644 --- a/aas-web-ui/src/composables/AAS/SMHandling.ts +++ b/aas-web-ui/src/composables/AAS/SMHandling.ts @@ -13,6 +13,7 @@ export function useSMHandling() { getSmEndpointById: getSmEndpointByIdFromRegistry, } = useSMRegistryClient(); const { + fetchSmList: fetchSmListFromRepo, fetchSm: fetchSmFromRepo, getSmEndpointById: getSmEndpointByIdFromRepo, fetchSme: fetchSmeFromRepo, @@ -44,6 +45,29 @@ export function useSMHandling() { return smDescriptors; } + /** + * Fetches a list of all available Submodels (SM). + * + * @async + * @returns {Promise>} A promise that resolves to an array of SM. + * An empty array is returned if the request fails or no AAS Descriptors are found. + */ + async function fetchSmList(): Promise> { + const failResponse = [] as Array; + + let smList = await fetchSmListFromRepo(); + + if (!smList || !Array.isArray(smList) || smList.length === 0) return failResponse; + + smList = smList.map((aas: any) => { + aas.timestamp = formatDate(new Date()); + aas.path = getSmEndpointByIdFromRepo(aas.id); + return aas; + }); + + return smList; + } + /** * Fetches an Submodel (SM) Descriptor by the provided SM ID. * @@ -309,6 +333,7 @@ export function useSMHandling() { fetchSmById, fetchSmDescriptorList, fetchSmDescriptor, + fetchSmList, setData, }; } diff --git a/aas-web-ui/src/pages/SubmodelViewer.vue b/aas-web-ui/src/pages/AASSubmodelViewer.vue similarity index 100% rename from aas-web-ui/src/pages/SubmodelViewer.vue rename to aas-web-ui/src/pages/AASSubmodelViewer.vue diff --git a/aas-web-ui/src/pages/SMEditor.vue b/aas-web-ui/src/pages/SMEditor.vue new file mode 100644 index 00000000..6436e3f4 --- /dev/null +++ b/aas-web-ui/src/pages/SMEditor.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + mdi-pan-left + + mdi-pan-right + + + + + + + + + + diff --git a/aas-web-ui/src/pages/SMViewer.vue b/aas-web-ui/src/pages/SMViewer.vue new file mode 100644 index 00000000..6436e3f4 --- /dev/null +++ b/aas-web-ui/src/pages/SMViewer.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + mdi-pan-left + + mdi-pan-right + + + + + + + + + + diff --git a/aas-web-ui/src/router.ts b/aas-web-ui/src/router.ts index 359ecac8..e9c0344e 100644 --- a/aas-web-ui/src/router.ts +++ b/aas-web-ui/src/router.ts @@ -7,12 +7,14 @@ import { useAASHandling } from '@/composables/AAS/AASHandling'; import { useSMEHandling } from '@/composables/AAS/SMEHandling'; import { useRouteHandling } from '@/composables/routeHandling'; import AASEditor from '@/pages/AASEditor.vue'; +import AASSubmodelViewer from '@/pages/AASSubmodelViewer.vue'; import AASViewer from '@/pages/AASViewer.vue'; import About from '@/pages/About.vue'; import Dashboard from '@/pages/Dashboard.vue'; import DashboardGroup from '@/pages/DashboardGroup.vue'; import Page404 from '@/pages/Page404.vue'; -import SubmodelViewer from '@/pages/SubmodelViewer.vue'; +import SMEditor from '@/pages/SMEditor.vue'; +import SMViewer from '@/pages/SMViewer.vue'; import { useAASStore } from '@/store/AASDataStore'; import { useEnvStore } from '@/store/EnvironmentStore'; import { useNavigationStore } from '@/store/NavigationStore'; @@ -36,11 +38,23 @@ const staticRoutes: Array = [ { path: '/componentvisualization', name: 'ComponentVisualization', component: ComponentVisualization }, { path: '/visu', name: 'Visualization', component: ComponentVisualization, meta: { name: 'Visualization' } }, { - path: '/submodelviewer', - name: 'SubmodelViewer', - component: SubmodelViewer, + path: '/aassmviewer', + name: 'AASSubmodelViewer', + component: AASSubmodelViewer, + meta: { name: 'AAS Submodel Viewer', subtitle: 'Visualize Submodels of AAS' }, + }, + { + path: '/smviewer', + name: 'SMViewer', + component: SMViewer, meta: { name: 'Submodel Viewer', subtitle: 'Visualize Submodels' }, }, + { + path: '/smeditor', + name: 'SMEditor', + component: SMEditor, + meta: { name: 'SM Editor', subtitle: 'Edit Submodels' }, + }, { path: '/about', name: 'About', @@ -126,23 +140,31 @@ export async function createAppRouter(): Promise { const { idRedirectHandled } = useRouteHandling(); // Data - const routesForMobile: Array = ['AASList', 'SubmodelList', 'Visualization']; + const routesForMobile: Array = ['AASList', 'SMList', 'SubmodelList', 'Visualization']; const routesForDesktop: Array = [ 'AASViewer', 'AASEditor', - 'SubmodelViewer', + 'AASSubmodelViewer', + 'SMViewer', + 'SMEditor', 'Visualization', ]; const routesToSaveAndLoadUrlQuery: Array = [ - 'AASList', - 'AASEditor', 'AASViewer', - 'SubmodelViewer', + 'AASEditor', + 'AASList', + 'AASSubmodelViewer', + 'SMViewer', + 'SMEditor', + 'SMList', 'Visualization', ]; + const routesSMViewerEditor: Array = ['SMViewer', 'SMEditor', 'SMList']; const routesStayOnPages: Array = ['About', 'NotFound404']; const routesDesktopToAASViewer: Array = ['AASList', 'SubmodelList']; - const routesMobileToAASList: Array = ['AASViewer', 'AASEditor', 'SubmodelViewer']; + const routesDesktopToSMViewer: Array = ['SMList']; + const routesMobileToAASList: Array = ['AASViewer', 'AASEditor', 'AASSubmodelViewer']; + const routesMobileToSMList: Array = ['SMViewer', 'SMEditor']; const routesToVisualization: Array = ['ComponentVisualization']; const possibleGloBalAssetIdQueryParameter = ['globalAssetId', 'globalassetid']; @@ -199,9 +221,9 @@ export async function createAppRouter(): Promise { if (Object.keys(from.query).length > 0) { const queryToDispatch = from.query; - // Strip idShortPath in case of switching to Submodel Viewer + // Strip idShortPath in case of switching to AAS Submodel Viewer const queryPathToDispatch = queryToDispatch?.path as string; - if (to.name === 'SubmodelViewer' && queryPathToDispatch && queryPathToDispatch.trim() !== '') { + if (to.name === 'AASSubmodelViewer' && queryPathToDispatch && queryPathToDispatch.trim() !== '') { queryToDispatch.path = queryPathToDispatch.trim().split('/submodel-elements/')[0]; } @@ -255,6 +277,14 @@ export async function createAppRouter(): Promise { // Redirect to 'AASList' with query next({ name: 'AASList', query: to.query }); return; + } else if ( + routesMobileToSMList.includes(to.name) || + (to.path.includes('/modules/') && !to.meta.isMobileModule) + ) { + // Redirect to 'SMList' with query + if (to.query.aas) delete to.query.ass; + next({ name: 'SMList', query: to.query }); + return; } else if (routesToVisualization.includes(to.name)) { // Redirect to 'Visualization' with query next({ name: 'Visualization', query: to.query }); @@ -276,6 +306,15 @@ export async function createAppRouter(): Promise { // Redirect to 'AASViewer' with query next({ name: 'AASViewer', query: to.query }); return; + } else if ( + routesDesktopToSMViewer.includes(to.name) || + (to.name === 'SMEditor' && !allowEditing.value) || + (to.path.includes('/modules/') && !to.meta.isDesktopModule) + ) { + // Redirect to 'SMViewer' with query + if (to.query.aas) delete to.query.ass; + next({ name: 'SMViewer', query: to.query }); + return; } else if (routesToVisualization.includes(to.name)) { // Redirect to 'Visualization' with query next({ name: 'Visualization', query: to.query }); @@ -283,13 +322,23 @@ export async function createAppRouter(): Promise { } } + if (to.query.aas && routesSMViewerEditor.includes(to.name)) { + delete to.query.aas; + const updatedRoute = Object.assign({}, to, { + query: to.query, + }); + next(updatedRoute); + return; + } + // Fetch and dispatch AAS if (to.query.aas && to.query.aas !== '' && from.query.aas !== to.query.aas) { const aas = await fetchAndDispatchAas(to.query.aas as string); if (!aas || Object.keys(aas).length === 0) { // Remove aas query for not available AAS endpoint + if (to.query.ass) delete to.query.aas; const updatedRoute = Object.assign({}, to, { - query: {}, + query: to.query, }); next(updatedRoute); return; @@ -299,12 +348,17 @@ export async function createAppRouter(): Promise { } // Fetch and dispatch SM/SME - if (to.query.path && to.query.path !== '' && from.query.path !== to.query.path) { + if ( + to.query.path && + to.query.path !== '' && + (from.query.path !== to.query.path || ['SMViewer', 'SMEditor'].includes(to.name as string)) + ) { const sme = await fetchAndDispatchSme(to.query.path as string, true); if (!sme || Object.keys(sme).length === 0) { // Remove path query for not available SME path + if (to.query.path) delete to.query.path; const updatedRoute = Object.assign({}, to, { - query: { aas: to.query.aas }, + query: to.query, }); next(updatedRoute); return;