allows scrolling backwards through history
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, inject, provide, ref, toValue, watch } from 'vue';
|
||||
import { AXIS_DATA, type AxisData, AxisType } from '@/graph/axis';
|
||||
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
||||
import NumericText from '@/components/NumericText.vue';
|
||||
import { useNow } from '@/composables/ticker';
|
||||
|
||||
const props = defineProps<{
|
||||
y_limits?: [number, number];
|
||||
@@ -28,7 +29,9 @@ const raw_max_y = ref(-Infinity);
|
||||
|
||||
const axis_update_watch = ref(0);
|
||||
|
||||
watch([graph_data.min_x, graph_data.max_x], () => {
|
||||
const axis_update_ticker = useNow(50);
|
||||
|
||||
watch([graph_data.min_x, graph_data.max_x, axis_update_ticker], () => {
|
||||
axis_update_watch.value++;
|
||||
min_y.value = raw_min_y.value;
|
||||
max_y.value = raw_max_y.value;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useNow } from '@/composables/ticker';
|
||||
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
||||
import TimeText from '@/components/TimeText.vue';
|
||||
import { getDateString } from '@/datetime';
|
||||
|
||||
const props = defineProps<{
|
||||
duration?: number;
|
||||
@@ -107,11 +108,47 @@ const border_right = computed(
|
||||
const border_top = computed(() => 6);
|
||||
const border_bottom = computed(() => (props.hide_time_labels ? 6 : 24));
|
||||
|
||||
const max_x = now;
|
||||
const max_x = ref(now.value);
|
||||
const min_x = computed(() => max_x.value - window_duration.value);
|
||||
|
||||
const fetch_history = ref(0);
|
||||
|
||||
const diff_x = computed(() => max_x.value - min_x.value);
|
||||
|
||||
const live = ref(true);
|
||||
watch([live], ([live_value]) => {
|
||||
if (live_value) {
|
||||
fetch_history.value++;
|
||||
}
|
||||
});
|
||||
const valid_max_x_text = ref(true);
|
||||
watch([now], ([now_value]) => {
|
||||
if (live.value) {
|
||||
max_x.value = now_value;
|
||||
valid_max_x_text.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const max_x_text = computed({
|
||||
// getter
|
||||
get() {
|
||||
return getDateString(new Date(max_x.value), props.utc, true);
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
const new_max_x = Date.parse(
|
||||
newValue.replace('/', '-').replace('/', '-').replace(' ', 'T'),
|
||||
);
|
||||
if (!Number.isNaN(new_max_x)) {
|
||||
fetch_history.value++;
|
||||
max_x.value = new_max_x;
|
||||
valid_max_x_text.value = true;
|
||||
} else {
|
||||
valid_max_x_text.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const x_map = (x: number) => {
|
||||
return (
|
||||
((width.value - border_left.value - border_right.value) *
|
||||
@@ -203,7 +240,9 @@ const should_fade = ref(false);
|
||||
provide<GraphData>(GRAPH_DATA, {
|
||||
border_top: border_top,
|
||||
min_x: min_x,
|
||||
max_x: now,
|
||||
max_x: max_x,
|
||||
live: live,
|
||||
fetch_history: fetch_history,
|
||||
width: () =>
|
||||
Math.max(width.value - border_left.value - border_right.value, 0),
|
||||
height: () =>
|
||||
@@ -231,7 +270,17 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<span>Duration Dropdown</span>
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="live"
|
||||
:size="max_x_text.length"
|
||||
v-model="max_x_text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="live" />
|
||||
<label>Live</label>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
|
||||
@@ -28,7 +28,6 @@ const props = defineProps<{
|
||||
data: string;
|
||||
minimum_separation?: number;
|
||||
class?: string;
|
||||
fetch_history?: number;
|
||||
}>();
|
||||
|
||||
const smoothing_distance_x = 5;
|
||||
@@ -42,13 +41,17 @@ const min_sep = computed(() =>
|
||||
Math.min(props.minimum_separation || 0, maximum_minimum_separation_live),
|
||||
);
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(telemetry_data, min_sep);
|
||||
|
||||
const graph_data = inject<GraphData>(GRAPH_DATA)!;
|
||||
const axis_data = inject<AxisData>(AXIS_DATA)!;
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(
|
||||
telemetry_data,
|
||||
min_sep,
|
||||
graph_data.live,
|
||||
);
|
||||
|
||||
const min = ref(Infinity);
|
||||
const max = ref(-Infinity);
|
||||
|
||||
@@ -104,8 +107,9 @@ watch([value], ([val]) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
const recompute_bounds = ref(0);
|
||||
watch(
|
||||
[telemetry_data, () => props.fetch_history],
|
||||
[telemetry_data, () => toValue(graph_data.fetch_history)],
|
||||
async ([data]) => {
|
||||
if (data) {
|
||||
const uuid = data.uuid;
|
||||
@@ -137,9 +141,10 @@ watch(
|
||||
);
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
recompute_bounds.value++;
|
||||
} catch (e) {
|
||||
// TODO: Response?
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -170,19 +175,22 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
|
||||
}
|
||||
}
|
||||
if (memo_changed) {
|
||||
let min_val = Infinity;
|
||||
let max_val = -Infinity;
|
||||
for (let i = 1; i < memo.value.data.length; i++) {
|
||||
const item_val = memo.value.data[i].y;
|
||||
min_val = Math.min(min_val, item_val);
|
||||
max_val = Math.max(max_val, item_val);
|
||||
}
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
max.value = max_val;
|
||||
min.value = min_val;
|
||||
recompute_bounds.value++;
|
||||
}
|
||||
});
|
||||
watch([recompute_bounds], () => {
|
||||
let min_val = Infinity;
|
||||
let max_val = -Infinity;
|
||||
for (let i = 1; i < memo.value.data.length; i++) {
|
||||
const item_val = memo.value.data[i].y;
|
||||
min_val = Math.min(min_val, item_val);
|
||||
max_val = Math.max(max_val, item_val);
|
||||
}
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
max.value = max_val;
|
||||
min.value = min_val;
|
||||
});
|
||||
|
||||
watch(
|
||||
[min, axis_data.axis_update_watch],
|
||||
@@ -354,7 +362,7 @@ function onMouseExit(event: MouseEvent) {
|
||||
:cx="marker_radius"
|
||||
:cy="marker_radius"
|
||||
:r="marker_radius"
|
||||
:class="`indexed-color color-${index}`"
|
||||
:class="`indexed-color color-${index} marker`"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
@@ -446,6 +454,10 @@ function onMouseExit(event: MouseEvent) {
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.fade rect.fade_other_selected {
|
||||
opacity: 10%;
|
||||
}
|
||||
|
||||
.fade .fade_other_selected {
|
||||
opacity: 25%;
|
||||
}
|
||||
@@ -463,6 +475,11 @@ function onMouseExit(event: MouseEvent) {
|
||||
fill: var(--indexed-color);
|
||||
}
|
||||
|
||||
circle.marker {
|
||||
stroke: variables.$background-color;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
polyline {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { getDateString } from '@/datetime';
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
@@ -9,57 +10,12 @@ const props = defineProps<{
|
||||
show_millis?: boolean;
|
||||
}>();
|
||||
|
||||
// This function is slow
|
||||
function getDateString(date: Date) {
|
||||
const year = props.utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
const month = (
|
||||
(props.utc ? date.getMonth() : date.getMonth()) + 1
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const day = (props.utc ? date.getUTCDate() : date.getDate()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const hour = (
|
||||
props.utc ? date.getUTCHours() : date.getHours()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const minute = (
|
||||
props.utc ? date.getUTCMinutes() : date.getMinutes()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const second = (
|
||||
props.utc ? date.getUTCSeconds() : date.getSeconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const milliseconds = (
|
||||
props.utc ? date.getUTCMilliseconds() : date.getMilliseconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 3,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
return `${year}/${month}/${day} ${hour}:${minute}:${second}${props.show_millis ? `.${milliseconds}` : ''}${props.utc ? 'Z' : ''}`;
|
||||
}
|
||||
|
||||
const timetext = computed(() => {
|
||||
return getDateString(new Date(props.timestamp));
|
||||
return getDateString(
|
||||
new Date(props.timestamp),
|
||||
props.utc,
|
||||
props.show_millis,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type MaybeRefOrGetter,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onWatcherCleanup,
|
||||
ref,
|
||||
type Ref,
|
||||
shallowRef,
|
||||
@@ -95,6 +96,7 @@ export class WebsocketHandle {
|
||||
listen_to_telemetry(
|
||||
telemetry: MaybeRefOrGetter<TelemetryDefinition | null>,
|
||||
minimum_separation_ms: MaybeRefOrGetter<number> | undefined,
|
||||
live: MaybeRefOrGetter<boolean>,
|
||||
) {
|
||||
const value_result = ref<TelemetryDataItem | null>(null);
|
||||
|
||||
@@ -114,10 +116,12 @@ export class WebsocketHandle {
|
||||
return 0;
|
||||
});
|
||||
|
||||
const is_live = computed(() => toValue(live));
|
||||
|
||||
watch(
|
||||
[uuid, this.connected, minimum_separation],
|
||||
([uuid_value, connected, min_sep]) => {
|
||||
if (connected && uuid_value) {
|
||||
[uuid, this.connected, minimum_separation, is_live],
|
||||
([uuid_value, connected, min_sep, live_value]) => {
|
||||
if (connected && uuid_value && live_value) {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
RegisterTlmListener: {
|
||||
@@ -129,8 +133,26 @@ export class WebsocketHandle {
|
||||
if (!this.on_telem_value.has(uuid_value)) {
|
||||
this.on_telem_value.set(uuid_value, []);
|
||||
}
|
||||
this.on_telem_value.get(uuid_value)?.push((value) => {
|
||||
const callback_fn = (value: TelemetryDataItem) => {
|
||||
value_result.value = value;
|
||||
};
|
||||
this.on_telem_value.get(uuid_value)?.push(callback_fn);
|
||||
onWatcherCleanup(() => {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
UnregisterTlmListener: {
|
||||
uuid: uuid_value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const index = this.on_telem_value
|
||||
.get(uuid_value)
|
||||
?.indexOf(callback_fn);
|
||||
if (index !== undefined && index >= 0) {
|
||||
this.on_telem_value
|
||||
.get(uuid_value)
|
||||
?.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
49
frontend/src/datetime.ts
Normal file
49
frontend/src/datetime.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// This function is slow
|
||||
export function getDateString(date: Date, utc: boolean, millis: boolean) {
|
||||
const year = utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
const month = (
|
||||
(utc ? date.getMonth() : date.getMonth()) + 1
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const day = (utc ? date.getUTCDate() : date.getDate()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const hour = (utc ? date.getUTCHours() : date.getHours()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const minute = (
|
||||
utc ? date.getUTCMinutes() : date.getMinutes()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const second = (
|
||||
utc ? date.getUTCSeconds() : date.getSeconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const milliseconds = (
|
||||
utc ? date.getUTCMilliseconds() : date.getMilliseconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 3,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
return `${year}/${month}/${day} ${hour}:${minute}:${second}${millis ? `.${milliseconds}` : ''}${utc ? 'Z' : ''}`;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export interface GraphData {
|
||||
border_top: MaybeRefOrGetter<number>;
|
||||
min_x: MaybeRefOrGetter<number>;
|
||||
max_x: MaybeRefOrGetter<number>;
|
||||
live: MaybeRefOrGetter<boolean>;
|
||||
fetch_history: MaybeRefOrGetter<number>;
|
||||
width: MaybeRefOrGetter<number>;
|
||||
height: MaybeRefOrGetter<number>;
|
||||
x_map: (x: number) => number;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::http::api::setup_api;
|
||||
use crate::http::websocket::setup_websocket;
|
||||
use crate::telemetry::management_service::TelemetryManagementService;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use log::{error, info};
|
||||
use log::info;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -29,7 +29,5 @@ pub async fn setup(
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
error!("http setup end");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::http::websocket::request::{RegisterTlmListenerRequest, WebsocketRequest};
|
||||
use std::collections::HashMap;
|
||||
use crate::http::websocket::request::{RegisterTlmListenerRequest, UnregisterTlmListenerRequest, WebsocketRequest};
|
||||
use crate::http::websocket::response::{TlmValueResponse, WebsocketResponse};
|
||||
use crate::telemetry::management_service::TelemetryManagementService;
|
||||
use actix_web::{rt, web, HttpRequest, HttpResponse};
|
||||
@@ -20,8 +21,13 @@ fn handle_register_tlm_listener(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
request: RegisterTlmListenerRequest,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
) {
|
||||
if let Some(tlm_data) = data.get_by_uuid(&request.uuid) {
|
||||
let token = CancellationToken::new();
|
||||
if let Some(token) = tlm_listeners.insert(tlm_data.definition.uuid.clone(), token.clone()) {
|
||||
token.cancel();
|
||||
}
|
||||
let minimum_separation = Duration::from_millis(request.minimum_separation_ms as u64);
|
||||
let mut rx = tlm_data.data.subscribe();
|
||||
let tx = tx.clone();
|
||||
@@ -35,6 +41,9 @@ fn handle_register_tlm_listener(
|
||||
_ = tx.closed() => {
|
||||
break;
|
||||
}
|
||||
_ = token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
Ok(_) = rx.changed() => {
|
||||
let now = Instant::now();
|
||||
let value = {
|
||||
@@ -71,15 +80,28 @@ fn handle_register_tlm_listener(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_unregister_tlm_listener(
|
||||
request: UnregisterTlmListenerRequest,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
) {
|
||||
if let Some(token) = tlm_listeners.remove(&request.uuid) {
|
||||
token.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_websocket_message(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
request: WebsocketRequest,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
) {
|
||||
match request {
|
||||
WebsocketRequest::RegisterTlmListener(request) => {
|
||||
handle_register_tlm_listener(data, request, tx)
|
||||
}
|
||||
handle_register_tlm_listener(data, request, tx, tlm_listeners)
|
||||
},
|
||||
WebsocketRequest::UnregisterTlmListener(request) => {
|
||||
handle_unregister_tlm_listener(request, tlm_listeners)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,12 +127,13 @@ async fn handle_websocket_incoming(
|
||||
data: &Arc<TelemetryManagementService>,
|
||||
session: &mut Session,
|
||||
tx: &Sender<WebsocketResponse>,
|
||||
tlm_listeners: &mut HashMap<String, CancellationToken>,
|
||||
) -> anyhow::Result<bool> {
|
||||
match msg {
|
||||
Ok(AggregatedMessage::Close(_)) => Ok(false),
|
||||
Ok(AggregatedMessage::Text(msg)) => match serde_json::from_str::<WebsocketRequest>(&msg) {
|
||||
Ok(request) => {
|
||||
handle_websocket_message(data, request, tx).await;
|
||||
handle_websocket_message(data, request, tx, tlm_listeners).await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(err) => Err(anyhow!("JSON Deserialization Error Encountered {err}")),
|
||||
@@ -141,12 +164,13 @@ pub async fn websocket_connect(
|
||||
let cancel_token = cancel_token.get_ref().clone();
|
||||
|
||||
rt::spawn(async move {
|
||||
let mut tlm_listeners = HashMap::new();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<WebsocketResponse>(128);
|
||||
loop {
|
||||
let result = select! {
|
||||
_ = cancel_token.cancelled() => Ok(false),
|
||||
Some(msg) = rx.recv() => handle_websocket_response(msg, &mut session).await,
|
||||
Some(msg) = stream.next() => handle_websocket_incoming(msg, data.get_ref(), &mut session, &tx).await,
|
||||
Some(msg) = stream.next() => handle_websocket_incoming(msg, data.get_ref(), &mut session, &tx, &mut tlm_listeners).await,
|
||||
else => Ok(false),
|
||||
};
|
||||
match result {
|
||||
@@ -160,6 +184,9 @@ pub async fn websocket_connect(
|
||||
}
|
||||
rx.close();
|
||||
let _ = session.close(None).await;
|
||||
for (_, token) in tlm_listeners.drain() {
|
||||
token.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
|
||||
@@ -7,7 +7,13 @@ pub struct RegisterTlmListenerRequest {
|
||||
pub minimum_separation_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnregisterTlmListenerRequest {
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, From)]
|
||||
pub enum WebsocketRequest {
|
||||
RegisterTlmListener(RegisterTlmListenerRequest),
|
||||
UnregisterTlmListener(UnregisterTlmListenerRequest),
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ pub async fn setup() -> anyhow::Result<()> {
|
||||
grpc_server.await?; //grpc server is dropped
|
||||
drop(cancellation_token); // All cancellation tokens are now dropped
|
||||
|
||||
error!("after awaits");
|
||||
|
||||
// Perform cleanup functions - at this point all servers have stopped and we can be sure that cleaning things up is safe
|
||||
for _ in 0..15 {
|
||||
if Arc::strong_count(&tlm) != 1 {
|
||||
|
||||
Reference in New Issue
Block a user