Creating Submodel Plugins#
This section explains how to create Submodel Plugins and SubmodelElement Plugins for the BaSyx AAS Web UI.
The goal of this page is to enable developers to implement a working plugin end-to-end: from choosing/finding the semantic ID to getting the right data and integrating with the existing UI architecture.
What is a Plugin?#
Plugins are Vue components that provide domain-specific visualization for a Submodel or SubmodelElement.
Two plugin categories exist:
Submodel Plugins – rendered when a Submodel is selected
SubmodelElement Plugins – rendered when a (nested) SubmodelElement is selected
Both are rendered in the same visualization panel.
Plugins are selected and rendered based on the semanticId of the currently selected element.
Where to Place Plugins#
Core plugins (contributed to BaSyx)#
Put plugins that should be part of the core Web UI into:
src/components/Plugins/Submodels/src/components/Plugins/SubmodelElements/
User / company-specific plugins#
If a plugin is intended for a specific deployment (company, demo, research prototype) and should not be merged into the core project, put it into:
src/UserPlugins/
Hint
During development, it is common to start in src/UserPlugins/ and move the plugin into src/components/Plugins/... once it is generic and intended for contribution.
Plugin Registration#
Plugins are discovered at startup via Vite’s import.meta.glob(...) and registered globally:
async function getVisualizations(app: AppType): Promise<PluginType[]> {
const pluginFileRecords = {
...import.meta.glob('./components/Plugins/Submodels/*.vue'),
...import.meta.glob('./components/Plugins/SubmodelElements/*.vue'),
...import.meta.glob('./UserPlugins/*.vue'),
};
const plugins = [] as Array<PluginType>;
for (const path in pluginFileRecords) {
const pluginName = path.split('/').pop()?.replace('.vue', '') || 'UnnamedPlugin';
const pluginComponent: any = await pluginFileRecords[path]();
if (pluginComponent.default.semanticId) {
app.component(pluginName, (pluginComponent.default || pluginComponent));
plugins.push({ name: pluginName, semanticId: pluginComponent.default.semanticId });
}
}
return plugins;
}
✅ Implications:
Adding a new
.vuefile in these folders is enough (no manual imports)New plugin files are picked up automatically by the dev server
The plugin is only registered if a
semanticIdis provided
Plugin Matching (Semantic IDs)#
How matching works#
Matching is performed only on the top-level semanticId of the currently selected Submodel/SubmodelElement.
Nested elements are not matched automatically
If you need nested logic, the plugin must handle it internally
Multiple plugins can match and will be rendered below each other (this is intended).
Supported semanticId formats#
A plugin may define:#
a single semanticId as a string
multiple semanticIds as an array of strings
The UI filters plugins roughly like this:
if (typeof plugin.semanticId === 'string') {
return checkSemanticId(submodelElementData.value, plugin.semanticId);
} else if (plugin.semanticId.constructor === Array) {
for (const pluginSemanticId of plugin.semanticId) {
if (checkSemanticId(submodelElementData.value, pluginSemanticId)) return true;
}
}
Matching strategies#
The matching logic follows the recommended AAS semantic identifier matching strategies and is implemented in SemanticIdUtils.ts:
For standardized submodels, use semantic IDs from the IDTA repository:
Warning
When designing a plugin, choose semanticIds that are stable and standardized whenever possible.
How Plugins Are Rendered#
Once plugins are loaded and filtered, they are dynamically instantiated:
<component
:is="plugin.name"
v-for="(plugin, index) in filteredPlugins"
:key="index"
:submodel-element-data="submodelElementData"
>
{{ plugin.name }}
</component>
Fallback behavior when no plugin matches:
either
GenericDataVisuis used (when in the Submodel view)or a “No available visualization” empty state is shown (when in the AAS view)
Plugin Contract#
Required: defineOptions and semanticId#
A plugin must define semanticId in defineOptions:
defineOptions({
name: 'MyPlugin',
semanticId: 'https://example.com/semantic-id',
});
Multiple semantic IDs:
defineOptions({
name: 'MyPlugin',
semanticId: [
'https://example.com/semantic-id-1',
'https://example.com/semantic-id-2',
],
});
Required: Prop submodelElementData#
All plugins receive the same prop:
const props = defineProps<{
submodelElementData: any;
}>();
For Submodel Plugins: this is the selected Submodel
For SubmodelElement Plugins: this is the selected SubmodelElement
Recommended Development Flow#
Pick the semanticId you want to support (prefer standardized IDs)
Create the plugin file in the right folder
Implement the UI using Vuetify (dense layout, responsive)
Reuse existing logic via composables and stores
Test manually in both viewer modes:
side-by-side (narrow panel ~200px)
fullscreen (wide layout)
Optionally add a unit test (see Testing & Quality Assurance)
Reusing Existing Logic (Best Practice)#
Use name/description helpers#
Do not implement naming fallbacks manually. Use the composables from the core UI:
nameToDisplay(...)descriptionToDisplay(...)
See Design Guidelines for details.
Accessing application state#
Plugins are expected to use stores and composables rather than reimplementing logic.
Recommended stores:
useAASStore()– primary store (selected node, paths, timestamps)useEnvironmentStore()– feature flags (e.g.ALLOW_EDITING)useNavigationStore()– optional (e.g. snackbars / notifications)useClipboardStore()– optional (clipboard support for editing)
Warning
Do not access the Infrastructure Store from plugins. Plugins should not depend on infrastructure internals.
Preparing nested data and ConceptDescriptions#
If your plugin needs:
stable
idfields for nested SMEscalculated
pathvaluestimestamppropagationConceptDescriptionsresolved and attached
use useSMHandling().setData(...).
What it does (high level):
assigns unique ids to SMEs
calculates and assigns paths recursively
propagates timestamps
optionally fetches and attaches ConceptDescriptions
Example usage:
import { computed, onMounted, ref } from 'vue';
import { useSMHandling } from '@/composables/AAS/SMHandling';
import { useAASStore } from '@/store/AASDataStore';
const aasStore = useAASStore();
const { setData } = useSMHandling();
const submodelData = ref<any>({});
const selectedNode = computed(() => aasStore.getSelectedNode);
onMounted(async () => {
if (!props.submodelElementData || Object.keys(props.submodelElementData).length === 0) return;
submodelData.value = await setData(
{ ...props.submodelElementData },
selectedNode.value.path,
true, // withConceptDescriptions
selectedNode.value.timestamp
);
});
Example: Minimal Submodel Plugin#
This minimal example demonstrates:
semanticIdregistrationusing
nameToDisplay(...)using
GenericDataVisuas a fallback visualizationpreparing nested data via
setData(...)
<template>
<v-container fluid class="pa-0">
<v-card class="mb-3">
<v-card-title>
<div class="text-subtitle-1">{{ nameToDisplay(submodelElementData) }}</div>
</v-card-title>
</v-card>
<v-card>
<v-card-text class="pt-1">
<GenericDataVisu :submodel-element-data="submodelElementData.submodelElements" />
</v-card-text>
</v-card>
</v-container>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useReferableUtils } from '@/composables/AAS/ReferableUtils';
import { useSMHandling } from '@/composables/AAS/SMHandling';
import { useAASStore } from '@/store/AASDataStore';
defineOptions({
name: 'HelloWorldPlugin',
semanticId: 'http://hello.world.de/plugin_submodel',
});
const props = defineProps<{ submodelElementData: any }>();
const aasStore = useAASStore();
const selectedNode = computed(() => aasStore.getSelectedNode);
const { setData } = useSMHandling();
const { nameToDisplay } = useReferableUtils();
const submodelData = ref<any>({});
onMounted(async () => {
if (!props.submodelElementData || Object.keys(props.submodelElementData).length === 0) return;
submodelData.value = await setData(
{ ...props.submodelElementData },
selectedNode.value.path,
false,
selectedNode.value.timestamp
);
});
</script>
Example: SubmodelElement Plugin (e.g., Chart)#
SubmodelElement plugins follow the same contract. They typically:
read and interpret the selected element value
update reactively when the prop changes
integrate with Vuetify theming (dark/light)
Use Vue best practices (e.g., watchers only where needed) and follow Design Guidelines.
UI Guidelines for Plugins#
Plugins must follow project UI conventions:
Prefer dense Vuetify components (
density="compact"where applicable)Consider both:
narrow visualization panel (~200px)
fullscreen viewer mode
Use Vuetify theming; do not hardcode corporate design colors
Hint
If you need a generic fallback visualization for nested structures, GenericDataVisu can render complex SME hierarchies via expansion panels.
Editing in Plugins#
If your plugin offers editing capabilities, it must respect the global feature flag:
ALLOW_EDITING=true
Check this via the Environment Store and hide/disable editing actions otherwise.
Testing Plugins (Optional)#
Component testing is currently not a major focus in this repository.
If you still want to test your plugin:
Focus on deterministic transformation logic (move into utils/composables)
Add unit tests under
src/tests/as described in:
Troubleshooting#
Plugin not showing up#
Checklist:
Is the file placed in one of the plugin directories?
Does it define
semanticIdviadefineOptions?Does the selected Submodel/SubmodelElement have a semanticId?
Does the semanticId match according to
SemanticIdUtils.ts?
Multiple plugins showing#
This is expected behavior when multiple plugins match. If you only want one visualization for a semanticId, ensure that only one plugin targets it.