increase frontend flexibility
This commit is contained in:
@@ -2,23 +2,47 @@
|
||||
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;
|
||||
.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.gap-half {
|
||||
gap: 0.0625em;
|
||||
}
|
||||
.gap-full {
|
||||
gap: 0.125em;
|
||||
}
|
||||
.gap-wide {
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.center {
|
||||
@@ -27,15 +51,6 @@
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.screen {
|
||||
min-height: 100vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.screen.limited {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20vh 10vw;
|
||||
}
|
||||
@@ -54,26 +69,6 @@
|
||||
padding-bottom: 5vw;
|
||||
}
|
||||
|
||||
//.content {
|
||||
// margin: 20vh 10vw;
|
||||
// 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;
|
||||
}
|
||||
@@ -105,3 +100,13 @@
|
||||
.scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 1fr;
|
||||
row-gap: 0.125em;
|
||||
column-gap: 0.5em;
|
||||
justify-items: stretch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
113
frontend/src/components/CopyableDynamicSpan.vue
Normal file
113
frontend/src/components/CopyableDynamicSpan.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const overlay = computed(() => {
|
||||
return '0'.repeat(props.value.length);
|
||||
});
|
||||
|
||||
const span = useTemplateRef<HTMLSpanElement>('data-span');
|
||||
|
||||
function copyHandler(event: ClipboardEvent) {
|
||||
const selection = document.getSelection();
|
||||
const span_value: HTMLSpanElement | null = span.value;
|
||||
if (selection && span_value && selection.containsNode(span_value, true)) {
|
||||
let copy_result = '';
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
const node_iter = document.createNodeIterator(
|
||||
selection.getRangeAt(i).commonAncestorContainer,
|
||||
NodeFilter.SHOW_ALL,
|
||||
);
|
||||
let found_start = false;
|
||||
while (true) {
|
||||
const node = node_iter.nextNode();
|
||||
if (node) {
|
||||
if (node == selection?.getRangeAt(i).startContainer) {
|
||||
found_start = true;
|
||||
}
|
||||
if (found_start && node.nodeType == Node.TEXT_NODE) {
|
||||
let append_to_copy = node.textContent?.trim() || '';
|
||||
const parent = node.parentElement;
|
||||
if (parent) {
|
||||
const copy_value =
|
||||
parent.getAttribute('copy-value');
|
||||
if (copy_value) {
|
||||
append_to_copy = copy_value;
|
||||
}
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).endContainer) {
|
||||
append_to_copy = append_to_copy.substring(
|
||||
0,
|
||||
selection?.getRangeAt(i).endOffset,
|
||||
);
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).startContainer) {
|
||||
append_to_copy = append_to_copy.substring(
|
||||
selection?.getRangeAt(i).startOffset,
|
||||
);
|
||||
}
|
||||
if (copy_result.length > 0) {
|
||||
copy_result += ' ';
|
||||
}
|
||||
copy_result += append_to_copy;
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).endContainer) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
event.clipboardData?.setData('text/plain', copy_result);
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('copy', copyHandler);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('copy', copyHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="test monospace" :data-after-content="value">
|
||||
<span ref="data-span" class="transparent" :copy-value="value">
|
||||
{{ overlay }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.monospace {
|
||||
font-family: variables.$monospace-text-font;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: transparent;
|
||||
border: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.test {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test::after {
|
||||
content: attr(data-after-content);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import CopyableDynamicSpan from '@/components/CopyableDynamicSpan.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: number;
|
||||
max_width: number;
|
||||
copyable?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update', value: string): void;
|
||||
}>();
|
||||
|
||||
const copyable = computed(() => {
|
||||
return props.copyable || false;
|
||||
});
|
||||
|
||||
const display_value = computed(() => {
|
||||
if (props.value == 0) {
|
||||
return '0';
|
||||
@@ -61,7 +67,12 @@ watch([display_value], ([display_str]) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="copyable">
|
||||
<CopyableDynamicSpan :value="display_value"></CopyableDynamicSpan>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ display_value }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
66
frontend/src/components/TelemetryValue.vue
Normal file
66
frontend/src/components/TelemetryValue.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useTelemetry } from '@/composables/telemetry.ts';
|
||||
import { computed, inject, type ShallowRef } from 'vue';
|
||||
import {
|
||||
WEBSOCKET_SYMBOL,
|
||||
type WebsocketHandle,
|
||||
} from '@/composables/websocket.ts';
|
||||
import NumericText from '@/components/NumericText.vue';
|
||||
|
||||
const max_update_rate = 50; // ms
|
||||
const default_update_rate = 200; // ms
|
||||
|
||||
const props = defineProps<{
|
||||
data: string;
|
||||
max_update_period?: number;
|
||||
}>();
|
||||
|
||||
const max_update_period = computed(() => {
|
||||
return Math.min(
|
||||
props.max_update_period || default_update_rate,
|
||||
max_update_rate,
|
||||
);
|
||||
});
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(
|
||||
telemetry_data,
|
||||
max_update_period,
|
||||
true,
|
||||
);
|
||||
|
||||
const is_data_present = computed(() => {
|
||||
return value.value != null;
|
||||
});
|
||||
|
||||
const numeric_data = computed(() => {
|
||||
const val = value.value;
|
||||
if (val) {
|
||||
const type = telemetry_data.value!.data_type;
|
||||
const item_val = val.value[type];
|
||||
if (typeof item_val == 'number') {
|
||||
return item_val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="!is_data_present"> No Data </span>
|
||||
<template v-else>
|
||||
<NumericText
|
||||
v-if="numeric_data"
|
||||
:value="numeric_data"
|
||||
:max_width="10"
|
||||
></NumericText>
|
||||
<span v-else>
|
||||
Cannot Display Data of Type {{ telemetry_data!.data_type }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
19
frontend/src/components/layout/GridLayout.vue
Normal file
19
frontend/src/components/layout/GridLayout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
cols: number;
|
||||
equal_col_width?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`grid`"
|
||||
:style="`grid-template-columns: repeat(${cols}, ${equal_col_width ? '1fr' : 'auto'});`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
33
frontend/src/components/layout/LinearLayout.vue
Normal file
33
frontend/src/components/layout/LinearLayout.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { Direction } from '@/composables/Direction.ts';
|
||||
import { Alignment } from '@/composables/Alignment.ts';
|
||||
import { Justification } from '@/composables/Justification.ts';
|
||||
import { computed } from 'vue';
|
||||
import { GapSize } from '@/composables/GapSize.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
direction: Direction;
|
||||
stretch?: boolean;
|
||||
// Direction
|
||||
justify?: Justification;
|
||||
// Cross Direction
|
||||
align?: Alignment;
|
||||
gap?: GapSize;
|
||||
}>();
|
||||
|
||||
const justification = computed(() => props.justify || Justification.Start);
|
||||
const alignment = computed(() => props.align || Alignment.Start);
|
||||
const gap_size = computed(() => props.gap || GapSize.Normal);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`${direction} linear ${stretch ? 'stretch' : ''} ${justification} ${alignment} ${gap_size}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
30
frontend/src/components/layout/ScreenLayout.vue
Normal file
30
frontend/src/components/layout/ScreenLayout.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
defineProps<{
|
||||
// Whether this should be limited to the height of the viewport
|
||||
// This allows scroll bars to be pushed to inner components
|
||||
limit?: boolean;
|
||||
type: ScreenType;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`column stretch screen ${limit ? 'limited' : ''} content ${type}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.screen {
|
||||
min-height: 100vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.screen.limited {
|
||||
max-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
5
frontend/src/composables/Alignment.ts
Normal file
5
frontend/src/composables/Alignment.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum Alignment {
|
||||
Start = 'align-start',
|
||||
Center = 'align-center',
|
||||
End = 'align-end',
|
||||
}
|
||||
4
frontend/src/composables/Direction.ts
Normal file
4
frontend/src/composables/Direction.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum Direction {
|
||||
Row = 'row',
|
||||
Column = 'column',
|
||||
}
|
||||
6
frontend/src/composables/GapSize.ts
Normal file
6
frontend/src/composables/GapSize.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum GapSize {
|
||||
None = '',
|
||||
Thin = 'gap-half',
|
||||
Normal = 'gap-full',
|
||||
Wide = 'gap-wide',
|
||||
}
|
||||
6
frontend/src/composables/Justification.ts
Normal file
6
frontend/src/composables/Justification.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum Justification {
|
||||
Start = 'justify-start',
|
||||
Center = 'justify-center',
|
||||
Between = 'justify-between',
|
||||
End = 'justify-end',
|
||||
}
|
||||
7
frontend/src/composables/ScreenType.ts
Normal file
7
frontend/src/composables/ScreenType.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ScreenType {
|
||||
Standard = '',
|
||||
Page = 'page',
|
||||
Wide = 'wide',
|
||||
Tall = 'tall',
|
||||
WideTall = 'wide tall',
|
||||
}
|
||||
@@ -43,6 +43,12 @@ export function getPanelHeirarchy(): PanelHeirarchyChildren {
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: 'Panel Test',
|
||||
to: { name: 'panel_test' },
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ const router = createRouter({
|
||||
name: 'chart',
|
||||
component: () => import('../views/ChartView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/panel_test',
|
||||
name: 'panel_test',
|
||||
component: () => import('../views/PanelTest.vue'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ 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';
|
||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
const settingsOpen = ref(true);
|
||||
const settings = ref<TelemetryDefinition[][][]>([]);
|
||||
@@ -17,7 +20,7 @@ function closeSettings() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column stretch screen content tall wide">
|
||||
<ScreenLayout :direction="Direction.Column" :type="ScreenType.WideTall">
|
||||
<template v-if="!settingsOpen">
|
||||
<div class="settings column" @click="openSettings">
|
||||
<InlineIcon icon="hamburger menu"></InlineIcon>
|
||||
@@ -30,7 +33,7 @@ function closeSettings() {
|
||||
</div>
|
||||
<ChartDefinitionView v-model="settings"></ChartDefinitionView>
|
||||
</template>
|
||||
</div>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
import PanelHeirarchy from '@/components/PanelHeirarchy.vue';
|
||||
import router from '@/router';
|
||||
import TextInput from '@/components/TextInput.vue';
|
||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
@@ -30,7 +33,7 @@ function onEnter() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column stretch screen limited content page">
|
||||
<ScreenLayout :direction="Direction.Column" :type="ScreenType.Page" limit>
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
@@ -52,7 +55,7 @@ function onEnter() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
28
frontend/src/views/PanelTest.vue
Normal file
28
frontend/src/views/PanelTest.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import GridLayout from '@/components/layout/GridLayout.vue';
|
||||
import TelemetryValue from '@/components/TelemetryValue.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GridLayout :cols="2" equal_col_width>
|
||||
<span class="justify-right"> simple_producer/cos </span>
|
||||
<TelemetryValue data="simple_producer/cos"></TelemetryValue>
|
||||
|
||||
<span class="justify-right"> simple_producer/sin </span>
|
||||
<TelemetryValue data="simple_producer/sin"></TelemetryValue>
|
||||
|
||||
<span class="justify-right"> simple_producer/cos2 </span>
|
||||
<TelemetryValue data="simple_producer/cos2"></TelemetryValue>
|
||||
|
||||
<span class="justify-right"> simple_producer/sin2 </span>
|
||||
<TelemetryValue data="simple_producer/sin2"></TelemetryValue>
|
||||
</GridLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.justify-right {
|
||||
justify-self: end;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,9 @@ 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 ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
@@ -13,7 +16,7 @@ const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row stretch screen limited content">
|
||||
<ScreenLayout :direction="Direction.Row" :type="ScreenType.Standard" limit>
|
||||
<div class="column grow2 stretch no-min-height no-basis">
|
||||
<div class="row">
|
||||
<TextInput
|
||||
@@ -44,7 +47,7 @@ const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
:selection="selected"
|
||||
></TelemetryInfo>
|
||||
</div>
|
||||
</div>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user