adds telemetry list

This commit is contained in:
2025-02-14 20:22:31 -08:00
parent 44523f3cdb
commit a864c0b41c
19 changed files with 553 additions and 58 deletions

View File

@@ -0,0 +1,67 @@
.column {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
}
.gap_half {
gap: 0.5em;
}
.row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: center;
gap: 1em;
}
.center {
justify-content: center;
align-items: center;
align-content: center;
}
.screen {
max-height: 100vh;
width: 100%;
}
.content {
margin: 20vh 10vw;
max-width: 1200px;
}
.screen.content {
width: calc(100% - 20vw);
height: calc(100vh - 40vh);
}
.stretch {
align-items: stretch;
}
.grow {
flex-grow: 1;
}
.grow2 {
flex-grow: 2;
}
.no-basis {
flex-basis: 0;
}
.no-min-height {
min-height: 0;
}
.scroll {
overflow: auto;
}

View File

@@ -4,17 +4,32 @@
body {
color: variables.$text-color;
font-size: variables.$normal-text-size;
font-family: variables.$text-font;
background-color: variables.$background-color;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
align-content: center;
min-height: 100vh;
margin: 0;
}
main {
width: 100%;
}
#app {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-content: center;
min-height: 100vh;
}
* {
stroke-width: 0;
box-sizing: border-box;
}
polyline {
@@ -26,3 +41,11 @@ polyline {
#{--indexed-color}: list.nth(variables.$colors, $i);
}
}
a,
a:visited,
a:hover,
a:active {
text-decoration: inherit;
color: inherit;
}

View File

@@ -27,8 +27,10 @@ $magenta-2: oklch(75% 0.2 330);
$text-color: $gray-1;
$background-color: $gray-7;
$light2-background-color: color.adjust($background-color, $lightness: 10%);
$light-background-color: color.adjust($background-color, $lightness: 5%);
$dark-background-color: color.adjust($background-color, $lightness: -5%);
$dark2-background-color: color.adjust($background-color, $lightness: -10%);
$cursor-tick: $gray-0;
$time-tick: $gray-1;

View File

