Widget type:
@@ -463,21 +463,17 @@
To be placed on the main view area
-
- (Click on card to add)
-
+
(Drag card to add)
To be placed on the top and bottom bars
-
- (Drag card in place to add)
-
+
(Drag card to add)
Create Custom Widget
+ >Add widget base
@@ -630,7 +626,7 @@
>
@@ -734,6 +730,7 @@ const availableCustomWidgetElementsTypes = computed(() =>
name: widgetType,
options: {},
hash: uuid(),
+ isBoolean: false,
managerVars: defaultCustomWidgetManagerVars,
cockpitActions: [],
}))
diff --git a/src/components/ElementConfigPanel.vue b/src/components/ElementConfigPanel.vue
index cae27548a..f68489e83 100644
--- a/src/components/ElementConfigPanel.vue
+++ b/src/components/ElementConfigPanel.vue
@@ -17,20 +17,29 @@
>
Delete
-
+
Options
{{ formatLabel(optionKey) }}
-
-
-
-
-
-
-
-
-
{{ index + 1 }}
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
+
+
-
+
-
-
-
-
- {{ currentElement?.hash }}
+
+ Cockpit actions
+
+
+
+
+
Action URL parameter
+
create
+
+
+
+
+
+
+ delete
+ {{ currentElement.options.actionParameter === undefined ? 'save' : 'update' }}
+
+
+
+
+
Action to trigger
+
+
+
+
+
+
+
+
+
diff --git a/src/components/WidgetHugger.vue b/src/components/WidgetHugger.vue
index 6f427f046..6b0186059 100644
--- a/src/components/WidgetHugger.vue
+++ b/src/components/WidgetHugger.vue
@@ -92,7 +92,7 @@ const hoveringWidgetOrOverlay = computed(() => hoveringOverlay.value || hovering
// Put the widget into highlighted state when in edit-mode and hovering over it
watch([hoveringWidgetOrOverlay, allowMoving], () => {
- widgetStore.widgetManagerVars(widget.value.hash).highlighted = hoveringWidgetOrOverlay.value && allowMoving.value
+ widgetStore.widgetManagerVars(widget.value.hash).highlighted = hoveringWidgetOrOverlay.value
})
const draggingWidget = ref(false)
diff --git a/src/components/custom-widget-elements/Button.vue b/src/components/custom-widget-elements/Button.vue
index 458a57d16..ff0b090f6 100644
--- a/src/components/custom-widget-elements/Button.vue
+++ b/src/components/custom-widget-elements/Button.vue
@@ -1,7 +1,7 @@
- {{ element.options.label || 'Button' }}
+ {{ element.options.layout?.label || 'Button' }}
diff --git a/src/components/custom-widget-elements/Dial.vue b/src/components/custom-widget-elements/Dial.vue
index 27a8cbcc0..83ddfa434 100644
--- a/src/components/custom-widget-elements/Dial.vue
+++ b/src/components/custom-widget-elements/Dial.vue
@@ -2,7 +2,7 @@
{{ Math.round(potentiometerValue) }}
@@ -28,8 +28,14 @@
diff --git a/src/components/custom-widget-elements/Label.vue b/src/components/custom-widget-elements/Label.vue
index 5431accf0..f941b21bf 100644
--- a/src/components/custom-widget-elements/Label.vue
+++ b/src/components/custom-widget-elements/Label.vue
@@ -10,11 +10,11 @@
@@ -24,8 +24,13 @@
diff --git a/src/components/custom-widget-elements/Slider.vue b/src/components/custom-widget-elements/Slider.vue
index f6bdef828..779d4a16d 100644
--- a/src/components/custom-widget-elements/Slider.vue
+++ b/src/components/custom-widget-elements/Slider.vue
@@ -7,32 +7,45 @@
: 'border-0'
"
>
-
-
- {{ element.options.label }}
+
+
+ {{ element.options.layout?.label }}
{
+ if (element.options.actionParameter) {
+ setCockpitActionVariableData(element.options.actionParameter.name, sliderValue.toFixed(1))
+ }
+ }
+ "
>
diff --git a/src/components/custom-widget-elements/Switch.vue b/src/components/custom-widget-elements/Switch.vue
index f37b01725..c63d053da 100644
--- a/src/components/custom-widget-elements/Switch.vue
+++ b/src/components/custom-widget-elements/Switch.vue
@@ -10,17 +10,22 @@
-
{{ element.options.label }}
+
{{ element.options.layout?.label }}
diff --git a/src/components/widgets/CustomWidgetBase.vue b/src/components/widgets/CustomWidgetBase.vue
index c09b0cf5d..d4761609e 100644
--- a/src/components/widgets/CustomWidgetBase.vue
+++ b/src/components/widgets/CustomWidgetBase.vue
@@ -2,7 +2,11 @@
-
-
+
+
@@ -125,8 +129,8 @@
()
@@ -210,6 +214,29 @@ const leftColumnWidth = ref(props.widget.options.leftColumnWidth || 50)
const isDragging = ref(false)
const widgetBase = ref(null)
const isWrapped = ref(false)
+const wrapDirection = ref<'left' | 'right'>('right')
+
+const updateWrapDirection = (): void => {
+ if (widgetBase.value) {
+ const widgetRect = widgetBase.value.getBoundingClientRect()
+ const screenWidth = window.innerWidth
+ const widgetCenterX = widgetRect.left + widgetRect.width / 2
+
+ if (widgetCenterX < screenWidth / 2) {
+ wrapDirection.value = 'left'
+ } else {
+ wrapDirection.value = 'right'
+ }
+ }
+}
+
+const wrapChevronIcon = computed(() => {
+ if (wrapDirection.value === 'left') {
+ return isWrapped.value ? 'mdi-chevron-right' : 'mdi-chevron-left'
+ } else {
+ return isWrapped.value ? 'mdi-chevron-left' : 'mdi-chevron-right'
+ }
+})
const enableEditing = (): void => {
editedName.value = props.widget.name
@@ -314,6 +341,7 @@ const enableMovingOnDrag = (): void => {
}
const disableMovingOnDrag = (): void => {
+ updateWrapDirection()
widgetStore.allowMovingAndResizing(currentWidget.value.hash, false)
window.removeEventListener('mouseup', disableMovingOnDrag)
window.removeEventListener('dragend', disableMovingOnDrag)
@@ -339,7 +367,7 @@ const widgetAdded = (e: SortableEvent.SortableEvent, containerName: string): voi
}
}
-const { width: windowWidth, height: windowHeight } = useWindowSize()
+const { width: windowWidth } = useWindowSize()
const canvasSize = computed(() => ({
width: currentWidget.value.size.width * windowWidth.value,
height: currentWidget.value.size.height * windowWidth.value,
@@ -369,7 +397,6 @@ const loadWidgetFromStore = (): void => {
}
onBeforeMount(() => {
- console.log('🚀 ~ props.widget:', props.widget)
props.widget.options.elementContainers.forEach((container: CustomWidgetElementContainer) => {
lastKnownHashes.value.set(
container.name,
@@ -379,6 +406,8 @@ onBeforeMount(() => {
})
onMounted(() => {
+ updateWrapDirection()
+ window.addEventListener('resize', updateWrapDirection)
containerRef.value = document.querySelector('.main')
loadWidgetFromStore()
disableMovingOnDrag()
diff --git a/src/libs/actions/data-lake.ts b/src/libs/actions/data-lake.ts
index 0cc437574..acc570ec4 100644
--- a/src/libs/actions/data-lake.ts
+++ b/src/libs/actions/data-lake.ts
@@ -1,3 +1,5 @@
+import { useSnackbar } from '@/composables/snackbar'
+
/**
* A variable to be used on a Cockpit action
* @param { string } id - The id of the variable
@@ -19,6 +21,8 @@ class CockpitActionVariable {
}
}
+const { showSnackbar } = useSnackbar()
+
const cockpitActionVariableInfo: Record = {}
export const cockpitActionVariableData: Record = {}
const cockpitActionVariableListeners: Record void)[]> = {}
@@ -33,7 +37,13 @@ export const getCockpitActionVariableInfo = (id: string): CockpitActionVariable
export const createCockpitActionVariable = (variable: CockpitActionVariable): void => {
if (cockpitActionVariableInfo[variable.id]) {
- throw new Error(`Cockpit action variable with id '${variable.id}' already exists. Update it instead.`)
+ console.log(`Cockpit action variable with id '${variable.id}' already exists. Renaming to ${variable.name}_new.`)
+ showSnackbar({
+ message: `Cockpit action variable with id '${variable.id}' already exists. Renaming to ${variable.name}_new.`,
+ })
+ variable.id = `${variable.id}_new`
+ variable.name = `${variable.name}_new`
+ cockpitActionVariableInfo[variable.id] = variable
}
cockpitActionVariableInfo[variable.id] = variable
}
@@ -43,6 +53,9 @@ export const updateCockpitActionVariableInfo = (variable: CockpitActionVariable)
throw new Error(`Cockpit action variable with id '${variable.id}' does not exist. Create it first.`)
}
cockpitActionVariableInfo[variable.id] = variable
+ if ((cockpitActionVariableInfo[variable.id] = variable)) {
+ showSnackbar({ message: 'Parameter successfully updated', variant: 'success' })
+ }
}
export const getCockpitActionVariableData = (id: string): string | number | boolean | undefined => {
@@ -57,6 +70,11 @@ export const setCockpitActionVariableData = (id: string, data: string | number |
export const deleteCockpitActionVariable = (id: string): void => {
delete cockpitActionVariableInfo[id]
delete cockpitActionVariableData[id]
+ if (!cockpitActionVariableInfo[id]) {
+ showSnackbar({ message: 'Parameter successfully deleted', variant: 'success' })
+ return
+ }
+ showSnackbar({ message: 'Parameter could not be deleted', variant: 'error' })
}
export const listenCockpitActionVariable = (id: string, listener: (value: string | number | boolean) => void): void => {
diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts
index 1553f06e7..4ee9a491f 100644
--- a/src/stores/widgetManager.ts
+++ b/src/stores/widgetManager.ts
@@ -88,7 +88,6 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}
const showElementPropsDrawer = (customWidgetElementHash: string): void => {
- console.log('🚀 ~ customWidgetElementHash:', customWidgetElementHash)
const customWidgetElement = getElementByHash(customWidgetElementHash)
if (!customWidgetElement) {
showSnackbar({ variant: 'error', message: 'Could not find element with the given hash.', duration: 3000 })
@@ -156,19 +155,47 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}
const currentPosition = currentViewWidgets[widgetIndex].position
- const newWidgetHash = uuid4()
- loadedWidget.hash = newWidgetHash
- loadedWidget.position = currentPosition
+ reassignHashesToWidget(loadedWidget)
+
+ loadedWidget.position = currentPosition
currentViewWidgets[widgetIndex] = loadedWidget
+
showSnackbar({ variant: 'success', message: 'Widget loaded successfully with new hash.', duration: 3000 })
}
+ const reassignHashesToWidget = (widget: Widget): void => {
+ const oldToNewHashMap = new Map()
+
+ const oldWidgetHash = widget.hash
+ widget.hash = uuid4()
+ oldToNewHashMap.set(oldWidgetHash, widget.hash)
+
+ for (const container of widget.options.elementContainers) {
+ for (const element of container.elements) {
+ reassignHashesToElement(element, oldToNewHashMap)
+ }
+ }
+ }
+
+ const reassignHashesToElement = (element: CustomWidgetElement, hashMap: Map): void => {
+ const oldHash = element.hash
+ element.hash = uuid4()
+ hashMap.set(oldHash, element.hash)
+
+ if (element.options && element.options.actionParameter) {
+ const actionParameter = element.options.actionParameter
+ actionParameter.id = `${actionParameter.id}_new`
+ actionParameter.name = `${actionParameter.name}_new`
+ }
+ }
+
/**
* Updates the options of a custom widget element by its hash.
* @param {string} elementHash - The unique identifier of the element.
* @param {Record} newOptions - The new options to merge with the existing ones.
*/
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateElementOptions = (elementHash: string, newOptions: Record): void => {
const element = getElementByHash(elementHash)
if (element) {
@@ -176,8 +203,6 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
...element.options,
...newOptions,
}
- } else {
- showSnackbar({ variant: 'error', message: 'Element not found.', duration: 3000 })
}
}
diff --git a/src/types/widgets.ts b/src/types/widgets.ts
index d842bf53c..bc9ecb638 100644
--- a/src/types/widgets.ts
+++ b/src/types/widgets.ts
@@ -59,7 +59,7 @@ export enum CustomWidgetElementType {
}
/**
- *
+ * Available variables to be used in the Custom Widget creator.
*/
export enum CustomWidgetVarsType {
Button = 'boolean',
@@ -72,7 +72,7 @@ export enum CustomWidgetVarsType {
}
/**
- *
+ * Available containers to be used in the Custom Widget creator.
*/
export enum CustomWidgetElementContainers {
Left0 = '0-left',
@@ -97,12 +97,34 @@ export enum CustomWidgetElementContainers {
Right9 = '9-right',
}
+/**
+ * Options for the Cockpit Actions parameters
+ */
+export interface CockpitActionParameter {
+ /**
+ * Parameter ID, equals to initial name of the parameter
+ */
+ id: string
+ /**
+ * Parameter name
+ */
+ name: string
+ /**
+ * Parameter type
+ */
+ type: 'string' | 'boolean' | 'number'
+ /**
+ * Parameter description
+ */
+ description?: string
+}
+
/**
* Options for the Custom Widgets inner elements
*/
export type CustomWidgetElementOptions = {
/**
- *
+ * Custom widget element - Label
*/
[CustomWidgetElementType.Label]: {
/**
@@ -110,45 +132,54 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
+ * Variable type
*/
- cockpitAction: CockpitAction
+ variableType: string
/**
- * The label text
- */
- text: string
- /**
- * The size of the label's font (in pixels)
+ * Action parameter
*/
- textSize: number
+ actionParameter: CockpitActionParameter
/**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- * The weight of the label's font
- */
- weight: 'normal' | 'bold' | 'bolder' | 'lighter'
- /**
- * The decoration for the label's text
+ * The label text
*/
- decoration: 'none' | 'underline' | 'line-through' | 'overline'
+ text: string
/**
- * The color of the label's text
- */
- color: string
+ * Layout options
+ */
+ layout: {
+ /**
+ * The size of the label's font (in pixels)
+ */
+ textSize: number
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The weight of the label's font
+ */
+ weight: 'normal' | 'bold' | 'bolder' | 'lighter'
+ /**
+ * The decoration for the label's text
+ */
+ decoration: 'none' | 'underline' | 'line-through' | 'overline'
+ /**
+ * The color of the label's text
+ */
+ color: string
+ }
}
}
/**
- *
+ * Custom widget element - Button
*/
[CustomWidgetElementType.Button]: {
/**
@@ -156,45 +187,54 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
- */
- cockpitAction: CockpitAction
- /**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- *
- */
- label: string
- /**
- *
- */
- buttonSize: 'small' | 'default' | 'large'
- /**
- *
+ * Variable type
*/
- backgroundColor: string
+ variableType: string
/**
- *
+ * Action parameter
*/
- textColor: string
+ cockpitAction: CockpitAction
/**
- *
- */
- variant: 'text' | 'outlined' | 'flat' | 'elevated' | 'tonal' | 'plain'
+ * Layout options
+ */
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The label of the button
+ */
+ label: string
+ /**
+ * The size of the button
+ */
+ buttonSize: 'small' | 'default' | 'large'
+ /**
+ * The color of the button
+ */
+ backgroundColor: string
+ /**
+ * The color of the button's text
+ */
+ textColor: string
+ /**
+ * The variant of the button
+ */
+ variant: 'text' | 'outlined' | 'flat' | 'elevated' | 'tonal' | 'plain'
+ }
}
}
/**
- *
+ * Custom widget element - Checkbox
*/
[CustomWidgetElementType.Checkbox]: {
/**
@@ -202,37 +242,46 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element value
+ */
+ checked: boolean
+ /**
+ * Element options
*/
options: {
/**
- *
+ * Variable type
*/
- cockpitAction: CockpitAction
+ variableType: string
/**
- * Alignment of the element
+ * Action parameter
*/
- align: 'start' | 'center' | 'end'
+ actionParameter: CockpitActionParameter
/**
- *
+ * Layout props for the element
*/
- checked: boolean
- /**
- *
- */
- color: string
- /**
- *
- */
- label: string
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The size of the checkbox
+ */
+ color: string
+ /**
+ * The label of the checkbox
+ */
+ label: string
+ }
}
}
/**
- *
+ * Custom widget element - Dial
*/
[CustomWidgetElementType.Dial]: {
/**
@@ -240,45 +289,54 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
- */
- cockpitAction: CockpitAction
- /**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- *
- */
- size: 'small' | 'medium' | 'large'
- /**
- *
- */
- color: string
- /**
- *
- */
- minValue: number
- /**
- *
- */
- maxValue: number
- /**
- *
- */
- showValue: boolean
+ * Variable type
+ */
+ variableType: string
+ /**
+ * Action parameter
+ */
+ actionParameter: CockpitActionParameter
+ /**
+ * Layout options
+ */
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The size of the dial
+ */
+ size: 'small' | 'medium' | 'large'
+ /**
+ * The color of the dial
+ */
+ color: string
+ /**
+ * The minimum value of the dial
+ */
+ minValue: number
+ /**
+ * The maximum value of the dial
+ */
+ maxValue: number
+ /**
+ * The step value of the dial
+ */
+ showValue: boolean
+ }
}
}
/**
- *
+ * Custom widget element - Dropdown
*/
[CustomWidgetElementType.Dropdown]: {
/**
@@ -286,29 +344,53 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
- */
- cockpitAction: CockpitAction
- /**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- *
- */
- options: string[]
+ * Variable type
+ */
+ variableType: string
+ /**
+ * Action parameter
+ */
+ actionParameter: CockpitActionParameter
+ /**
+ * Layout options
+ */
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ selectorOptions: [
+ {
+ /**
+ * The name of the option
+ */
+ name: string
+ /**
+ * The value of the option
+ */
+ value: string
+ }
+ ]
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The size of the dropdown
+ */
+ width: number
+ }
}
}
/**
- *
+ * Custom widget element - Slider
*/
[CustomWidgetElementType.Slider]: {
/**
@@ -316,53 +398,62 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
- */
- cockpitAction: CockpitAction
- /**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- *
- */
- size: 'small' | 'medium' | 'large'
- /**
- *
- */
- color: string
- /**
- *
- */
- minValue: number
- /**
- *
- */
- maxValue: number
- /**
- *
- */
- showValue: boolean
- /**
- *
- */
- label: string
- /**
- *
- */
- labelWidth: number
+ * Variable type
+ */
+ variableType: string
+ /**
+ * Action parameter
+ */
+ actionParameter: CockpitActionParameter
+ /**
+ * Layout options
+ */
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The size of the slider
+ */
+ size: 'small' | 'medium' | 'large'
+ /**
+ * The color of the slider
+ */
+ color: string
+ /**
+ * The minimum value of the slider
+ */
+ minValue: number
+ /**
+ * The maximum value of the slider
+ */
+ maxValue: number
+ /**
+ * The step value of the slider
+ */
+ showTooltip: boolean
+ /**
+ * The label of the slider
+ */
+ label: string
+ /**
+ * The width of the label
+ */
+ labelWidth: number
+ }
}
}
/**
- *
+ * Custom widget element - Switch
*/
[CustomWidgetElementType.Switch]: {
/**
@@ -370,33 +461,42 @@ export type CustomWidgetElementOptions = {
*/
hash: string
/**
- *
+ * Element name
*/
name: string
/**
- *
+ * Element options
*/
options: {
/**
- *
- */
- cockpitAction: CockpitAction
- /**
- * Alignment of the element
- */
- align: 'start' | 'center' | 'end'
- /**
- *
- */
- size: 'small' | 'medium' | 'large'
- /**
- *
- */
- color: string
- /**
- *
- */
- label: string
+ * Variable type
+ */
+ variableType: string
+ /**
+ * Action parameter
+ */
+ actionParameter: CockpitActionParameter
+ /**
+ * Layout options
+ */
+ layout: {
+ /**
+ * Alignment of the element
+ */
+ align: 'start' | 'center' | 'end'
+ /**
+ * The size of the switch
+ */
+ size: 'small' | 'medium' | 'large'
+ /**
+ * The color of the switch
+ */
+ color: string
+ /**
+ * The label of the switch
+ */
+ label: string
+ }
}
}
}
@@ -535,42 +635,42 @@ export type MiniWidget = {
export type CustomWidget = {
/**
- *
+ * Unique identifier for the widget
*/
hash: string
/**
- *
+ * Component type of the widget
*/
component: WidgetType
/**
- *
+ * 2D position of the widget (top-left corner)
*/
position: Point2D
/**
- *
+ * Size of the widget box
*/
size: SizeRect2D
/**
- *
+ * Editable name for the widget
*/
name: string
/**
- *
+ * Internal options of the widget
*/
elementContainers: Array<{
/**
- *
+ * Editable name for the container
*/
name: CustomWidgetElementContainers
/**
- *
+ * Array of elements that are stored in the container
*/
elements: CustomWidgetElement[]
}>
/**
- *
+ * Internal options of the widget
*/
- options: Record
+ options: Record // eslint-disable-line @typescript-eslint/no-explicit-any
}
export type CustomWidgetElement = {
@@ -586,6 +686,10 @@ export type CustomWidgetElement = {
* Component type of the element
*/
component: CustomWidgetElementType
+ /**
+ * If the element is a boolean input or not
+ */
+ isBoolean: boolean
/**
* Internal options of the widget
*/
diff --git a/src/views/ConfigurationActionsView.vue b/src/views/ConfigurationActionsView.vue
index 171fea4f7..28407384a 100644
--- a/src/views/ConfigurationActionsView.vue
+++ b/src/views/ConfigurationActionsView.vue
@@ -312,7 +312,7 @@