+ Create complex transformations by combining existing Data Lake variables using JavaScript expressions.
+ Type '\{\{' to access available variables. Return is optional, but should be included in complex
+ expressions. Remember to set the type accordingly.
+
+
+
+
+
+
+
+
+ Cancel
+ Save
+
+
+
+
+
+
+
+
+
diff --git a/src/libs/actions/data-lake-transformations.ts b/src/libs/actions/data-lake-transformations.ts
new file mode 100644
index 000000000..edc8e3727
--- /dev/null
+++ b/src/libs/actions/data-lake-transformations.ts
@@ -0,0 +1,195 @@
+import {
+ findDataLakeVariablesIdsInString,
+ getDataLakeVariableIdFromInput,
+ replaceDataLakeInputsInString,
+} from '../utils-data-lake'
+import {
+ createDataLakeVariable,
+ DataLakeVariable,
+ deleteDataLakeVariable,
+ listenDataLakeVariable,
+ setDataLakeVariableData,
+ unlistenDataLakeVariable,
+} from './data-lake'
+
+const transformingFunctionsKey = 'cockpit-transforming-functions'
+
+let globalTransformingFunctions: TransformingFunction[] = []
+
+const loadTransformingFunctions = (): void => {
+ const transformingFunctions = localStorage.getItem(transformingFunctionsKey)
+ if (!transformingFunctions) {
+ globalTransformingFunctions = []
+ return
+ }
+ globalTransformingFunctions = JSON.parse(transformingFunctions)
+ updateTransformingFunctionListeners()
+}
+
+const saveTransformingFunctions = (): void => {
+ localStorage.setItem(transformingFunctionsKey, JSON.stringify(globalTransformingFunctions))
+ updateTransformingFunctionListeners()
+}
+
+const getExpressionValue = (func: TransformingFunction): string | number | boolean => {
+ const expressionWithValues = replaceDataLakeInputsInString(func.expression)
+ if (func.expression.includes('return')) {
+ return eval(`(function() { ${expressionWithValues} })()`)
+ }
+ return eval(`(function() { return ${expressionWithValues} })()`)
+}
+
+const variablesListeners: Record> = {}
+
+const nextDelayToEvaluateFaillingTransformingFunction: Record = {}
+const lastTimeTriedToEvaluateFaillingTransformingFunction: Record = {}
+
+const setupTransformingFunctionsListeners = (): void => {
+ globalTransformingFunctions.forEach((func) => {
+ const dataLakeVariablesInExpression = getDataLakeVariableIdFromInput(func.expression)
+ if (dataLakeVariablesInExpression) {
+ const variableIds = findDataLakeVariablesIdsInString(func.expression)
+ variableIds.forEach((variableId) => {
+ const listenerId = listenDataLakeVariable(variableId, () => {
+ try {
+ // If the function is failing, we don't want to evaluate it too often
+ const currentDelay = nextDelayToEvaluateFaillingTransformingFunction[func.id] || 10
+ const lastTimeTried = lastTimeTriedToEvaluateFaillingTransformingFunction[func.id] || 0
+ if (currentDelay > 0 && lastTimeTried + currentDelay > Date.now()) {
+ return
+ } else {
+ const expressionValue = getExpressionValue(func)
+ setDataLakeVariableData(func.id, expressionValue)
+ }
+ } catch (error) {
+ lastTimeTriedToEvaluateFaillingTransformingFunction[func.id] = Date.now()
+ const currentDelay = nextDelayToEvaluateFaillingTransformingFunction[func.id] || 10
+ const nextDelay = Math.min(2 * currentDelay, 10000)
+ nextDelayToEvaluateFaillingTransformingFunction[func.id] = nextDelay
+ const msg = `Error evaluating expression for transforming function '${func.id}'. Next evaluation in ${nextDelay} ms. Error: ${error}`
+ console.error(msg)
+ }
+ })
+ if (!variablesListeners[func.id]) {
+ variablesListeners[func.id] = { [variableId]: [listenerId] }
+ } else if (!variablesListeners[func.id][variableId]) {
+ variablesListeners[func.id][variableId] = [listenerId]
+ } else {
+ variablesListeners[func.id][variableId].push(listenerId)
+ }
+ })
+ }
+ })
+}
+
+const deleteAllTransformingFunctionsListeners = (): void => {
+ Object.keys(nextDelayToEvaluateFaillingTransformingFunction).forEach((funcId) => {
+ delete nextDelayToEvaluateFaillingTransformingFunction[funcId]
+ delete lastTimeTriedToEvaluateFaillingTransformingFunction[funcId]
+ })
+ Object.keys(lastTimeTriedToEvaluateFaillingTransformingFunction).forEach((funcId) => {
+ delete lastTimeTriedToEvaluateFaillingTransformingFunction[funcId]
+ })
+ Object.entries(variablesListeners).forEach(([funcId, variableListeners]) => {
+ Object.entries(variableListeners).forEach(([variableId, listenerIds]) => {
+ listenerIds.forEach((listenerId) => unlistenDataLakeVariable(variableId, listenerId))
+ })
+ delete variablesListeners[funcId]
+ })
+}
+
+const deleteAllTransformingFunctionsVariables = (): void => {
+ globalTransformingFunctions.forEach((func) => {
+ deleteDataLakeVariable(func.id)
+ })
+}
+
+const setupAllTransformingFunctionsVariables = (): void => {
+ globalTransformingFunctions.forEach((func) => {
+ try {
+ createDataLakeVariable(new DataLakeVariable(func.id, func.name, func.type, func.description))
+ } catch (createError) {
+ const msg = `Could not create data lake variable info for transforming function ${func.id}. Error: ${createError}`
+ console.error(msg)
+ }
+ })
+}
+
+const updateTransformingFunctionListeners = (): void => {
+ deleteAllTransformingFunctionsListeners()
+ deleteAllTransformingFunctionsVariables()
+ setupAllTransformingFunctionsVariables()
+ setupTransformingFunctionsListeners()
+}
+
+/**
+ * Interface for a transforming function that creates a new data lake variable
+ * based on an expression using other variables
+ */
+export interface TransformingFunction {
+ /** Name of the new variable */
+ name: string
+ /** ID of the new variable */
+ id: string
+ /** Type of the new variable */
+ type: 'string' | 'number' | 'boolean'
+ /** Description of the new variable */
+ description?: string
+ /** JavaScript expression that defines how to calculate the new variable */
+ expression: string
+}
+
+/**
+ * Creates a new transforming function that listens to its dependencies
+ * and updates its value when they change
+ * @param {string} id - ID of the new variable
+ * @param {string} name - Name of the new variable
+ * @param {'string' | 'number' | 'boolean'} type - Type of the new variable
+ * @param {string} expression - Expression to calculate the variable's value
+ * @param {string?} description - Description of the new variable
+ */
+export const createTransformingFunction = (
+ id: string,
+ name: string,
+ type: 'string' | 'number' | 'boolean',
+ expression: string,
+ description?: string
+): void => {
+ const transformingFunction: TransformingFunction = { name, id, type, expression, description }
+ globalTransformingFunctions.push(transformingFunction)
+ createDataLakeVariable(new DataLakeVariable(id, name, type, description))
+ saveTransformingFunctions()
+}
+
+/**
+ * Returns all transforming functions
+ * @returns {TransformingFunction[]} All transforming functions
+ */
+export const getAllTransformingFunctions = (): TransformingFunction[] => {
+ return globalTransformingFunctions
+}
+
+/**
+ * Updates a transforming function
+ * @param {TransformingFunction} func - The function to update
+ */
+export const updateTransformingFunction = (func: TransformingFunction): void => {
+ const index = globalTransformingFunctions.findIndex((f) => f.id === func.id)
+ if (index !== -1) {
+ globalTransformingFunctions[index] = func
+ saveTransformingFunctions()
+ }
+}
+
+/**
+ * Deletes a transforming function and cleans up its subscriptions
+ * @param {TransformingFunction} func - The function to delete
+ */
+export const deleteTransformingFunction = (func: TransformingFunction): void => {
+ // Remove the variable from the data lake
+ globalTransformingFunctions = globalTransformingFunctions.filter((f) => f.id !== func.id)
+ deleteDataLakeVariable(func.id)
+ saveTransformingFunctions()
+}
+
+loadTransformingFunctions()
diff --git a/src/libs/utils-data-lake.ts b/src/libs/utils-data-lake.ts
index 93accf8cf..610584f64 100644
--- a/src/libs/utils-data-lake.ts
+++ b/src/libs/utils-data-lake.ts
@@ -70,6 +70,16 @@ export const replaceDataLakeInputsInString = (input: string, replaceFunction?: (
return input.toString().replace(dataLakeInputRegex, (match) => replaceFunctionToUse(match))
}
+/**
+ * Find all data lake variable ids in a string.
+ * @param {string} input The string to search for data lake variable ids
+ * @returns {string[]} An array of data lake variable ids
+ */
+export const findDataLakeVariablesIdsInString = (input: string): string[] => {
+ const inputs = findDataLakeInputsInString(input)
+ return inputs.map((i) => getDataLakeVariableIdFromInput(i)).filter((id) => id !== null)
+}
+
export const replaceDataLakeInputsInJsonString = (jsonString: string): string => {
let parsedJson = jsonString
diff --git a/src/libs/utils.ts b/src/libs/utils.ts
index a3b8d00bf..4db03b8ce 100644
--- a/src/libs/utils.ts
+++ b/src/libs/utils.ts
@@ -224,3 +224,17 @@ export const humanizeString = (str: string): string => {
.trim()
.replace(/\b\w/g, (match) => match.toUpperCase())
}
+
+/**
+ * Convert a string to a machine-friendly version of it
+ * @param {string} str The string to convert
+ * @returns {string} The machine-friendly string
+ */
+export const machinizeString = (str: string): string => {
+ return str
+ .toLowerCase()
+ .trim()
+ .replace(/[^a-zA-Z0-9-]/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-+|-+$/g, '')
+}
diff --git a/src/stores/appInterface.ts b/src/stores/appInterface.ts
index f330e555e..503e54a30 100644
--- a/src/stores/appInterface.ts
+++ b/src/stores/appInterface.ts
@@ -28,6 +28,7 @@ export enum SubMenuComponentName {
SettingsDev = 'settings-dev',
SettingsMission = 'settings-mission',
SettingsActions = 'settings-actions',
+ SettingsDataLake = 'settings-datalake',
ToolsMAVLink = 'tools-mavlink',
ToolsDataLake = 'tools-datalake',
}
diff --git a/src/views/ToolsDataLakeView.vue b/src/views/ToolsDataLakeView.vue
index 365945d79..faccf5868 100644
--- a/src/views/ToolsDataLakeView.vue
+++ b/src/views/ToolsDataLakeView.vue
@@ -24,6 +24,10 @@
@click="searchQuery = ''"
/>
+
+ mdi-plus
+ Add compound variable
+ updateListOfActiveVariables(currentItems)"
@@ -44,13 +49,8 @@