@@ -0,0 +1,13 @@
<script setup lang="ts"></script>
<template>
<div></div>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
div {
align-self: stretch;
border: 1px solid variables.$major-tick;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import {
type PanelHeirarchyChildren,
PanelHeirarchyType,
} from '@/panels/panel';
defineProps<{
heirarchy: PanelHeirarchyChildren;
}>();
</script>
<template>
<ul>
<li v-for="child in heirarchy" :key="child.name">
<RouterLink
v-if="child.type == PanelHeirarchyType.LEAF"
:to="child.to"
>
{{ child.name }}
</RouterLink>
<template v-if="child.type == PanelHeirarchyType.FOLDER">
{{ child.name }}
<PanelHeirarchy :heirarchy="child.children"> </PanelHeirarchy>
</template>
</li>
</ul>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -35,6 +35,7 @@ const width = ref(0);
const height = ref(0);
const controls_height = 32;
const min_time_label_separation = 250;
const resize_observer = new ResizeObserver((elements) => {
for (const element of elements) {
@@ -80,6 +81,7 @@ const time_lines = [
10000, // 10s
30000, // 30s
60000, // 1m
150000, // 2.5m
300000, // 5m
6000000, // 10m
18000000, // 30m
@@ -92,7 +94,7 @@ const time_lines = [
1728000000, // 2d
6048000000, // 1w
];
time_lines.reverse();
// time_lines.reverse();
const text_offset = computed(() => 5);
const legend_width = 160;
@@ -192,15 +194,20 @@ const legend_y_stride = computed(() => 16);
const legend_width_output = computed(() => legend_width - 8);
const line_duration = computed(() => {
const width_px = width.value;
const diff_x = max_x.value - min_x.value;
return time_lines.find((duration) => diff_x / duration >= 2)!;
return time_lines.find((duration) => {
const line_count = diff_x / duration;
const width_per_line = width_px / line_count;
return width_per_line >= min_time_label_separation;
})!;
});
const lines = computed(() => {
const result = [];
for (
let i = Math.ceil(max_x.value / line_duration.value);
i >= Math.ceil(min_x.value / line_duration.value) - 5;
i >= Math.floor(min_x.value / line_duration.value);
i--
) {
const x = i * line_duration.value;
@@ -356,6 +363,7 @@ provide<GraphData>(GRAPH_DATA, {
:timestamp="tick"
:utc="props.utc"
:show_millis="line_duration < 1000"
:key="tick"
></TimeText>
</template>
</g>
@@ -425,8 +433,4 @@ div.controls-header {
gap: 0 1em;
margin: 0 1em 0 1em;
}
div.controls-header > div.grow {
flex-grow: 1;
}
</style>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { TelemetryDefinition } from '@/composables/telemetry';
import Line from '@/components/TelemetryLine.vue';
import SvgGraph from '@/components/SvgGraph.vue';
import Axis from '@/components/GraphAxis.vue';
import { computed } from 'vue';
const props = defineProps<{
telemetry_definition: TelemetryDefinition | null;
secondary: TelemetryDefinition | null;
}>();
const lines = computed(() => {
const result = [];
if (props.secondary) {
result.push(props.secondary);
}
if (props.telemetry_definition) {
result.push(props.telemetry_definition);
}
return result;
});
</script>
<template>
<div class="row">
<span>
{{ telemetry_definition?.name || 'No Telemetry Selected' }}
</span>
</div>
<div :class="`row ${telemetry_definition == null ? 'hidden' : ''}`">
<span>
{{ telemetry_definition?.uuid || '0' }}
</span>
</div>
<div :class="`row ${telemetry_definition == null ? 'hidden' : ''}`">
<span>
{{ telemetry_definition?.data_type || '0' }}
</span>
</div>
<div class="row graph grow no-min-height no-basis">
<SvgGraph right_axis>
<Axis>
<Line
v-for="line in lines"
:data="line.name"
:key="line.uuid"
></Line>
</Axis>
</SvgGraph>
</div>
</template>
<style scoped lang="scss">
.graph {
min-height: 50px;
}
.hidden {
visibility: hidden;
}
</style>

View File

@@ -525,21 +525,8 @@ rect.legend:has(~ .legend:hover) {
cursor: help;
}
div.column {
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
gap: 0.25em;
}
div.column,
div.row {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: center;
align-content: center;
gap: 0.25em;
}

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import {
type TelemetryDefinition,
useAllTelemetry,
} from '@/composables/telemetry';
import { computed, ref, watch } from 'vue';
const props = defineProps<{
search?: string;
}>();
const emit = defineEmits<{
(
e: 'select',
tlm_entry: [TelemetryDefinition | null, TelemetryDefinition | null],
): void;
}>();
const search_value = computed(() => (props.search || '').toLowerCase());
const { data: telemetry_data } = useAllTelemetry();
const sorted_tlm_data = computed(() => {
const tlm_data = telemetry_data.value;
if (tlm_data != null) {
return tlm_data
.filter((entry) =>
entry.name.toLowerCase().includes(search_value.value),
)
.sort((a, b) => a.name.localeCompare(b.name));
}
return [];
});
const mousedover = ref<TelemetryDefinition | null>(null);
const selected = ref<TelemetryDefinition | null>(null);
function onMouseover(tlm_entry: TelemetryDefinition) {
mousedover.value = tlm_entry;
}
function onMouseleave() {
mousedover.value = null;
}
function onClick(tlm_entry: TelemetryDefinition) {
selected.value = tlm_entry;
}
watch([mousedover, selected], ([mousedover_val, selected_val]) => {
if (mousedover_val) {
emit('select', [
mousedover_val,
mousedover_val.uuid != selected_val?.uuid ? selected_val : null,
]);
} else if (selected_val) {
emit('select', [selected_val, null]);
} else {
emit('select', [null, null]);
}
});
</script>
<template>
<template v-if="sorted_tlm_data.length > 0">
<div
v-for="tlm_entry in sorted_tlm_data"
:class="`row data ${selected?.uuid == tlm_entry.uuid ? 'selected' : ''}`"
:key="tlm_entry.uuid"
@mouseover="() => onMouseover(tlm_entry)"
@mouseleave="() => onMouseleave()"
@click="() => onClick(tlm_entry)"
>
<span>
{{ tlm_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>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
const props = defineProps<{
autofocus?: boolean;
placeholder?: string;
}>();
const model = defineModel();
const emit = defineEmits<{
(e: 'enter'): void;
}>();
const inputRef = useTemplateRef<HTMLInputElement>('input-ref');
onMounted(() => {
if (props.autofocus) {
inputRef.value?.focus();
}
});
</script>
<template>
<input
ref="input-ref"
class="grow"
v-model="model"
:placeholder="placeholder"
v-on:keyup.enter="emit('enter')"
/>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
input {
appearance: none;
color: inherit;
background-color: inherit;
border: 0 solid variables.$gray-3;
border-bottom: 1px solid variables.$gray-3;
padding: 0.5ex;
font-size: inherit;
outline: none;
min-width: 5em;
}
</style>

View File

@@ -26,7 +26,6 @@ export function useAllTelemetry() {
return { data, error };
}
export function useTelemetry(name: MaybeRefOrGetter<string>) {
const data = ref<TelemetryDefinition | null>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,4 +1,5 @@
import './assets/main.scss';
import './assets/layout.scss';
import { createApp } from 'vue';
import App from './App.vue';

View File

@@ -0,0 +1,92 @@
import type { RouteLocationRaw } from 'vue-router';
export enum PanelHeirarchyType {
LEAF,
FOLDER,
}
export type PanelHeirarchyLeaf = {
name: string;
to: RouteLocationRaw;
type: PanelHeirarchyType.LEAF;
};
export type PanelHeirarchyFolder = {
name: string;
children: PanelHeirarchyChildren;
type: PanelHeirarchyType.FOLDER;
};
export type PanelHeirarchyChildren = (
| PanelHeirarchyFolder
| PanelHeirarchyLeaf
)[];
export function getPanelHeirarchy(): PanelHeirarchyChildren {
const result: PanelHeirarchyChildren = [];
result.push({
name: 'Graph Test',
to: { name: 'graph' },
type: PanelHeirarchyType.LEAF,
});
result.push({
name: 'Telemetry Elements',
to: { name: 'list' },
type: PanelHeirarchyType.LEAF,
});
return result;
}
export function filterHeirarchy(
heirarchy: PanelHeirarchyChildren,
predicate: (leaf: PanelHeirarchyLeaf) => boolean,
): PanelHeirarchyChildren {
const result: PanelHeirarchyChildren = [];
for (const element of heirarchy) {
switch (element.type) {
case PanelHeirarchyType.LEAF:
if (predicate(element)) {
result.push(element);
}
break;
case PanelHeirarchyType.FOLDER:
const folder_contents = filterHeirarchy(
element.children,
predicate,
);
if (folder_contents.length > 0) {
result.push({
name: element.name,
children: folder_contents,
type: PanelHeirarchyType.FOLDER,
});
}
break;
}
}
return result;
}
export function getFirstLeaf(
heirarchy: PanelHeirarchyChildren,
): PanelHeirarchyLeaf | null {
for (const element of heirarchy) {
switch (element.type) {
case PanelHeirarchyType.LEAF:
return element;
case PanelHeirarchyType.FOLDER:
const leaf = getFirstLeaf(element.children);
if (leaf != null) {
return leaf;
}
break;
}
}
return null;
}

View File

@@ -6,7 +6,7 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
component: () => import('../views/EmptyPanelView.vue'),
},
{
path: '/graph',

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
filterHeirarchy,
getFirstLeaf,
getPanelHeirarchy,
} from '@/panels/panel';
import PanelHeirarchy from '@/components/PanelHeirarchy.vue';
import router from '@/router';
import TextInput from '@/components/TextInput.vue';
const searchValue = ref('');
const heirarchy = getPanelHeirarchy();
const filtered_heirarchy = computed(() =>
filterHeirarchy(heirarchy, (leaf) => {
return leaf.name
.toLowerCase()
.includes(searchValue.value.toLowerCase());
}),
);
function onEnter() {
const leaf = getFirstLeaf(filtered_heirarchy.value);
if (leaf != null) {
router.push(leaf.to);
}
}
</script>
<template>
<div class="column stretch screen content center no-min-height">
<div class="row">
<TextInput
autofocus
class="grow"
v-model="searchValue"
placeholder="Search"
@enter="onEnter"
></TextInput>
</div>
<div class="row grow stretch">
<div class="column grow scroll no-min-height">
<PanelHeirarchy
:heirarchy="filtered_heirarchy"
v-if="filtered_heirarchy.length > 0"
></PanelHeirarchy>
<div v-else>
<span>No Matches Found</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
</script>
<template>
<RouterLink :to="{ name: 'graph' }">
Graph
</RouterLink>
<RouterLink :to="{ name: 'list' }">
Tlm List
</RouterLink>
</template>
<style lang="scss">
@use '@/assets/variables';
</style>

View File

@@ -1,25 +1,46 @@
<script setup lang="ts">
import TextInput from '@/components/TextInput.vue';
import { ref } from 'vue';
import TelemetryList from '@/components/TelemetryList.vue';
import type { TelemetryDefinition } from '@/composables/telemetry';
import TelemetryInfo from '@/components/TelemetryInfo.vue';
import FlexDivider from '@/components/FlexDivider.vue';
import { useAllTelemetry } from '@/composables/telemetry';
const searchValue = ref('');
const { data: telemetry_data } = useAllTelemetry();
const selectValue = ref<
[TelemetryDefinition | null, TelemetryDefinition | null]
>([null, null]);
</script>
<template>
<div v-if="telemetry_data">
<div>
<div v-for="entry in telemetry_data" :key="entry.uuid">
<span>
{{ entry.name }}
</span>
<span>
{{ entry.data_type }}
</span>
<div class="row stretch screen content center divider">
<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"
@select="(selection) => (selectValue = selection)"
></TelemetryList>
</div>
</div>
</div>
</div>
<div v-else>
No Telemetry Data
<FlexDivider></FlexDivider>
<div class="column grow stretch no-basis">
<TelemetryInfo
:telemetry_definition="selectValue[0]"
:secondary="selectValue[1]"
></TelemetryInfo>
</div>
</div>
</template>

View File

@@ -79,8 +79,7 @@ async fn get_tlm_history(
}
pub fn setup_api(cfg: &mut web::ServiceConfig) {
cfg
.service(get_all_tlm_definitions)
cfg.service(get_all_tlm_definitions)
.service(get_tlm_definition)
.service(get_tlm_history);
}

View File

@@ -505,7 +505,7 @@ impl TelemetryHistory {
service,
self.data.definition.data_type,
)
.await,
.await,
);
}
}
@@ -543,7 +543,6 @@ impl TelemetryHistory {
let mut disk_result = vec![];
let mut ram_result = vec![];
let mut from = from;
let mut to = to;
let initial_to = to;