Implement Commanding (#6)
Reviewed-on: #6 Co-authored-by: Sergey Savelyev <sergeysav.nn@gmail.com> Co-committed-by: Sergey Savelyev <sergeysav.nn@gmail.com>
This commit was merged in pull request #6.
This commit is contained in:
40
frontend/src/components/CommandInput.vue
Normal file
40
frontend/src/components/CommandInput.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import {
|
||||
type AnyTypeId,
|
||||
type DynamicDataType,
|
||||
isBooleanType,
|
||||
isNumericType,
|
||||
} from '@/composables/dynamic.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
type: AnyTypeId;
|
||||
}>();
|
||||
|
||||
const model = defineModel<DynamicDataType>();
|
||||
|
||||
const is_numeric = computed(() => {
|
||||
return isNumericType(props.type);
|
||||
});
|
||||
|
||||
const is_boolean = computed(() => {
|
||||
return isBooleanType(props.type);
|
||||
});
|
||||
|
||||
// Initialize the parameter to some value:
|
||||
onMounted(() => {
|
||||
if (is_numeric.value) {
|
||||
model.value = 0.0;
|
||||
} else if (is_boolean.value) {
|
||||
model.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-if="is_numeric" type="number" v-model="model" />
|
||||
<input v-else-if="is_boolean" type="checkbox" v-model="model" />
|
||||
<span v-else>UNKNOWN INPUT</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
70
frontend/src/components/CommandList.vue
Normal file
70
frontend/src/components/CommandList.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { type CommandDefinition, useAllCommands } from '@/composables/command';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
search?: string;
|
||||
}>();
|
||||
|
||||
const selected = defineModel<CommandDefinition | null>();
|
||||
|
||||
const search_value = computed(() => (props.search || '').toLowerCase());
|
||||
|
||||
const { data: command_data } = useAllCommands();
|
||||
|
||||
const sorted_cmd_data = computed(() => {
|
||||
const cmd_data = command_data.value;
|
||||
if (cmd_data != null) {
|
||||
return cmd_data
|
||||
.filter((entry) =>
|
||||
entry.name.toLowerCase().includes(search_value.value),
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
function onClick(cmd_entry: CommandDefinition) {
|
||||
selected.value = cmd_entry;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="sorted_cmd_data.length > 0">
|
||||
<div
|
||||
v-for="cmd_entry in sorted_cmd_data"
|
||||
:class="`row data ${selected?.name == cmd_entry.name ? 'selected' : ''}`"
|
||||
:key="cmd_entry.name"
|
||||
@click="() => onClick(cmd_entry)"
|
||||
>
|
||||
<span>
|
||||
{{ cmd_entry.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row">
|
||||
<span> No Matches Found </span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
div {
|
||||
padding: 0.3em;
|
||||
border: 0;
|
||||
border-bottom: variables.$gray-3 solid 1px;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.data.selected:has(~ .data:hover),
|
||||
.data:hover ~ .data.selected {
|
||||
background-color: variables.$light-background-color;
|
||||
}
|
||||
|
||||
.data.selected,
|
||||
.data:hover {
|
||||
background-color: variables.$light2-background-color;
|
||||
}
|
||||
</style>
|
||||
23
frontend/src/components/CommandParameter.vue
Normal file
23
frontend/src/components/CommandParameter.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { CommandParameterDefinition } from '@/composables/command.ts';
|
||||
import { type DynamicDataType } from '@/composables/dynamic.ts';
|
||||
import CommandInput from '@/components/CommandInput.vue';
|
||||
|
||||
defineProps<{
|
||||
parameter: CommandParameterDefinition;
|
||||
}>();
|
||||
|
||||
const model = defineModel<DynamicDataType>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<label> {{ parameter.name }} </label>
|
||||
<CommandInput
|
||||
:type="parameter.data_type"
|
||||
v-model="model"
|
||||
></CommandInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
29
frontend/src/components/CommandParameterDataConfigurator.vue
Normal file
29
frontend/src/components/CommandParameterDataConfigurator.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { type CommandParameterData } from '@/composables/dynamic.ts';
|
||||
import { type CommandParameterDefinition } from '@/composables/command.ts';
|
||||
import CommandInput from '@/components/CommandInput.vue';
|
||||
|
||||
defineProps<{
|
||||
param: CommandParameterDefinition;
|
||||
}>();
|
||||
|
||||
const model = defineModel<CommandParameterData>({
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="model.type == 'constant'" class="row">
|
||||
<label>Value:</label>
|
||||
<CommandInput
|
||||
:type="param.data_type"
|
||||
v-model="model.value"
|
||||
></CommandInput>
|
||||
</div>
|
||||
<div v-if="model.type == 'input'" class="row">
|
||||
<label>ID:</label>
|
||||
<input type="text" v-model="model.id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
99
frontend/src/components/CommandParameterListConfigurator.vue
Normal file
99
frontend/src/components/CommandParameterListConfigurator.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type CommandParameterData,
|
||||
type DynamicDataType,
|
||||
isBooleanType,
|
||||
isNumericType,
|
||||
} from '@/composables/dynamic.ts';
|
||||
import { useCommand } from '@/composables/command.ts';
|
||||
import { watch } from 'vue';
|
||||
import FlexDivider from '@/components/FlexDivider.vue';
|
||||
import CommandParameterDataConfigurator from '@/components/CommandParameterDataConfigurator.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
command_name: string;
|
||||
}>();
|
||||
|
||||
const model = defineModel<{ [key: string]: CommandParameterData }>({
|
||||
required: true,
|
||||
});
|
||||
|
||||
const { data: command_info } = useCommand(props.command_name);
|
||||
|
||||
watch([command_info], ([cmd_info]) => {
|
||||
if (cmd_info == null) {
|
||||
return;
|
||||
}
|
||||
const model_value = model.value;
|
||||
for (const key in model_value) {
|
||||
const is_valid_param = cmd_info.parameters.some(
|
||||
(param) => param.name == key,
|
||||
);
|
||||
if (!is_valid_param) {
|
||||
delete model_value[key];
|
||||
}
|
||||
}
|
||||
for (const param of cmd_info.parameters) {
|
||||
let model_param_value: CommandParameterData | undefined =
|
||||
model_value[param.name];
|
||||
if (model_param_value) {
|
||||
switch (model_param_value.type) {
|
||||
case 'constant':
|
||||
if (
|
||||
typeof model_param_value.value == 'number' &&
|
||||
!isNumericType(param.data_type)
|
||||
) {
|
||||
model_param_value = undefined;
|
||||
} else if (
|
||||
typeof model_param_value.value == 'boolean' &&
|
||||
!isBooleanType(param.data_type)
|
||||
) {
|
||||
model_param_value = undefined;
|
||||
}
|
||||
break;
|
||||
case 'input':
|
||||
// Nothing to do
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!model_param_value) {
|
||||
let default_value: DynamicDataType = 0;
|
||||
if (isNumericType(param.data_type)) {
|
||||
default_value = 0;
|
||||
} else if (isBooleanType(param.data_type)) {
|
||||
default_value = false;
|
||||
}
|
||||
model_param_value = {
|
||||
type: 'constant',
|
||||
value: default_value,
|
||||
};
|
||||
}
|
||||
model_value[param.name] = model_param_value;
|
||||
}
|
||||
model.value = model_value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="command_info">
|
||||
<template v-for="param in command_info.parameters" :key="param.name">
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="row">
|
||||
<label>{{ param.name }}</label>
|
||||
<select v-model="model[param.name].type">
|
||||
<option value="constant">Constant</option>
|
||||
<option value="input">Input</option>
|
||||
</select>
|
||||
</div>
|
||||
<CommandParameterDataConfigurator
|
||||
:param="param"
|
||||
v-model="model[param.name]"
|
||||
></CommandParameterDataConfigurator>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span> Loading... </span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
68
frontend/src/components/CommandSender.vue
Normal file
68
frontend/src/components/CommandSender.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import type { CommandDefinition } from '@/composables/command.ts';
|
||||
import { ref } from 'vue';
|
||||
import CommandParameter from '@/components/CommandParameter.vue';
|
||||
import FlexDivider from '@/components/FlexDivider.vue';
|
||||
import type { DynamicDataType } from '@/composables/dynamic.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
command: CommandDefinition | null;
|
||||
}>();
|
||||
|
||||
const parameters = ref<{ [key: string]: DynamicDataType }>({});
|
||||
const busy = ref(false);
|
||||
const result = ref('');
|
||||
|
||||
async function sendCommand() {
|
||||
const command = props.command;
|
||||
const params = parameters.value;
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
result.value = 'Loading...';
|
||||
|
||||
const response = await fetch(`/api/cmd/${command.name}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.ok) {
|
||||
result.value = await response.json();
|
||||
} else {
|
||||
result.value = await response.text();
|
||||
}
|
||||
busy.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row" v-if="!command">
|
||||
<span> No Command Selected </span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="row">
|
||||
<span>
|
||||
{{ command.name }}
|
||||
</span>
|
||||
</div>
|
||||
<FlexDivider></FlexDivider>
|
||||
<CommandParameter
|
||||
v-for="param in command.parameters"
|
||||
:key="param.name"
|
||||
:parameter="param"
|
||||
v-model="parameters[param.name]"
|
||||
></CommandParameter>
|
||||
<div class="row">
|
||||
<button :disabled="busy" @click.stop.prevent="sendCommand">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div class="row shrink grow"></div>
|
||||
<div class="row">{{ result }}</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { OptionalDynamicComponentData } from '@/composables/dynamic.ts';
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import {
|
||||
AnyTypes,
|
||||
type CommandParameterData,
|
||||
type DynamicDataType,
|
||||
type OptionalDynamicComponentData,
|
||||
} from '@/composables/dynamic.ts';
|
||||
import { computed, defineAsyncComponent, inject, type Ref, ref } from 'vue';
|
||||
import CommandParameterListConfigurator from '@/components/CommandParameterListConfigurator.vue';
|
||||
|
||||
const TelemetryValue = defineAsyncComponent(
|
||||
() => import('@/components/TelemetryValue.vue'),
|
||||
@@ -8,6 +14,9 @@ const TelemetryValue = defineAsyncComponent(
|
||||
const GridLayout = defineAsyncComponent(
|
||||
() => import('@/components/layout/GridLayout.vue'),
|
||||
);
|
||||
const CommandInput = defineAsyncComponent(
|
||||
() => import('@/components/CommandInput.vue'),
|
||||
);
|
||||
|
||||
const model = defineModel<OptionalDynamicComponentData>('data', {
|
||||
required: true,
|
||||
@@ -19,13 +28,26 @@ const props = defineProps<{
|
||||
editable: boolean;
|
||||
}>();
|
||||
|
||||
const busy = ref(false);
|
||||
|
||||
// Provide a fallback option
|
||||
const inputs = inject<Ref<{ [id: string]: DynamicDataType }>>(
|
||||
'inputs',
|
||||
ref({}),
|
||||
);
|
||||
|
||||
const thisSymbol = Symbol();
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return selection.value == thisSymbol && props.editable;
|
||||
});
|
||||
|
||||
function selectThis() {
|
||||
function selectThis(e: Event) {
|
||||
if (props.editable) {
|
||||
// Only do this when we are editable
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
selection.value = thisSymbol;
|
||||
}
|
||||
|
||||
@@ -100,6 +122,54 @@ function deleteColumn() {
|
||||
model.value = grid;
|
||||
}
|
||||
}
|
||||
|
||||
function makeInput() {
|
||||
model.value = {
|
||||
type: 'input',
|
||||
id: [...Array(32)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
data_type: 'Float32',
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommandButton() {
|
||||
model.value = {
|
||||
type: 'command_button',
|
||||
text: 'Button Text',
|
||||
command_name: '',
|
||||
parameters: {},
|
||||
};
|
||||
}
|
||||
|
||||
async function sendCommand(command: {
|
||||
command_name: string;
|
||||
parameters: { [key: string]: CommandParameterData };
|
||||
}) {
|
||||
busy.value = true;
|
||||
|
||||
const params: { [key: string]: DynamicDataType } = {};
|
||||
for (const param_name in command.parameters) {
|
||||
const parameter = command.parameters[param_name];
|
||||
switch (parameter.type) {
|
||||
case 'constant':
|
||||
params[param_name] = parameter.value;
|
||||
break;
|
||||
case 'input':
|
||||
params[param_name] = inputs.value[parameter.id];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await fetch(`/api/cmd/${command.command_name}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
busy.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -123,20 +193,30 @@ function deleteColumn() {
|
||||
<button v-if="model.type != 'grid'" @click.stop.prevent="makeGrid">
|
||||
Make Grid
|
||||
</button>
|
||||
<button
|
||||
v-if="model.type != 'input'"
|
||||
@click.stop.prevent="makeInput"
|
||||
>
|
||||
Make Input
|
||||
</button>
|
||||
<button
|
||||
v-if="model.type != 'command_button'"
|
||||
@click.stop.prevent="makeCommandButton"
|
||||
>
|
||||
Make Command Button
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
<template v-if="model.type == 'none'">
|
||||
<!-- Intentionally Left Empty -->
|
||||
<span
|
||||
v-if="editable"
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
@click.stop.prevent="selectThis"
|
||||
@click="selectThis"
|
||||
></span>
|
||||
</template>
|
||||
<template v-else-if="model.type == 'text'">
|
||||
<span
|
||||
:class="`${model.justify_right ? 'justify-right' : ''} ${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
@click.stop.prevent="selectThis"
|
||||
@click="selectThis"
|
||||
>
|
||||
{{ model.text }}
|
||||
</span>
|
||||
@@ -155,7 +235,7 @@ function deleteColumn() {
|
||||
<span
|
||||
v-if="editable"
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
@click.stop.prevent="selectThis"
|
||||
@click="selectThis"
|
||||
>
|
||||
{{ '{' }} {{ model.data }} {{ '}' }}
|
||||
</span>
|
||||
@@ -163,7 +243,7 @@ function deleteColumn() {
|
||||
v-else
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
:data="model.data"
|
||||
@click.stop.prevent="selectThis"
|
||||
@click="selectThis"
|
||||
></TelemetryValue>
|
||||
<Teleport v-if="isSelected" to="#inspector">
|
||||
<label>Telemetry Item: </label>
|
||||
@@ -175,7 +255,7 @@ function deleteColumn() {
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
:cols="model.columns"
|
||||
:equal_col_width="model.equal_width"
|
||||
@click.stop.prevent="selectThis"
|
||||
@click="selectThis"
|
||||
>
|
||||
<template v-for="x in model.cells.length" :key="x">
|
||||
<template v-for="y in model.columns" :key="y">
|
||||
@@ -210,6 +290,60 @@ function deleteColumn() {
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<template v-else-if="model.type == 'input'">
|
||||
<span
|
||||
v-if="editable"
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
@click="selectThis"
|
||||
>
|
||||
{{ '[' }} {{ model.id }} {{ ']' }}
|
||||
</span>
|
||||
<CommandInput
|
||||
v-else
|
||||
:type="model.data_type"
|
||||
v-model="inputs[model.id]"
|
||||
></CommandInput>
|
||||
<Teleport v-if="isSelected" to="#inspector">
|
||||
<div class="row">
|
||||
<label>Input ID: </label>
|
||||
<input v-model="model.id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Data Type: </label>
|
||||
<select v-model="model.data_type">
|
||||
<option v-for="type in AnyTypes" :key="type" :value="type">
|
||||
{{ type }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<template v-else-if="model.type == 'command_button'">
|
||||
<button
|
||||
:disabled="busy"
|
||||
:class="`${editable ? 'editable' : ''} ${isSelected ? 'selected' : ''}`"
|
||||
@click.stop.prevent="
|
||||
(e) => (editable ? selectThis(e) : sendCommand(model as any))
|
||||
"
|
||||
>
|
||||
{{ model.text }}
|
||||
</button>
|
||||
<Teleport v-if="isSelected" to="#inspector">
|
||||
<div class="row">
|
||||
<label>Button Text: </label>
|
||||
<input v-model="model.text" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Command: </label>
|
||||
<input v-model="model.command_name" />
|
||||
</div>
|
||||
<CommandParameterListConfigurator
|
||||
:key="model.command_name"
|
||||
:command_name="model.command_name"
|
||||
v-model="model.parameters"
|
||||
></CommandParameterListConfigurator>
|
||||
</Teleport>
|
||||
</template>
|
||||
<template v-else> ERROR: Unknown data: {{ model }} </template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ import { AXIS_DATA, type AxisData } from '@/graph/axis';
|
||||
import ValueLabel from '@/components/ValueLabel.vue';
|
||||
import { type Point, PointLine } from '@/graph/line';
|
||||
import TooltipDialog from '@/components/TooltipDialog.vue';
|
||||
import {
|
||||
type DynamicDataType,
|
||||
isBooleanType,
|
||||
isNumericType,
|
||||
} from '@/composables/dynamic.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
data: string;
|
||||
@@ -98,13 +103,9 @@ watch([value], ([val]) => {
|
||||
if (val_t >= min_x) {
|
||||
const raw_item_val = val.value[telemetry_data.value!.data_type];
|
||||
let item_val = 0;
|
||||
if (
|
||||
['Float32', 'Float64'].some(
|
||||
(e) => e == telemetry_data.value!.data_type,
|
||||
)
|
||||
) {
|
||||
if (isNumericType(telemetry_data.value!.data_type)) {
|
||||
item_val = raw_item_val as number;
|
||||
} else if (telemetry_data.value!.data_type == 'Boolean') {
|
||||
} else if (isBooleanType(telemetry_data.value!.data_type)) {
|
||||
item_val = (raw_item_val as boolean) ? 1 : 0;
|
||||
}
|
||||
const new_item = {
|
||||
@@ -140,15 +141,13 @@ watch(
|
||||
const response = (await res.json()) as TelemetryDataItem[];
|
||||
for (const data_item of response) {
|
||||
const val_t = Date.parse(data_item.timestamp);
|
||||
const raw_item_val = data_item.value[type];
|
||||
const raw_item_val = data_item.value[
|
||||
type
|
||||
] as DynamicDataType;
|
||||
let item_val = 0;
|
||||
if (
|
||||
['Float32', 'Float64'].some(
|
||||
(e) => e == telemetry_data.value!.data_type,
|
||||
)
|
||||
) {
|
||||
if (isNumericType(type)) {
|
||||
item_val = raw_item_val as number;
|
||||
} else if (type == 'Boolean') {
|
||||
} else if (isBooleanType(type)) {
|
||||
item_val = (raw_item_val as boolean) ? 1 : 0;
|
||||
}
|
||||
const new_item = {
|
||||
|
||||
53
frontend/src/composables/command.ts
Normal file
53
frontend/src/composables/command.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ref, toValue, watchEffect } from 'vue';
|
||||
import { type MaybeRefOrGetter } from 'vue';
|
||||
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
||||
|
||||
export interface CommandParameterDefinition {
|
||||
name: string;
|
||||
data_type: AnyTypeId;
|
||||
}
|
||||
|
||||
export interface CommandDefinition {
|
||||
name: string;
|
||||
parameters: CommandParameterDefinition[];
|
||||
}
|
||||
|
||||
export function useAllCommands() {
|
||||
const data = ref<CommandDefinition[] | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const error = ref<any | null>(null);
|
||||
|
||||
watchEffect(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/cmd`);
|
||||
data.value = await res.json();
|
||||
error.value = null;
|
||||
} catch (e) {
|
||||
data.value = null;
|
||||
error.value = e;
|
||||
}
|
||||
});
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export function useCommand(name: MaybeRefOrGetter<string>) {
|
||||
const data = ref<CommandDefinition | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const error = ref<any | null>(null);
|
||||
|
||||
watchEffect(async () => {
|
||||
const name_value = toValue(name);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/cmd/${name_value}`);
|
||||
data.value = await res.json();
|
||||
error.value = null;
|
||||
} catch (e) {
|
||||
data.value = null;
|
||||
error.value = e;
|
||||
}
|
||||
});
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
@@ -1,3 +1,29 @@
|
||||
export const NumericTypes = ['Float32', 'Float64'] as const;
|
||||
export type NumericTypeId = (typeof NumericTypes)[number];
|
||||
export const BooleanTypes = ['Boolean'] as const;
|
||||
export type BooleanTypeId = (typeof BooleanTypes)[number];
|
||||
export const AnyTypes = [...NumericTypes, ...BooleanTypes] as const;
|
||||
export type AnyTypeId = (typeof AnyTypes)[number];
|
||||
|
||||
export function isNumericType(type: AnyTypeId): type is NumericTypeId {
|
||||
return NumericTypes.some((it) => it == type);
|
||||
}
|
||||
export function isBooleanType(type: AnyTypeId): type is BooleanTypeId {
|
||||
return BooleanTypes.some((it) => it == type);
|
||||
}
|
||||
|
||||
export type DynamicDataType = number | boolean;
|
||||
|
||||
export type CommandParameterData =
|
||||
| {
|
||||
type: 'constant';
|
||||
value: DynamicDataType;
|
||||
}
|
||||
| {
|
||||
type: 'input';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DynamicComponentData =
|
||||
| { type: 'text'; text: string; justify_right: boolean }
|
||||
| { type: 'telemetry'; data: string }
|
||||
@@ -6,6 +32,17 @@ export type DynamicComponentData =
|
||||
columns: number;
|
||||
equal_width: boolean;
|
||||
cells: OptionalDynamicComponentData[][];
|
||||
}
|
||||
| {
|
||||
type: 'input';
|
||||
id: string;
|
||||
data_type: AnyTypeId;
|
||||
}
|
||||
| {
|
||||
type: 'command_button';
|
||||
text: string;
|
||||
command_name: string;
|
||||
parameters: { [key: string]: CommandParameterData };
|
||||
};
|
||||
|
||||
export type OptionalDynamicComponentData =
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ref, toValue, watchEffect } from 'vue';
|
||||
import { type MaybeRefOrGetter } from 'vue';
|
||||
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
||||
|
||||
export interface TelemetryDefinition {
|
||||
uuid: string;
|
||||
name: string;
|
||||
data_type: string;
|
||||
data_type: AnyTypeId;
|
||||
}
|
||||
|
||||
export function useAllTelemetry() {
|
||||
|
||||
@@ -34,7 +34,12 @@ export function usePanelHeirarchy(): Ref<PanelHeirarchyChildren> {
|
||||
},
|
||||
{
|
||||
name: 'Telemetry Elements',
|
||||
to: { name: 'list' },
|
||||
to: { name: 'tlm' },
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
},
|
||||
{
|
||||
name: 'Commands',
|
||||
to: { name: 'cmd' },
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,10 +14,15 @@ const router = createRouter({
|
||||
component: () => import('../views/GraphView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
path: '/tlm',
|
||||
name: 'tlm',
|
||||
component: () => import('../views/TelemetryListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/cmd',
|
||||
name: 'cmd',
|
||||
component: () => import('../views/CommandListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chart',
|
||||
name: 'chart',
|
||||
|
||||
61
frontend/src/views/CommandListView.vue
Normal file
61
frontend/src/views/CommandListView.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import TextInput from '@/components/TextInput.vue';
|
||||
import { ref } from 'vue';
|
||||
import FlexDivider from '@/components/FlexDivider.vue';
|
||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
import LinearLayout from '@/components/layout/LinearLayout.vue';
|
||||
import CommandList from '@/components/CommandList.vue';
|
||||
import type { CommandDefinition } from '@/composables/command.ts';
|
||||
import CommandSender from '@/components/CommandSender.vue';
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
const selected = ref<CommandDefinition | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScreenLayout :type="ScreenType.Standard" limit>
|
||||
<LinearLayout
|
||||
:direction="Direction.Row"
|
||||
stretch
|
||||
class="grow no-min-height no-basis"
|
||||
>
|
||||
<div class="column grow2 stretch no-min-height no-basis">
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
class="grow"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
></TextInput>
|
||||
</div>
|
||||
|
||||
<div class="row scroll no-min-height">
|
||||
<div class="column grow stretch">
|
||||
<CommandList
|
||||
:search="searchValue"
|
||||
v-model="selected"
|
||||
></CommandList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow stretch no-basis command-sender">
|
||||
<CommandSender
|
||||
:command="selected"
|
||||
:key="selected?.name || ''"
|
||||
></CommandSender>
|
||||
</div>
|
||||
</LinearLayout>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.command-sender {
|
||||
row-gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import DynamicComponent from '@/components/DynamicComponent.vue';
|
||||
import type { OptionalDynamicComponentData } from '@/composables/dynamic.ts';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import type {
|
||||
DynamicDataType,
|
||||
OptionalDynamicComponentData,
|
||||
} from '@/composables/dynamic.ts';
|
||||
import { computed, provide, ref, watchEffect } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -12,6 +15,10 @@ const panel = ref<OptionalDynamicComponentData>({
|
||||
type: 'none',
|
||||
});
|
||||
|
||||
const inputs = ref<{ [id: string]: DynamicDataType }>({});
|
||||
|
||||
provide('inputs', inputs);
|
||||
|
||||
watchEffect(async () => {
|
||||
const panel_data = await fetch(`/api/panel/${id.value}`);
|
||||
const panel_json_value = await panel_data.json();
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { TelemetryDefinition } from '@/composables/telemetry';
|
||||
import TelemetryInfo from '@/components/TelemetryInfo.vue';
|
||||
import FlexDivider from '@/components/FlexDivider.vue';
|
||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
const searchValue = ref('');
|
||||
@@ -16,36 +15,38 @@ const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScreenLayout :direction="Direction.Row" :type="ScreenType.Standard" limit>
|
||||
<div class="column grow2 stretch no-min-height no-basis">
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
class="grow"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
></TextInput>
|
||||
</div>
|
||||
<ScreenLayout :type="ScreenType.Standard" limit>
|
||||
<div class="row grow stretch no-min-height no-basis">
|
||||
<div class="column grow2 stretch no-min-height no-basis">
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
class="grow"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
></TextInput>
|
||||
</div>
|
||||
|
||||
<div class="row scroll no-min-height">
|
||||
<div class="column grow stretch">
|
||||
<TelemetryList
|
||||
:search="searchValue"
|
||||
v-model="selected"
|
||||
@mouseover="
|
||||
(mousedover_value) =>
|
||||
(mousedover = mousedover_value)
|
||||
"
|
||||
></TelemetryList>
|
||||
<div class="row scroll no-min-height">
|
||||
<div class="column grow stretch">
|
||||
<TelemetryList
|
||||
:search="searchValue"
|
||||
v-model="selected"
|
||||
@mouseover="
|
||||
(mousedover_value) =>
|
||||
(mousedover = mousedover_value)
|
||||
"
|
||||
></TelemetryList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow stretch no-basis">
|
||||
<TelemetryInfo
|
||||
:mouseover="mousedover"
|
||||
:selection="selected"
|
||||
></TelemetryInfo>
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow stretch no-basis">
|
||||
<TelemetryInfo
|
||||
:mouseover="mousedover"
|
||||
:selection="selected"
|
||||
></TelemetryInfo>
|
||||
</div>
|
||||
</div>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user