Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for dynamic Cockpit Actions and add support for customizable HTTP request actions #1386

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ import { useAppInterfaceStore } from './stores/appInterface'
import { useMainVehicleStore } from './stores/mainVehicle'
import { useWidgetManagerStore } from './stores/widgetManager'
import { ConfigComponent } from './types/general'
import ConfigurationActionsView from './views/ConfigurationActionsView.vue'
import ConfigurationAlertsView from './views/ConfigurationAlertsView.vue'
import ConfigurationDevelopmentView from './views/ConfigurationDevelopmentView.vue'
import ConfigurationGeneralView from './views/ConfigurationGeneralView.vue'
Expand Down Expand Up @@ -410,6 +411,11 @@ const configMenu = [
title: 'Mission',
component: markRaw(ConfigurationMissionView) as ConfigComponent,
},
{
icon: 'mdi-run-fast',
title: 'Actions',
component: markRaw(ConfigurationActionsView) as ConfigComponent,
},
]

watch(
Expand Down
78 changes: 78 additions & 0 deletions src/libs/actions/data-lake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* A variable to be used on a Cockpit action
* @param { string } id - The id of the variable
* @param { string } name - The name of the variable
* @param { 'string' | 'number' | 'boolean' } type - The type of the variable (string, number or boolean)
* @param { string } description - What the variable does or means
*/
class CockpitActionVariable {
id: string
name: string
type: 'string' | 'number' | 'boolean'
description?: string
// eslint-disable-next-line jsdoc/require-jsdoc
constructor(id: string, name: string, type: 'string' | 'number' | 'boolean', description?: string) {
this.id = id
this.name = name
this.type = type
this.description = description
}
}

const cockpitActionVariableInfo: Record<string, CockpitActionVariable> = {}
export const cockpitActionVariableData: Record<string, string | number | boolean> = {}
const cockpitActionVariableListeners: Record<string, ((value: string | number | boolean) => void)[]> = {}

export const getAllCockpitActionVariablesInfo = (): Record<string, CockpitActionVariable> => {
return cockpitActionVariableInfo
}

export const getCockpitActionVariableInfo = (id: string): CockpitActionVariable | undefined => {
return cockpitActionVariableInfo[id]
}

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.`)
}
cockpitActionVariableInfo[variable.id] = variable
}

export const updateCockpitActionVariableInfo = (variable: CockpitActionVariable): void => {
if (!cockpitActionVariableInfo[variable.id]) {
throw new Error(`Cockpit action variable with id '${variable.id}' does not exist. Create it first.`)
}
cockpitActionVariableInfo[variable.id] = variable
}

export const getCockpitActionVariableData = (id: string): string | number | boolean | undefined => {
return cockpitActionVariableData[id]
}

export const setCockpitActionVariableData = (id: string, data: string | number | boolean): void => {
cockpitActionVariableData[id] = data
notifyCockpitActionVariableListeners(id)
}

export const deleteCockpitActionVariable = (id: string): void => {
delete cockpitActionVariableInfo[id]
delete cockpitActionVariableData[id]
}

export const listenCockpitActionVariable = (id: string, listener: (value: string | number | boolean) => void): void => {
if (!cockpitActionVariableListeners[id]) {
cockpitActionVariableListeners[id] = []
}
cockpitActionVariableListeners[id].push(listener)
}

export const unlistenCockpitActionVariable = (id: string): void => {
delete cockpitActionVariableListeners[id]
}

const notifyCockpitActionVariableListeners = (id: string): void => {
if (cockpitActionVariableListeners[id]) {
const value = cockpitActionVariableData[id]
cockpitActionVariableListeners[id].forEach((listener) => listener(value))
}
}
160 changes: 160 additions & 0 deletions src/libs/actions/http-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
availableCockpitActions,
CockpitAction,
CockpitActionsFunction,
deleteAction,
registerActionCallback,
registerNewAction,
} from '../joystick/protocols/cockpit-actions'
import { getCockpitActionVariableData } from './data-lake'

const httpRequestActionIdPrefix = 'http-request-action'

/**
* The types of HTTP methods that can be used.
*/
export enum HttpRequestMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH',
}
export const availableHttpRequestMethods: HttpRequestMethod[] = Object.values(HttpRequestMethod)

export type HttpRequestActionConfig = {
/**
* The name of the action.
*/
name: string
/**
* The URL to send the request to.
*/
url: string
/**
* The HTTP method to use.
*/
method: HttpRequestMethod
/**
* The headers to send with the request.
*/
headers: Record<string, string>
/**
* The URL parameters to send with the request.
*/
urlParams: Record<string, string>
/**
* The body of the request.
*/
body: string
}

let registeredHttpRequestActionConfigs: Record<string, HttpRequestActionConfig> = {}

export const registerHttpRequestActionConfig = (action: HttpRequestActionConfig): void => {
const id = `${httpRequestActionIdPrefix} (${action.name})`
registeredHttpRequestActionConfigs[id] = action
saveHttpRequestActionConfigs()
updateCockpitActions()
}

export const getHttpRequestActionConfig = (id: string): HttpRequestActionConfig | undefined => {
return registeredHttpRequestActionConfigs[id]
}

export const getAllHttpRequestActionConfigs = (): Record<string, HttpRequestActionConfig> => {
return registeredHttpRequestActionConfigs
}

export const deleteHttpRequestActionConfig = (id: string): void => {
delete registeredHttpRequestActionConfigs[id]
saveHttpRequestActionConfigs()
updateCockpitActions()
}

export const updateHttpRequestActionConfig = (id: string, updatedAction: HttpRequestActionConfig): void => {
registeredHttpRequestActionConfigs[id] = updatedAction
saveHttpRequestActionConfigs()
updateCockpitActions()
}

export const updateCockpitActions = (): void => {
Object.entries(availableCockpitActions).forEach(([id]) => {
if (id.includes(httpRequestActionIdPrefix)) {
deleteAction(id as CockpitActionsFunction)
}
})

const httpResquestActions = getAllHttpRequestActionConfigs()
for (const [id, action] of Object.entries(httpResquestActions)) {
try {
const cockpitAction = new CockpitAction(id as CockpitActionsFunction, action.name)
registerNewAction(cockpitAction)
registerActionCallback(cockpitAction, getHttpRequestActionCallback(id))
} catch (error) {
console.error(`Error registering action ${id}: ${error}`)
}
}
}

export const loadHttpRequestActionConfigs = (): void => {
const savedActions = localStorage.getItem('cockpit-http-request-actions')
if (savedActions) {
registeredHttpRequestActionConfigs = JSON.parse(savedActions)
}
}

export const saveHttpRequestActionConfigs = (): void => {
localStorage.setItem('cockpit-http-request-actions', JSON.stringify(registeredHttpRequestActionConfigs))
}

export type HttpRequestActionCallback = () => void

export const getHttpRequestActionCallback = (id: string): HttpRequestActionCallback => {
const action = getHttpRequestActionConfig(id)
if (!action) {
throw new Error(`Action with id ${id} not found.`)
}

let parsedBody = action.body
const parsedUrlParams = action.urlParams

const cockpitInputsInBody = action.body.match(/{{\s*([^{}\s]+)\s*}}/g)
if (cockpitInputsInBody) {
for (const input of cockpitInputsInBody) {
const parsedInput = input.replace('{{', '').replace('}}', '').trim()
const inputData = getCockpitActionVariableData(parsedInput)
if (inputData) {
parsedBody = parsedBody.replace(input, inputData.toString())
}
}
}

const cockpitInputsInUrlParams = Object.entries(action.urlParams).filter(
([, value]) => typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')
)
if (cockpitInputsInUrlParams) {
for (const [key, value] of cockpitInputsInUrlParams) {
const parsedInput = value.replace('{{', '').replace('}}', '').trim()
const inputData = getCockpitActionVariableData(parsedInput)
if (inputData) {
parsedUrlParams[key] = inputData.toString()
}
}
}

const url = new URL(action.url)

url.search = new URLSearchParams(parsedUrlParams).toString()

return () => {
fetch(url, {
method: action.method,
headers: action.headers,
body: action.method === HttpRequestMethod.GET ? undefined : parsedBody,
})
}
}

loadHttpRequestActionConfigs()
updateCockpitActions()
21 changes: 11 additions & 10 deletions src/libs/joystick/protocols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
} from './protocols/mavlink-manual-control'
import { modifierKeyActions, otherAvailableActions } from './protocols/other'

export const allAvailableAxes: ProtocolAction[] = [
...Object.values(mavlinkManualControlAxes),
...Object.values(otherAvailableActions),
]
export const allAvailableAxes = (): ProtocolAction[] => {
return [...Object.values(mavlinkManualControlAxes), ...Object.values(otherAvailableActions)]
}

export const allAvailableButtons: ProtocolAction[] = [
...Object.values(availableCockpitActions),
...Object.values(availableMavlinkManualControlButtonFunctions),
...Object.values(otherAvailableActions),
...Object.values(modifierKeyActions),
]
export const allAvailableButtons = (): ProtocolAction[] => {
return [
...Object.values(availableCockpitActions),
...Object.values(availableMavlinkManualControlButtonFunctions),
...Object.values(otherAvailableActions),
...Object.values(modifierKeyActions),
]
}
Loading
Loading