diff --git a/src/components/EditMenu.vue b/src/components/EditMenu.vue
index 90ecd3eba..6771b4907 100644
--- a/src/components/EditMenu.vue
+++ b/src/components/EditMenu.vue
@@ -485,27 +485,22 @@
class="flex items-center justify-between w-full h-full gap-3 overflow-x-auto text-white -mb-1 pr-2 cursor-pointer"
>
-
+
{{
- widgetType.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
+ widget.name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
}}
@@ -648,6 +643,7 @@ import URLVideoPlayerImg from '@/assets/widgets/URLVideoPlayer.png'
import VideoPlayerImg from '@/assets/widgets/VideoPlayer.png'
import VirtualHorizonImg from '@/assets/widgets/VirtualHorizon.png'
import { useInteractionDialog } from '@/composables/interactionDialog'
+import { getWidgetsFromBlueOS } from '@/libs/blueos'
import { MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum'
import { isHorizontalScroll } from '@/libs/utils'
import { useAppInterfaceStore } from '@/stores/appInterface'
@@ -657,6 +653,8 @@ import {
type View,
type Widget,
CustomWidgetElementType,
+ ExternalWidgetSetupInfo,
+ InternalWidgetSetupInfo,
MiniWidgetType,
WidgetType,
} from '@/types/widgets'
@@ -685,6 +683,8 @@ const toggleDial = (): void => {
const forceUpdate = ref(0)
+const ExternalWidgetSetupInfos = ref([])
+
watch(
() => store.currentView.widgets,
() => {
@@ -712,7 +712,31 @@ watch(
}
)
-const availableWidgetTypes = computed(() => Object.values(WidgetType))
+const availableWidgetTypes = computed(() =>
+ Object.values(WidgetType).map((widgetType) => {
+ return {
+ component: widgetType,
+ name: widgetType,
+ icon: widgetImages[widgetType] as string,
+ options: {},
+ }
+ })
+)
+
+const allAvailableWidgets = computed(() => {
+ return [
+ ...ExternalWidgetSetupInfos.value.map((widget) => ({
+ component: WidgetType.IFrame,
+ icon: widget.iframe_icon,
+ name: widget.name,
+ options: {
+ source: widget.iframe_url,
+ },
+ })),
+ ...availableInternalWidgets.value,
+ ]
+})
+
const availableMiniWidgetTypes = computed(() =>
Object.values(MiniWidgetType).map((widgetType) => ({
component: widgetType,
@@ -925,6 +949,10 @@ const miniWidgetsContainerOptions = ref({
})
useDraggable(availableMiniWidgetsContainer, availableMiniWidgetTypes, miniWidgetsContainerOptions)
+const getExternalWidgetSetupInfos = async (): Promise => {
+ ExternalWidgetSetupInfos.value = await getWidgetsFromBlueOS()
+}
+
// @ts-ignore: Documentation is not clear on what generic should be passed to 'UseDraggableOptions'
const customWidgetElementContainerOptions = ref({
animation: '150',
@@ -938,6 +966,7 @@ useDraggable(
)
onMounted(() => {
+ getExternalWidgetSetupInfos()
const widgetContainers = [
availableWidgetsContainer.value,
availableMiniWidgetsContainer.value,
@@ -989,7 +1018,7 @@ const onRegularWidgetDragStart = (event: DragEvent): void => {
}
}
-const onRegularWidgetDragEnd = (widgetType: WidgetType): void => {
+const onRegularWidgetDragEnd = (widgetType: ExtendedWidget): void => {
store.addWidget(widgetType, store.currentView)
const widgetCards = document.querySelectorAll('[draggable="true"]')
diff --git a/src/libs/blueos.ts b/src/libs/blueos.ts
index 0eaba093a..3a71680f9 100644
--- a/src/libs/blueos.ts
+++ b/src/libs/blueos.ts
@@ -1,5 +1,26 @@
import ky, { HTTPError } from 'ky'
+import { useMainVehicleStore } from '@/stores/mainVehicle'
+import { ExternalWidgetSetupInfo } from '@/types/widgets'
+
+/**
+ * Cockpits extra json format. Taken from extensions in BlueOS and (eventually) other places
+ */
+interface ExtrasJson {
+ /**
+ * The version of the cockpit API that the extra json is compatible with
+ */
+ target_cockpit_api_version: string
+ /**
+ * The target system that the extra json is compatible with, in our case, "cockpit"
+ */
+ target_system: string
+ /**
+ * A list of widgets that the extra json contains. src/types/widgets.ts
+ */
+ widgets: ExternalWidgetSetupInfo[]
+}
+
export const NoPathInBlueOsErrorName = 'NoPathInBlueOS'
const defaultTimeout = 10000
@@ -30,6 +51,46 @@ export const getKeyDataFromCockpitVehicleStorage = async (
return await getBagOfHoldingFromVehicle(vehicleAddress, `cockpit/${storageKey}`)
}
+export const getWidgetsFromBlueOS = async (): Promise => {
+ const vehicleStore = useMainVehicleStore()
+
+ // Wait until we have a global address
+ while (vehicleStore.globalAddress === undefined) {
+ await new Promise((r) => setTimeout(r, 1000))
+ }
+ const options = { timeout: defaultTimeout, retry: 0 }
+ const services = (await ky
+ .get(`http://${vehicleStore.globalAddress}/helper/v1.0/web_services`, options)
+ .json()) as Record
+ // first we gather all the extra jsons with the cockpit key
+ const extraWidgets = await services.reduce(
+ async (accPromise: Promise, service: Record) => {
+ const acc = await accPromise
+ const worksInRelativePaths = service.metadata?.works_in_relative_paths
+ if (service.metadata?.extras?.cockpit === undefined) {
+ return acc
+ }
+ const baseUrl = worksInRelativePaths
+ ? `http://${vehicleStore.globalAddress}/extensionv2/${service.metadata.sanitized_name}`
+ : `http://${vehicleStore.globalAddress}:${service.port}`
+ const fullUrl = baseUrl + service.metadata?.extras?.cockpit
+
+ const extraJson: ExtrasJson = await ky.get(fullUrl, options).json()
+ const widgets: ExternalWidgetSetupInfo[] = extraJson.widgets.map((widget) => {
+ return {
+ ...widget,
+ iframe_url: baseUrl + widget.iframe_url,
+ iframe_icon: baseUrl + widget.iframe_icon,
+ }
+ })
+ return acc.concat(widgets)
+ },
+ Promise.resolve([] as ExternalWidgetSetupInfo[])
+ )
+
+ return extraWidgets
+}
+
export const setBagOfHoldingOnVehicle = async (
vehicleAddress: string,
bagName: string,
diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts
index 551de6da2..8afc40017 100644
--- a/src/stores/widgetManager.ts
+++ b/src/stores/widgetManager.ts
@@ -36,6 +36,7 @@ import {
type Widget,
CustomWidgetElement,
CustomWidgetElementContainer,
+ InternalWidgetSetupInfo,
MiniWidgetManagerVars,
validateProfile,
validateView,
@@ -555,22 +556,22 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
/**
* Add widget with given type to given view
- * @param { WidgetType } widgetType - Type of the widget
+ * @param { WidgetType } widget - Type of the widget
* @param { View } view - View
*/
- function addWidget(widgetType: WidgetType, view: View): void {
+ function addWidget(widget: InternalWidgetSetupInfo, view: View): void {
const widgetHash = uuid4()
- const widget = {
+ const newWidget = {
hash: widgetHash,
- name: widgetType,
- component: widgetType,
+ name: widget.name,
+ component: widget.component,
position: { x: 0.4, y: 0.32 },
size: { width: 0.2, height: 0.36 },
- options: {},
+ options: widget.options,
}
- if (widgetType === WidgetType.CustomWidgetBase) {
+ if (widget.component === WidgetType.CustomWidgetBase) {
widget.options = {
elementContainers: defaultCustomWidgetContainers,
columns: 1,
@@ -581,8 +582,8 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}
}
- view.widgets.unshift(widget)
- Object.assign(widgetManagerVars(widget.hash), {
+ view.widgets.unshift(newWidget)
+ Object.assign(widgetManagerVars(newWidget.hash), {
...defaultWidgetManagerVars,
...{ allowMoving: true },
})
diff --git a/src/types/widgets.ts b/src/types/widgets.ts
index 820cfaeb8..967051b8d 100644
--- a/src/types/widgets.ts
+++ b/src/types/widgets.ts
@@ -2,6 +2,47 @@ import { CockpitAction } from '@/libs/joystick/protocols/cockpit-actions'
import type { Point2D, SizeRect2D } from './general'
+/**
+ * Widget configuration object as received from BlueOS or another external source
+ */
+export interface ExternalWidgetSetupInfo {
+ /**
+ * Name of the widget, this is displayed on edit mode widget browser
+ */
+ name: string
+ /**
+ * The URL at which the widget is located
+ * This is expected to be an absolute url
+ */
+ iframe_url: string
+
+ /**
+ * The icon of the widget, this is displayed on the widget browser
+ */
+ iframe_icon: string
+}
+
+/**
+ * Internal data used for setting up a new widget. This includes WidgetType, a custom name, options, and icon
+ */ export interface InternalWidgetSetupInfo {
+ /**
+ * Widget type
+ */
+ component: WidgetType
+ /**
+ * Widget name, this will be displayed on edit mode
+ */
+ name: string
+ /**
+ * Widget options, this is the configuration that will be passed to the widget when it is created
+ */
+ options: Record
+ /**
+ * Widget icon, this is the icon that will be displayed on the widget browser
+ */
+ icon: string
+}
+
/**
* Available components to be used in the Widget system
* The enum value is equal to the component's filename, without the '.vue' extension