adds charts panel
This commit is contained in:
0
frontend/src/assets/icons.scss
Normal file
0
frontend/src/assets/icons.scss
Normal file
@@ -37,11 +37,21 @@
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.content.full {
|
||||
margin: 5vh 5vw;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.screen.content {
|
||||
width: calc(100% - 20vw);
|
||||
height: calc(100vh - 40vh);
|
||||
}
|
||||
|
||||
.screen.content.full {
|
||||
width: calc(100% - 10vw);
|
||||
height: calc(100vh - 10vh);
|
||||
}
|
||||
|
||||
.stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -54,6 +64,10 @@
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-basis {
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ body {
|
||||
align-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -28,10 +29,13 @@ main {
|
||||
}
|
||||
|
||||
* {
|
||||
stroke-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
polyline {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
50
frontend/src/components/InlineIcon.vue
Normal file
50
frontend/src/components/InlineIcon.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
icon: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i :class="`icon ${icon}`"></i>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.icon:before {
|
||||
display: inline-block;
|
||||
content: ' ';
|
||||
background-color: variables.$text-color;
|
||||
mask-size: cover;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.icon.hamburger.menu:before {
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIxIiB4Mj0iMTAiIHkyPSIxIj48L2xpbmU+PGxpbmUgeDE9IjAiIHkxPSI1IiB4Mj0iMTAiIHkyPSI1Ij48L2xpbmU+PGxpbmUgeDE9IjAiIHkxPSI5IiB4Mj0iMTAiIHkyPSI5Ij48L2xpbmU+PC9zdmc+IA==');
|
||||
}
|
||||
|
||||
.icon.close:before {
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iMTAiIHkyPSIxMCI+PC9saW5lPjxsaW5lIHgxPSIxMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMTAiPjwvbGluZT48L3N2Zz4g');
|
||||
}
|
||||
|
||||
.icon.up.arrow:before {
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIxMCIgeDI9IjUiIHkyPSIwIj48L2xpbmU+PGxpbmUgeDE9IjUiIHkxPSIwIiB4Mj0iMTAiIHkyPSIxMCI+PC9saW5lPjwvc3ZnPg==');
|
||||
}
|
||||
|
||||
.icon.down.arrow:before {
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iNSIgeTI9IjEwIj48L2xpbmU+PGxpbmUgeDE9IjUiIHkxPSIxMCIgeDI9IjEwIiB5Mj0iMCI+PC9saW5lPjwvc3ZnPg==');
|
||||
}
|
||||
|
||||
/*
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox="0 0 10 10" stroke="white" strokeWidth="1px">
|
||||
<line x1="0" y1="0" x2="5" y2="10"></line>
|
||||
<line x1="5" y1="10" x2="10" y2="0"></line>
|
||||
</svg>
|
||||
*/
|
||||
</style>
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getDurationString,
|
||||
parseDurationString,
|
||||
} from '@/datetime';
|
||||
import { onDocumentShown } from '@/composables/document.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
initial_duration?: number;
|
||||
@@ -32,7 +33,7 @@ const props = defineProps<{
|
||||
const divRef = useTemplateRef<HTMLDivElement>('graph-div');
|
||||
|
||||
const width = ref(0);
|
||||
const height = ref(0);
|
||||
const raw_height = ref(0);
|
||||
|
||||
const controls_height = 32;
|
||||
const min_time_label_separation = 250;
|
||||
@@ -41,13 +42,18 @@ const resize_observer = new ResizeObserver((elements) => {
|
||||
for (const element of elements) {
|
||||
if (element.target == divRef.value) {
|
||||
width.value = element.contentBoxSize[0].inlineSize;
|
||||
height.value =
|
||||
element.contentBoxSize[0].blockSize -
|
||||
(props.include_controls ? controls_height : 0);
|
||||
raw_height.value = element.contentBoxSize[0].blockSize;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const height = computed(() => {
|
||||
return Math.max(
|
||||
raw_height.value - (props.include_controls ? controls_height : 0),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
watch([divRef], ([divRef]) => {
|
||||
if (divRef) {
|
||||
resize_observer.observe(divRef);
|
||||
@@ -132,6 +138,10 @@ watch([now], ([now_value]) => {
|
||||
}
|
||||
});
|
||||
|
||||
onDocumentShown(() => {
|
||||
fetch_history.value++;
|
||||
});
|
||||
|
||||
const max_x_text = computed({
|
||||
// getter
|
||||
get() {
|
||||
@@ -193,8 +203,12 @@ const legend_x_stride = computed(() => 0);
|
||||
const legend_y_stride = computed(() => 16);
|
||||
const legend_width_output = computed(() => legend_width - 8);
|
||||
|
||||
const graph_width = computed(() => {
|
||||
return Math.max(width.value - border_left.value - border_right.value, 1);
|
||||
});
|
||||
|
||||
const line_duration = computed(() => {
|
||||
const width_px = width.value;
|
||||
const width_px = graph_width.value;
|
||||
const diff_x = max_x.value - min_x.value;
|
||||
return time_lines.find((duration) => {
|
||||
const line_count = diff_x / duration;
|
||||
@@ -263,8 +277,7 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
max_temporal_resolution: max_temporal_resolution,
|
||||
live: live,
|
||||
fetch_history: fetch_history,
|
||||
width: () =>
|
||||
Math.max(width.value - border_left.value - border_right.value, 0),
|
||||
width: graph_width,
|
||||
height: () =>
|
||||
Math.max(height.value - border_top.value - border_bottom.value, 0),
|
||||
x_map: x_map,
|
||||
|
||||
@@ -6,17 +6,35 @@ import Axis from '@/components/GraphAxis.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
telemetry_definition: TelemetryDefinition | null;
|
||||
secondary: TelemetryDefinition | null;
|
||||
selection: TelemetryDefinition | null;
|
||||
mouseover: TelemetryDefinition | null;
|
||||
}>();
|
||||
|
||||
const telemetry_definition = computed(() => {
|
||||
if (props.mouseover) {
|
||||
return props.mouseover;
|
||||
}
|
||||
return props.selection;
|
||||
});
|
||||
|
||||
const secondary = computed(() => {
|
||||
if (props.mouseover) {
|
||||
if (props.selection) {
|
||||
if (props.selection.uuid != props.mouseover.uuid) {
|
||||
return props.selection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const lines = computed(() => {
|
||||
const result = [];
|
||||
if (props.secondary) {
|
||||
result.push(props.secondary);
|
||||
if (secondary.value) {
|
||||
result.push(secondary.value);
|
||||
}
|
||||
if (props.telemetry_definition) {
|
||||
result.push(props.telemetry_definition);
|
||||
if (telemetry_definition.value) {
|
||||
result.push(telemetry_definition.value);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -389,7 +389,7 @@ function onMouseExit(event: MouseEvent) {
|
||||
</g>
|
||||
<ValueLabel
|
||||
v-if="current_data_point"
|
||||
class="fade_other_selected"
|
||||
class="fade_other_selected label"
|
||||
:x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset"
|
||||
:y="axis_data.y_map(current_data_point.y)"
|
||||
:value="current_data_point.y"
|
||||
@@ -473,6 +473,10 @@ function onMouseExit(event: MouseEvent) {
|
||||
opacity: 25%;
|
||||
}
|
||||
|
||||
.fade .fade_other_selected.label {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade .no-fade .fade_other_selected {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ const props = defineProps<{
|
||||
search?: string;
|
||||
}>();
|
||||
|
||||
const selected = defineModel<TelemetryDefinition | null>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'select',
|
||||
tlm_entry: [TelemetryDefinition | null, TelemetryDefinition | null],
|
||||
): void;
|
||||
(e: 'mouseover', tlm_entry: TelemetryDefinition | null): void;
|
||||
}>();
|
||||
|
||||
const search_value = computed(() => (props.search || '').toLowerCase());
|
||||
@@ -33,7 +32,6 @@ const sorted_tlm_data = computed(() => {
|
||||
});
|
||||
|
||||
const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
const selected = ref<TelemetryDefinition | null>(null);
|
||||
|
||||
function onMouseover(tlm_entry: TelemetryDefinition) {
|
||||
mousedover.value = tlm_entry;
|
||||
@@ -47,17 +45,8 @@ 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]);
|
||||
}
|
||||
watch([mousedover], ([mousedover_val]) => {
|
||||
emit('mouseover', mousedover_val);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
24
frontend/src/composables/document.ts
Normal file
24
frontend/src/composables/document.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
export function onDocumentVisibilityChange(
|
||||
handler: (visible: boolean) => void,
|
||||
) {
|
||||
const handlerRef = ref(() => {
|
||||
handler(!document.hidden);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', handlerRef.value);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handlerRef.value);
|
||||
});
|
||||
}
|
||||
|
||||
export function onDocumentShown(handler: () => void) {
|
||||
onDocumentVisibilityChange((visible) => {
|
||||
if (visible) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
watch,
|
||||
} from 'vue';
|
||||
import type { TelemetryDefinition } from '@/composables/telemetry';
|
||||
import { onDocumentVisibilityChange } from '@/composables/document.ts';
|
||||
|
||||
export interface TelemetryDataItem {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -27,12 +28,14 @@ export class WebsocketHandle {
|
||||
websocket: WebSocket | null;
|
||||
should_be_connected: boolean;
|
||||
connected: Ref<boolean>;
|
||||
enabled: Ref<boolean>;
|
||||
on_telem_value: Map<string, Array<(value: TelemetryDataItem) => void>>;
|
||||
|
||||
constructor() {
|
||||
this.websocket = null;
|
||||
this.should_be_connected = false;
|
||||
this.connected = ref(false);
|
||||
this.enabled = ref(true);
|
||||
this.on_telem_value = new Map();
|
||||
}
|
||||
|
||||
@@ -119,9 +122,9 @@ export class WebsocketHandle {
|
||||
const is_live = computed(() => toValue(live));
|
||||
|
||||
watch(
|
||||
[uuid, this.connected, minimum_separation, is_live],
|
||||
([uuid_value, connected, min_sep, live_value]) => {
|
||||
if (connected && uuid_value && live_value) {
|
||||
[uuid, this.connected, this.enabled, minimum_separation, is_live],
|
||||
([uuid_value, connected, enabled, min_sep, live_value]) => {
|
||||
if (connected && enabled && uuid_value && live_value) {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
RegisterTlmListener: {
|
||||
@@ -160,6 +163,14 @@ export class WebsocketHandle {
|
||||
|
||||
return value_result;
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.enabled.value = false;
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.enabled.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const WEBSOCKET_SYMBOL = Symbol();
|
||||
@@ -170,6 +181,13 @@ export function useWebsocket() {
|
||||
onMounted(() => {
|
||||
handle.value.connect();
|
||||
});
|
||||
onDocumentVisibilityChange((visible) => {
|
||||
if (visible) {
|
||||
handle.value.resume();
|
||||
} else {
|
||||
handle.value.pause();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
handle.value.disconnect();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
export enum PanelHeirarchyType {
|
||||
LEAF,
|
||||
FOLDER,
|
||||
LEAF = 'leaf',
|
||||
FOLDER = 'folder',
|
||||
}
|
||||
|
||||
export type PanelHeirarchyLeaf = {
|
||||
@@ -37,6 +37,12 @@ export function getPanelHeirarchy(): PanelHeirarchyChildren {
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: 'Chart',
|
||||
to: { name: 'chart' },
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ const router = createRouter({
|
||||
name: 'list',
|
||||
component: () => import('../views/TelemetryListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chart',
|
||||
name: 'chart',
|
||||
component: () => import('../views/ChartView.vue'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
309
frontend/src/views/ChartDefinitionView.vue
Normal file
309
frontend/src/views/ChartDefinitionView.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<script setup lang="ts">
|
||||
import FlexDivider from '@/components/FlexDivider.vue';
|
||||
import TelemetryList from '@/components/TelemetryList.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import TextInput from '@/components/TextInput.vue';
|
||||
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||
import InlineIcon from '@/components/InlineIcon.vue';
|
||||
|
||||
const model = defineModel<TelemetryDefinition[][][]>({
|
||||
required: true,
|
||||
default: [],
|
||||
});
|
||||
|
||||
const selected = ref(0);
|
||||
const selected_cell_x = ref(0);
|
||||
const selected_cell_y = ref(0);
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
const options = [
|
||||
[1, 1],
|
||||
[2, 1],
|
||||
[1, 2],
|
||||
[2, 2],
|
||||
[3, 2],
|
||||
];
|
||||
|
||||
function selectOption(i: number) {
|
||||
selected.value = i;
|
||||
if (selected_cell_x.value >= options[i][0]) {
|
||||
selected_cell_x.value = 0;
|
||||
}
|
||||
if (selected_cell_y.value >= options[i][1]) {
|
||||
selected_cell_y.value = 0;
|
||||
}
|
||||
const initial_cells = model.value;
|
||||
const result_cells: TelemetryDefinition[][][] = [];
|
||||
for (let x = 0; x < options[i][0]; x++) {
|
||||
result_cells.push([]);
|
||||
for (let y = 0; y < options[i][1]; y++) {
|
||||
if (x < initial_cells.length && y < initial_cells[x].length) {
|
||||
result_cells[x].push(initial_cells[x][y]);
|
||||
} else {
|
||||
result_cells[x].push([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
model.value = result_cells;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const model_value = model.value;
|
||||
let s = 0;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
// X length correct
|
||||
if (options[i][0] == model_value.length) {
|
||||
// Y length correct
|
||||
if (options[i][1] == model_value[0].length) {
|
||||
// selectOption(i);
|
||||
s = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to option 0
|
||||
selectOption(s);
|
||||
});
|
||||
|
||||
function selectCell(x: number, y: number) {
|
||||
selected_cell_x.value = x;
|
||||
selected_cell_y.value = y;
|
||||
}
|
||||
|
||||
function selectTelemetry(telemetry: TelemetryDefinition | null) {
|
||||
if (telemetry != null) {
|
||||
if (
|
||||
!model.value[selected_cell_x.value][selected_cell_y.value].includes(
|
||||
telemetry,
|
||||
)
|
||||
) {
|
||||
model.value[selected_cell_x.value][selected_cell_y.value].push(
|
||||
telemetry,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveUp(index: number) {
|
||||
if (index > 0) {
|
||||
const removed = model.value[selected_cell_x.value][
|
||||
selected_cell_y.value
|
||||
].splice(index - 1, 2);
|
||||
removed.reverse();
|
||||
model.value[selected_cell_x.value][selected_cell_y.value].splice(
|
||||
index - 1,
|
||||
0,
|
||||
...removed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function moveDown(index: number) {
|
||||
if (
|
||||
index + 1 <
|
||||
model.value[selected_cell_x.value][selected_cell_y.value].length
|
||||
) {
|
||||
const removed = model.value[selected_cell_x.value][
|
||||
selected_cell_y.value
|
||||
].splice(index, 2);
|
||||
removed.reverse();
|
||||
model.value[selected_cell_x.value][selected_cell_y.value].splice(
|
||||
index,
|
||||
0,
|
||||
...removed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removeTelemetry(index: number) {
|
||||
model.value[selected_cell_x.value][selected_cell_y.value].splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row grow stretch">
|
||||
<div class="column grow stretch no-basis">
|
||||
<div class="grow no-basis type_grid no-min-height scroll">
|
||||
<svg
|
||||
v-for="(option, i) in options"
|
||||
:key="i"
|
||||
:class="`layout ${selected == i ? 'selected' : ''}`"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
@click="selectOption(i)"
|
||||
>
|
||||
<template v-for="x in option[0]" :key="x">
|
||||
<template v-for="y in option[1]" :key="y">
|
||||
<rect
|
||||
:x="1 + ((x - 1) * 98) / option[0]"
|
||||
:y="1 + ((y - 1) * 98) / option[1]"
|
||||
:width="98 / option[0]"
|
||||
:height="98 / option[1]"
|
||||
></rect>
|
||||
</template>
|
||||
</template>
|
||||
</svg>
|
||||
</div>
|
||||
<FlexDivider class="horizontal_divider_margin"></FlexDivider>
|
||||
<div class="row grow center no-basis">
|
||||
<div
|
||||
class="selected_grid grow"
|
||||
:style="`grid-template: repeat(${options[selected][1]}, 1fr) / repeat(${options[selected][0]}, 1fr);`"
|
||||
>
|
||||
<template v-for="y in options[selected][1]" :key="y">
|
||||
<template v-for="x in options[selected][0]" :key="x">
|
||||
<div
|
||||
:class="`cell ${selected_cell_x == x - 1 && selected_cell_y == y - 1 ? 'selected' : ''}`"
|
||||
@click="selectCell(x - 1, y - 1)"
|
||||
></div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow2 stretch no-basis">
|
||||
<div class="row grow stretch no-basis">
|
||||
<div class="column grow stretch">
|
||||
<template v-if="model.length > 0">
|
||||
<div
|
||||
class="row chosen"
|
||||
v-for="(selected, i) in model[selected_cell_x][
|
||||
selected_cell_y
|
||||
]"
|
||||
:key="i"
|
||||
>
|
||||
<span>
|
||||
{{ selected.name }}
|
||||
</span>
|
||||
<span class="grow"></span>
|
||||
<div class="column tiny_text">
|
||||
<InlineIcon
|
||||
icon="up arrow"
|
||||
:class="`${i == 0 ? 'hidden' : 'button'}`"
|
||||
@click="moveUp(i)"
|
||||
></InlineIcon>
|
||||
<InlineIcon
|
||||
icon="down arrow"
|
||||
:class="`${i == model[selected_cell_x][selected_cell_y].length - 1 ? 'hidden' : 'button'}`"
|
||||
@click="moveDown(i)"
|
||||
></InlineIcon>
|
||||
</div>
|
||||
<span
|
||||
class="close icon button"
|
||||
@click="removeTelemetry(i)"
|
||||
>
|
||||
<InlineIcon icon="close"></InlineIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<FlexDivider class="horizontal_divider_margin"></FlexDivider>
|
||||
<div class="row grow stretch no-basis">
|
||||
<div class="column grow stretch no-basis">
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
class="grow"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
></TextInput>
|
||||
</div>
|
||||
<div class="row scroll grow no-min-height no-basis">
|
||||
<div class="column grow stretch">
|
||||
<TelemetryList
|
||||
:search="searchValue"
|
||||
:model-value="null"
|
||||
@update:model-value="
|
||||
(value) => selectTelemetry(value)
|
||||
"
|
||||
></TelemetryList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.type_grid {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
grid-auto-rows: max-content;
|
||||
gap: 1em;
|
||||
justify-items: stretch;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.type_grid > * {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.layout {
|
||||
stroke: variables.$grid-line;
|
||||
stroke-width: 1px;
|
||||
fill: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: variables.$light-background-color;
|
||||
}
|
||||
|
||||
.selected_grid {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
justify-items: stretch;
|
||||
align-self: stretch;
|
||||
justify-content: stretch;
|
||||
align-content: stretch;
|
||||
}
|
||||
|
||||
.cell {
|
||||
border: variables.$grid-line 1px solid;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.horizontal_divider_margin {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
padding: 0.3em;
|
||||
border: 0;
|
||||
border-bottom: variables.$gray-3 solid 1px;
|
||||
border-top: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chosen:first-child {
|
||||
border-top: variables.$gray-3 solid 1px;
|
||||
}
|
||||
|
||||
.chosen:hover {
|
||||
background-color: variables.$light2-background-color;
|
||||
}
|
||||
|
||||
.close.icon.button {
|
||||
height: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow.icon.button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.column.tiny_text {
|
||||
font-size: variables.$normal-text-size / 2;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
51
frontend/src/views/ChartRenderView.vue
Normal file
51
frontend/src/views/ChartRenderView.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||
import SvgGraph from '@/components/SvgGraph.vue';
|
||||
import { GraphSide } from '@/graph/graph.ts';
|
||||
import GraphAxis from '@/components/GraphAxis.vue';
|
||||
import TelemetryLine from '@/components/TelemetryLine.vue';
|
||||
|
||||
defineProps<{
|
||||
charts: TelemetryDefinition[][][];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="chart grid grow"
|
||||
:style="`grid-template: repeat(${charts[0].length} , 1fr) / repeat(${charts.length}, 1fr);`"
|
||||
v-if="charts.length > 0"
|
||||
>
|
||||
<template v-for="y in charts[0].length" :key="y">
|
||||
<template v-for="x in charts.length" :key="x">
|
||||
<div class="no-min-height">
|
||||
<SvgGraph
|
||||
right_axis
|
||||
cursor
|
||||
:legend="GraphSide.Left"
|
||||
include_controls
|
||||
>
|
||||
<GraphAxis>
|
||||
<TelemetryLine
|
||||
v-for="tlm in charts[x - 1][y - 1]"
|
||||
:key="tlm.uuid"
|
||||
:data="tlm.name"
|
||||
></TelemetryLine>
|
||||
</GraphAxis>
|
||||
</SvgGraph>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.chart.grid {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
place-content: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
50
frontend/src/views/ChartView.vue
Normal file
50
frontend/src/views/ChartView.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import InlineIcon from '@/components/InlineIcon.vue';
|
||||
import ChartDefinitionView from '@/views/ChartDefinitionView.vue';
|
||||
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||
import ChartRenderView from '@/views/ChartRenderView.vue';
|
||||
|
||||
const settingsOpen = ref(true);
|
||||
const settings = ref<TelemetryDefinition[][][]>([]);
|
||||
|
||||
function openSettings() {
|
||||
settingsOpen.value = true;
|
||||
}
|
||||
function closeSettings() {
|
||||
settingsOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column stretch screen content full center divider">
|
||||
<template v-if="!settingsOpen">
|
||||
<div class="settings column center" @click="openSettings">
|
||||
<InlineIcon icon="hamburger menu"></InlineIcon>
|
||||
</div>
|
||||
<ChartRenderView :charts="settings"></ChartRenderView>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="settings column center" @click="closeSettings">
|
||||
<InlineIcon icon="close"></InlineIcon>
|
||||
</div>
|
||||
<ChartDefinitionView v-model="settings"></ChartDefinitionView>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.settings {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: variables.$dark-background-color;
|
||||
border: 1px solid variables.$light2-background-color;
|
||||
padding: 0.5em;
|
||||
margin: 1px;
|
||||
aspect-ratio: 1 / 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -8,9 +8,8 @@ import FlexDivider from '@/components/FlexDivider.vue';
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
const selectValue = ref<
|
||||
[TelemetryDefinition | null, TelemetryDefinition | null]
|
||||
>([null, null]);
|
||||
const selected = ref<TelemetryDefinition | null>(null);
|
||||
const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,7 +28,11 @@ const selectValue = ref<
|
||||
<div class="column grow stretch">
|
||||
<TelemetryList
|
||||
:search="searchValue"
|
||||
@select="(selection) => (selectValue = selection)"
|
||||
v-model="selected"
|
||||
@mouseover="
|
||||
(mousedover_value) =>
|
||||
(mousedover = mousedover_value)
|
||||
"
|
||||
></TelemetryList>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,8 +40,8 @@ const selectValue = ref<
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow stretch no-basis">
|
||||
<TelemetryInfo
|
||||
:telemetry_definition="selectValue[0]"
|
||||
:secondary="selectValue[1]"
|
||||
:mouseover="mousedover"
|
||||
:selection="selected"
|
||||
></TelemetryInfo>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user