-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cockpit-actions: Allow actions to be triggered by changes in data-lak…
…e variables - Added a new button in the configuration view to link actions to data-lake variables - User can specify the minimum interval between two automatic calls to the action
- Loading branch information
1 parent
0feda53
commit c85f739
Showing
4 changed files
with
392 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
<template> | ||
<v-dialog v-model="dialog.show" max-width="500px"> | ||
<v-card class="rounded-lg" :style="interfaceStore.globalGlassMenuStyles"> | ||
<v-card-title class="text-h6 font-weight-bold py-4 text-center">Link Action to Variables</v-card-title> | ||
<v-card-text class="px-8"> | ||
<div v-if="dialog.action" class="mb-4"> | ||
<p class="text-subtitle-1 font-weight-bold">Action: {{ dialog.action.name }}</p> | ||
<p class="text-caption">Type: {{ humanizeString(dialog.action.type) }}</p> | ||
</div> | ||
|
||
<v-text-field | ||
v-model="searchQuery" | ||
label="Search variables" | ||
variant="outlined" | ||
density="compact" | ||
prepend-inner-icon="mdi-magnify" | ||
class="mb-2" | ||
clearable | ||
@update:model-value="menuOpen = true" | ||
@click:clear="menuOpen = false" | ||
@update:focused="(isFocused: boolean) => (menuOpen = isFocused)" | ||
/> | ||
|
||
<v-select | ||
v-model="dialog.selectedVariables" | ||
:items="filteredDataLakeVariables" | ||
label="Data Lake Variables" | ||
multiple | ||
chips | ||
variant="outlined" | ||
density="compact" | ||
theme="dark" | ||
closable-chips | ||
:menu-props="{ modelValue: menuOpen }" | ||
@update:menu="menuOpen = $event" | ||
/> | ||
|
||
<v-text-field | ||
v-model="dialog.minInterval" | ||
label="Minimum interval between calls (ms)" | ||
type="number" | ||
min="0" | ||
variant="outlined" | ||
density="compact" | ||
class="mt-4" | ||
/> | ||
|
||
<div class="mt-4"> | ||
<p class="text-caption"> | ||
The action will be called whenever any of the selected variables change, respecting the minimum interval | ||
between consecutive calls. | ||
</p> | ||
</div> | ||
</v-card-text> | ||
<v-divider class="mt-2 mx-10" /> | ||
<v-card-actions> | ||
<div class="flex justify-between items-center pa-2 w-full h-full"> | ||
<v-btn variant="text" @click="closeDialog">Cancel</v-btn> | ||
<v-btn :disabled="!isFormValid" @click="saveConfig">Save</v-btn> | ||
</div> | ||
</v-card-actions> | ||
</v-card> | ||
</v-dialog> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { computed, ref } from 'vue' | ||
import { getActionLink, removeActionLink, saveActionLink } from '@/libs/actions/action-links' | ||
import { getAllDataLakeVariablesInfo } from '@/libs/actions/data-lake' | ||
import { humanizeString } from '@/libs/utils' | ||
import { useAppInterfaceStore } from '@/stores/appInterface' | ||
import { ActionConfig } from '@/types/cockpit-actions' | ||
const interfaceStore = useAppInterfaceStore() | ||
const searchQuery = ref('') | ||
const menuOpen = ref(false) | ||
const defaultDialogConfig = { | ||
show: false, | ||
action: null as ActionConfig | null, | ||
selectedVariables: [] as string[], | ||
minInterval: 1000, | ||
} | ||
const dialog = ref(defaultDialogConfig) | ||
const availableDataLakeVariables = computed(() => { | ||
const variables = getAllDataLakeVariablesInfo() | ||
return Object.values(variables).map((variable) => ({ | ||
title: variable.id, | ||
value: variable.id, | ||
})) | ||
}) | ||
const filteredDataLakeVariables = computed(() => { | ||
const variables = availableDataLakeVariables.value | ||
if (!searchQuery.value) return variables | ||
const query = searchQuery.value.toLowerCase() | ||
return variables.filter((variable) => variable.title.toLowerCase().includes(query)) | ||
}) | ||
const isFormValid = computed(() => { | ||
return dialog.value.action && dialog.value.minInterval >= 0 | ||
}) | ||
const openDialog = (item: ActionConfig): void => { | ||
const existingLink = getActionLink(item.id) | ||
dialog.value = { | ||
show: true, | ||
action: item, | ||
selectedVariables: existingLink?.variables || [], | ||
minInterval: existingLink?.minInterval || 1000, | ||
} | ||
} | ||
const closeDialog = (): void => { | ||
dialog.value = defaultDialogConfig | ||
} | ||
const saveConfig = (): void => { | ||
if (!dialog.value.action) return | ||
// Always remove the existing link first | ||
removeActionLink(dialog.value.action.id) | ||
// Only create a new link if variables are selected | ||
if (dialog.value.selectedVariables.length > 0) { | ||
saveActionLink( | ||
dialog.value.action.id, | ||
dialog.value.action.type, | ||
dialog.value.selectedVariables, | ||
dialog.value.minInterval | ||
) | ||
} | ||
closeDialog() | ||
} | ||
defineExpose({ | ||
openDialog, | ||
}) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { listenDataLakeVariable, unlistenDataLakeVariable } from '@/libs/actions/data-lake' | ||
import { executeActionCallback } from '@/libs/joystick/protocols/cockpit-actions' | ||
|
||
/** | ||
* Interface representing a link between an action and data-lake variables | ||
*/ | ||
interface ActionLink { | ||
/** The ID of the action */ | ||
actionId: string | ||
/** The type of the action */ | ||
actionType: string | ||
/** Array of data-lake variable IDs to watch */ | ||
variables: string[] | ||
/** Minimum time (in ms) between consecutive action executions */ | ||
minInterval: number | ||
/** Timestamp of the last execution */ | ||
lastExecutionTime: number | ||
} | ||
|
||
const actionLinks: Record<string, ActionLink> = {} | ||
const listenerIds: Record<string, string[]> = {} | ||
|
||
/** | ||
* Save a new action link configuration and set up the watchers | ||
* @param {string} actionId The ID of the action to link | ||
* @param {string} actionType The type of the action | ||
* @param {string[]} variables Array of data-lake variable IDs to watch | ||
* @param {number} minInterval Minimum time (in ms) between consecutive action executions | ||
*/ | ||
export const saveActionLink = ( | ||
actionId: string, | ||
actionType: string, | ||
variables: string[], | ||
minInterval: number | ||
): void => { | ||
// Remove any existing link for this action | ||
removeActionLink(actionId) | ||
|
||
// Save the new link configuration | ||
actionLinks[actionId] = { | ||
actionId, | ||
actionType, | ||
variables, | ||
minInterval, | ||
lastExecutionTime: 0, | ||
} | ||
|
||
// Set up listeners for each variable | ||
listenerIds[actionId] = variables.map((variableId) => | ||
listenDataLakeVariable(variableId, () => { | ||
executeLinkedAction(actionId) | ||
}) | ||
) | ||
|
||
saveLinksToPersistentStorage() | ||
} | ||
|
||
/** | ||
* Remove an action link and clean up its watchers | ||
* @param {string} actionId The ID of the action to unlink | ||
*/ | ||
export const removeActionLink = (actionId: string): void => { | ||
const link = actionLinks[actionId] | ||
if (!link) return | ||
|
||
// Remove all listeners | ||
if (listenerIds[actionId]) { | ||
link.variables.forEach((variableId, index) => { | ||
unlistenDataLakeVariable(variableId, listenerIds[actionId][index]) | ||
}) | ||
delete listenerIds[actionId] | ||
} | ||
|
||
delete actionLinks[actionId] | ||
|
||
saveLinksToPersistentStorage() | ||
} | ||
|
||
/** | ||
* Get the link configuration for an action | ||
* @param {string} actionId The ID of the action | ||
* @returns {ActionLink | null} The link configuration if it exists, null otherwise | ||
*/ | ||
export const getActionLink = (actionId: string): ActionLink | null => { | ||
return actionLinks[actionId] || null | ||
} | ||
|
||
/** | ||
* Execute a linked action, respecting the minimum interval between executions | ||
* @param {string} actionId The ID of the action to execute | ||
*/ | ||
const executeLinkedAction = (actionId: string): void => { | ||
const link = actionLinks[actionId] | ||
if (!link) return | ||
|
||
const now = Date.now() | ||
if (now - link.lastExecutionTime >= link.minInterval) { | ||
link.lastExecutionTime = now | ||
executeActionCallback(actionId) | ||
} | ||
} | ||
|
||
/** | ||
* Get all action links | ||
* @returns {Record<string, ActionLink>} Record of all action links | ||
*/ | ||
export const getAllActionLinks = (): Record<string, ActionLink> => { | ||
return { ...actionLinks } | ||
} | ||
|
||
// Load saved links from localStorage on startup | ||
const loadSavedLinks = (): void => { | ||
try { | ||
const savedLinks = localStorage.getItem('cockpit-action-links') | ||
if (savedLinks) { | ||
const links = JSON.parse(savedLinks) as Record<string, Omit<ActionLink, 'lastExecutionTime'>> | ||
Object.entries(links).forEach(([actionId, link]) => { | ||
saveActionLink(actionId, link.actionType, link.variables, link.minInterval) | ||
}) | ||
} | ||
} catch (error) { | ||
console.error('Failed to load saved action links:', error) | ||
} | ||
} | ||
|
||
// Save links to localStorage when they change | ||
const saveLinksToPersistentStorage = (): void => { | ||
try { | ||
// Don't save the lastExecutionTime | ||
const linksToSave = Object.entries(actionLinks).reduce( | ||
(acc, [id, link]) => ({ | ||
...acc, | ||
[id]: { | ||
actionId: link.actionId, | ||
actionType: link.actionType, | ||
variables: link.variables, | ||
minInterval: link.minInterval, | ||
}, | ||
}), | ||
{} | ||
) | ||
localStorage.setItem('cockpit-action-links', JSON.stringify(linksToSave)) | ||
} catch (error) { | ||
console.error('Failed to save action links:', error) | ||
} | ||
} | ||
|
||
// Initialize | ||
loadSavedLinks() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* Custom action types | ||
*/ | ||
export enum customActionTypes { | ||
httpRequest = 'http-request', | ||
mavlinkMessage = 'mavlink-message', | ||
javascript = 'javascript', | ||
} | ||
|
||
/** | ||
* Custom action types names | ||
*/ | ||
export const customActionTypesNames: Record<customActionTypes, string> = { | ||
[customActionTypes.httpRequest]: 'HTTP Request', | ||
[customActionTypes.mavlinkMessage]: 'MAVLink Message', | ||
[customActionTypes.javascript]: 'JavaScript', | ||
} | ||
|
||
/** | ||
* Represents the configuration of a custom action | ||
*/ | ||
export interface ActionConfig { | ||
/** | ||
* Action ID | ||
*/ | ||
id: string | ||
/** | ||
* Action name | ||
*/ | ||
name: string | ||
/** | ||
* Action type | ||
*/ | ||
type: customActionTypes | ||
/** | ||
* Action configuration | ||
* Specific to the action type | ||
*/ | ||
config: any | ||
} |
Oops, something went wrong.