Skip to content

Commit 9bd1943

Browse files
Widgets: add support for Widget discovery from BlueOS
1 parent 595c5d8 commit 9bd1943

File tree

4 files changed

+153
-21
lines changed

4 files changed

+153
-21
lines changed

src/components/EditMenu.vue

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -485,27 +485,22 @@
485485
class="flex items-center justify-between w-full h-full gap-3 overflow-x-auto text-white -mb-1 pr-2 cursor-pointer"
486486
>
487487
<div
488-
v-for="widgetType in availableWidgetTypes"
489-
:key="widgetType"
488+
v-for="widget in allAvailableWidgets"
489+
:key="widget.name"
490490
class="flex flex-col items-center justify-between rounded-md bg-[#273842] hover:brightness-125 h-[90%] aspect-square cursor-pointer elevation-4"
491491
draggable="true"
492492
@dragstart="onRegularWidgetDragStart"
493-
@dragend="onRegularWidgetDragEnd(widgetType)"
493+
@dragend="onRegularWidgetDragEnd(widget)"
494494
>
495495
<v-tooltip text="Drag to add" location="top" theme="light">
496496
<template #activator="{ props: tooltipProps }">
497497
<div />
498-
<img
499-
v-bind="tooltipProps"
500-
:src="widgetImages[widgetType]"
501-
alt="widget-icon"
502-
class="p-4 max-h-[75%] max-w-[95%]"
503-
/>
498+
<img v-bind="tooltipProps" :src="widget.icon" alt="widget-icon" class="p-4 max-h-[75%] max-w-[95%]" />
504499
<div
505500
class="flex items-center justify-center w-full p-1 transition-all bg-[#3B78A8] rounded-b-md text-white"
506501
>
507502
<span class="whitespace-normal text-center">{{
508-
widgetType.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
503+
widget.name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
509504
}}</span>
510505
</div>
511506
</template>
@@ -659,6 +654,7 @@ import URLVideoPlayerImg from '@/assets/widgets/URLVideoPlayer.png'
659654
import VideoPlayerImg from '@/assets/widgets/VideoPlayer.png'
660655
import VirtualHorizonImg from '@/assets/widgets/VirtualHorizon.png'
661656
import { useInteractionDialog } from '@/composables/interactionDialog'
657+
import { getWidgetsFromBlueOS } from '@/libs/blueos'
662658
import { MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum'
663659
import { isHorizontalScroll } from '@/libs/utils'
664660
import { useAppInterfaceStore } from '@/stores/appInterface'
@@ -668,6 +664,8 @@ import {
668664
type View,
669665
type Widget,
670666
CustomWidgetElementType,
667+
ExternalWidgetSetupInfo,
668+
InternalWidgetSetupInfo,
671669
MiniWidgetType,
672670
WidgetType,
673671
} from '@/types/widgets'
@@ -695,6 +693,8 @@ const toggleDial = (): void => {
695693
696694
const forceUpdate = ref(0)
697695
696+
const ExternalWidgetSetupInfos = ref<ExternalWidgetSetupInfo[]>([])
697+
698698
watch(
699699
() => store.currentView.widgets,
700700
() => {
@@ -714,7 +714,31 @@ const emit = defineEmits<{
714714
(e: 'update:editMode', editMode: boolean): void
715715
}>()
716716
717-
const availableWidgetTypes = computed(() => Object.values(WidgetType))
717+
const availableWidgetTypes = computed(() =>
718+
Object.values(WidgetType).map((widgetType) => {
719+
return {
720+
component: widgetType,
721+
name: widgetType,
722+
icon: widgetImages[widgetType] as string,
723+
options: {},
724+
}
725+
})
726+
)
727+
728+
const allAvailableWidgets = computed(() => {
729+
return [
730+
...ExternalWidgetSetupInfos.value.map((widget) => ({
731+
component: WidgetType.IFrame,
732+
icon: widget.iframe_icon,
733+
name: widget.name,
734+
options: {
735+
source: widget.iframe_url,
736+
},
737+
})),
738+
...availableInternalWidgets.value,
739+
]
740+
})
741+
718742
const availableMiniWidgetTypes = computed(() =>
719743
Object.values(MiniWidgetType).map((widgetType) => ({
720744
component: widgetType,
@@ -927,6 +951,10 @@ const miniWidgetsContainerOptions = ref<UseDraggableOptions>({
927951
})
928952
useDraggable(availableMiniWidgetsContainer, availableMiniWidgetTypes, miniWidgetsContainerOptions)
929953
954+
const getExternalWidgetSetupInfos = async (): Promise<void> => {
955+
ExternalWidgetSetupInfos.value = await getWidgetsFromBlueOS()
956+
}
957+
930958
// @ts-ignore: Documentation is not clear on what generic should be passed to 'UseDraggableOptions'
931959
const customWidgetElementContainerOptions = ref<UseDraggableOptions>({
932960
animation: '150',
@@ -940,6 +968,7 @@ useDraggable(
940968
)
941969
942970
onMounted(() => {
971+
getExternalWidgetSetupInfos()
943972
const widgetContainers = [
944973
availableWidgetsContainer.value,
945974
availableMiniWidgetsContainer.value,
@@ -991,7 +1020,7 @@ const onRegularWidgetDragStart = (event: DragEvent): void => {
9911020
}
9921021
}
9931022
994-
const onRegularWidgetDragEnd = (widgetType: WidgetType): void => {
1023+
const onRegularWidgetDragEnd = (widgetType: ExtendedWidget): void => {
9951024
store.addWidget(widgetType, store.currentView)
9961025
9971026
const widgetCards = document.querySelectorAll('[draggable="true"]')

src/libs/blueos.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
import ky, { HTTPError } from 'ky'
22

3+
import { useMainVehicleStore } from '@/stores/mainVehicle'
4+
import { ExternalWidgetSetupInfo } from '@/types/widgets'
5+
6+
/**
7+
* Cockpits extra json format. Taken from extensions in BlueOS and (eventually) other places
8+
*/
9+
interface ExtrasJson {
10+
/**
11+
* The version of the cockpit API that the extra json is compatible with
12+
*/
13+
target_cockpit_api_version: string
14+
/**
15+
* The target system that the extra json is compatible with, in our case, "cockpit"
16+
*/
17+
target_system: string
18+
/**
19+
* A list of widgets that the extra json contains. src/types/widgets.ts
20+
*/
21+
widgets: ExternalWidgetSetupInfo[]
22+
}
23+
324
export const NoPathInBlueOsErrorName = 'NoPathInBlueOS'
425

526
const defaultTimeout = 10000
@@ -30,6 +51,46 @@ export const getKeyDataFromCockpitVehicleStorage = async (
3051
return await getBagOfHoldingFromVehicle(vehicleAddress, `cockpit/${storageKey}`)
3152
}
3253

54+
export const getWidgetsFromBlueOS = async (): Promise<ExternalWidgetSetupInfo[]> => {
55+
const vehicleStore = useMainVehicleStore()
56+
57+
// Wait until we have a global address
58+
while (vehicleStore.globalAddress === undefined) {
59+
await new Promise((r) => setTimeout(r, 1000))
60+
}
61+
const options = { timeout: defaultTimeout, retry: 0 }
62+
const services = (await ky
63+
.get(`http://${vehicleStore.globalAddress}/helper/v1.0/web_services`, options)
64+
.json()) as Record<string, any>
65+
// first we gather all the extra jsons with the cockpit key
66+
const extraWidgets = await services.reduce(
67+
async (accPromise: Promise<ExternalWidgetSetupInfo[]>, service: Record<string, any>) => {
68+
const acc = await accPromise
69+
const worksInRelativePaths = service.metadata?.works_in_relative_paths
70+
if (service.metadata?.extras?.cockpit === undefined) {
71+
return acc
72+
}
73+
const baseUrl = worksInRelativePaths
74+
? `http://${vehicleStore.globalAddress}/extensionv2/${service.metadata.sanitized_name}`
75+
: `http://${vehicleStore.globalAddress}:${service.port}`
76+
const fullUrl = baseUrl + service.metadata?.extras?.cockpit
77+
78+
const extraJson: ExtrasJson = await ky.get(fullUrl, options).json()
79+
const widgets: ExternalWidgetSetupInfo[] = extraJson.widgets.map((widget) => {
80+
return {
81+
...widget,
82+
iframe_url: baseUrl + widget.iframe_url,
83+
iframe_icon: baseUrl + widget.iframe_icon,
84+
}
85+
})
86+
return acc.concat(widgets)
87+
},
88+
Promise.resolve([] as ExternalWidgetSetupInfo[])
89+
)
90+
91+
return extraWidgets
92+
}
93+
3394
export const setBagOfHoldingOnVehicle = async (
3495
vehicleAddress: string,
3596
bagName: string,

src/stores/widgetManager.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type Widget,
3737
CustomWidgetElement,
3838
CustomWidgetElementContainer,
39+
InternalWidgetSetupInfo,
3940
MiniWidgetManagerVars,
4041
validateProfile,
4142
validateView,
@@ -555,22 +556,22 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
555556

556557
/**
557558
* Add widget with given type to given view
558-
* @param { WidgetType } widgetType - Type of the widget
559+
* @param { WidgetType } widget - Type of the widget
559560
* @param { View } view - View
560561
*/
561-
function addWidget(widgetType: WidgetType, view: View): void {
562+
function addWidget(widget: InternalWidgetSetupInfo, view: View): void {
562563
const widgetHash = uuid4()
563564

564-
const widget = {
565+
const newWidget = {
565566
hash: widgetHash,
566-
name: widgetType,
567-
component: widgetType,
567+
name: widget.name,
568+
component: widget.component,
568569
position: { x: 0.4, y: 0.32 },
569570
size: { width: 0.2, height: 0.36 },
570-
options: {},
571+
options: widget.options,
571572
}
572573

573-
if (widgetType === WidgetType.CustomWidgetBase) {
574+
if (widget.component === WidgetType.CustomWidgetBase) {
574575
widget.options = {
575576
elementContainers: defaultCustomWidgetContainers,
576577
columns: 1,
@@ -581,8 +582,8 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
581582
}
582583
}
583584

584-
view.widgets.unshift(widget)
585-
Object.assign(widgetManagerVars(widget.hash), {
585+
view.widgets.unshift(newWidget)
586+
Object.assign(widgetManagerVars(newWidget.hash), {
586587
...defaultWidgetManagerVars,
587588
...{ allowMoving: true },
588589
})

src/types/widgets.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@ import { CockpitAction } from '@/libs/joystick/protocols/cockpit-actions'
22

33
import type { Point2D, SizeRect2D } from './general'
44

5+
/**
6+
* Widget configuration object as received from BlueOS or another external source
7+
*/
8+
export interface ExternalWidgetSetupInfo {
9+
/**
10+
* Name of the widget, this is displayed on edit mode widget browser
11+
*/
12+
name: string
13+
/**
14+
* The URL at which the widget is located
15+
* This is expected to be an absolute url
16+
*/
17+
iframe_url: string
18+
19+
/**
20+
* The icon of the widget, this is displayed on the widget browser
21+
*/
22+
iframe_icon: string
23+
}
24+
25+
/**
26+
* Internal data used for setting up a new widget. This includes WidgetType, a custom name, options, and icon
27+
*/ export interface InternalWidgetSetupInfo {
28+
/**
29+
* Widget type
30+
*/
31+
component: WidgetType
32+
/**
33+
* Widget name, this will be displayed on edit mode
34+
*/
35+
name: string
36+
/**
37+
* Widget options, this is the configuration that will be passed to the widget when it is created
38+
*/
39+
options: Record<string, unknown>
40+
/**
41+
* Widget icon, this is the icon that will be displayed on the widget browser
42+
*/
43+
icon: string
44+
}
45+
546
/**
647
* Available components to be used in the Widget system
748
* The enum value is equal to the component's filename, without the '.vue' extension

0 commit comments

Comments
 (0)