initial frontend command stuff
This commit is contained in:
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>
|
||||
37
frontend/src/components/CommandParameter.vue
Normal file
37
frontend/src/components/CommandParameter.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { CommandParameterDefinition } from '@/composables/command.ts';
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
parameter: CommandParameterDefinition;
|
||||
}>();
|
||||
|
||||
const model = defineModel<any>(); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const is_numeric = computed(() => {
|
||||
return ['Float32', 'Float64'].some((x) => x == props.parameter.data_type);
|
||||
});
|
||||
|
||||
const is_boolean = computed(() => {
|
||||
return 'Boolean' == props.parameter.data_type;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (is_numeric.value) {
|
||||
model.value = 0.0;
|
||||
} else if (is_boolean.value) {
|
||||
model.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<label> {{ parameter.name }} </label>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
67
frontend/src/components/CommandSender.vue
Normal file
67
frontend/src/components/CommandSender.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<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';
|
||||
|
||||
const props = defineProps<{
|
||||
command: CommandDefinition | null;
|
||||
}>();
|
||||
|
||||
const parameters = ref<any>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
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>
|
||||
52
frontend/src/composables/command.ts
Normal file
52
frontend/src/composables/command.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ref, toValue, watchEffect } from 'vue';
|
||||
import { type MaybeRefOrGetter } from 'vue';
|
||||
|
||||
export interface CommandParameterDefinition {
|
||||
name: string;
|
||||
data_type: string;
|
||||
}
|
||||
|
||||
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 ueCommand(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 };
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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