Transfer Panel Details Without an Extra Layer of JSON Encoding (#14)
**Rationale:** This made it harder to snoop on the traffic in the network monitor because it was encoded as a string. **Changes:** - Backend now accepts & provides the panel data as a JSON object rather than as a string - Backend now supports compression - Minor improvements to error handling - Some panel structures were getting saved in the JSON when they weren't supposed to be (now this no longer happens) Reviewed-on: #14 Co-authored-by: Sergey Savelyev <sergeysav.nn@gmail.com> Co-committed-by: Sergey Savelyev <sergeysav.nn@gmail.com>
This commit was merged in pull request #14.
This commit is contained in:
@@ -193,7 +193,6 @@ impl Client {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Exited Loop");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(outer_rx)
|
Ok(outer_rx)
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ onMounted(() => {
|
|||||||
model.value = 0.0;
|
model.value = 0.0;
|
||||||
}
|
}
|
||||||
} else if (is_boolean.value) {
|
} else if (is_boolean.value) {
|
||||||
debugger;
|
|
||||||
model.value = false;
|
model.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,21 @@ watch([command_info], ([cmd_info]) => {
|
|||||||
value: default_value,
|
value: default_value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Perform some cleanup to remove data that shouldn't be there
|
||||||
|
switch (model_param_value.type) {
|
||||||
|
case 'constant':
|
||||||
|
model_param_value = {
|
||||||
|
type: model_param_value.type,
|
||||||
|
value: model_param_value.value,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'input':
|
||||||
|
model_param_value = {
|
||||||
|
type: model_param_value.type,
|
||||||
|
id: model_param_value.id,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
model_value[param.name] = model_param_value;
|
model_value[param.name] = model_param_value;
|
||||||
}
|
}
|
||||||
model.value = model_value;
|
model.value = model_value;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ref } from 'vue';
|
|||||||
import CommandParameter from '@/components/CommandParameter.vue';
|
import CommandParameter from '@/components/CommandParameter.vue';
|
||||||
import FlexDivider from '@/components/FlexDivider.vue';
|
import FlexDivider from '@/components/FlexDivider.vue';
|
||||||
import type { DynamicDataType } from '@/composables/dynamic.ts';
|
import type { DynamicDataType } from '@/composables/dynamic.ts';
|
||||||
import { toJsonString } from '@/composables/json.ts';
|
import { parseJsonString, toJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
command: CommandDefinition | null;
|
command: CommandDefinition | null;
|
||||||
@@ -30,10 +30,11 @@ async function sendCommand() {
|
|||||||
},
|
},
|
||||||
body: toJsonString(params),
|
body: toJsonString(params),
|
||||||
});
|
});
|
||||||
|
const text_response = await response.text();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
result.value = await response.json();
|
result.value = parseJsonString(text_response);
|
||||||
} else {
|
} else {
|
||||||
result.value = await response.text();
|
result.value = text_response;
|
||||||
}
|
}
|
||||||
busy.value = false;
|
busy.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
isBooleanType,
|
isBooleanType,
|
||||||
isNumericType,
|
isNumericType,
|
||||||
} from '@/composables/dynamic.ts';
|
} from '@/composables/dynamic.ts';
|
||||||
|
import { parseJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: string;
|
data: string;
|
||||||
@@ -138,7 +139,14 @@ watch(
|
|||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`,
|
`/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`,
|
||||||
);
|
);
|
||||||
const response = (await res.json()) as TelemetryDataItem[];
|
const text_response = await res.text();
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(text_response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = parseJsonString(
|
||||||
|
text_response,
|
||||||
|
) as TelemetryDataItem[];
|
||||||
for (const data_item of response) {
|
for (const data_item of response) {
|
||||||
const val_t = Date.parse(data_item.timestamp);
|
const val_t = Date.parse(data_item.timestamp);
|
||||||
const raw_item_val = data_item.value[
|
const raw_item_val = data_item.value[
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ref, toValue, watchEffect } from 'vue';
|
import { ref, toValue, watchEffect } from 'vue';
|
||||||
import { type MaybeRefOrGetter } from 'vue';
|
import { type MaybeRefOrGetter } from 'vue';
|
||||||
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
||||||
|
import { parseJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
export interface CommandParameterDefinition {
|
export interface CommandParameterDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,8 +21,14 @@ export function useAllCommands() {
|
|||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/cmd`);
|
const res = await fetch(`/api/cmd`);
|
||||||
data.value = await res.json();
|
const text_response = await res.text();
|
||||||
error.value = null;
|
if (res.ok) {
|
||||||
|
data.value = parseJsonString(text_response);
|
||||||
|
error.value = null;
|
||||||
|
} else {
|
||||||
|
data.value = null;
|
||||||
|
error.value = text_response;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
error.value = e;
|
error.value = e;
|
||||||
@@ -41,12 +48,13 @@ export function useCommand(name: MaybeRefOrGetter<string>) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/cmd/${name_value}`);
|
const res = await fetch(`/api/cmd/${name_value}`);
|
||||||
|
const text_response = await res.text();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
data.value = await res.json();
|
data.value = parseJsonString(text_response);
|
||||||
error.value = null;
|
error.value = null;
|
||||||
} else {
|
} else {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
error.value = await res.text();
|
error.value = text_response;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function toJsonString(data: any): string {
|
export function toJsonString(data: any, pretty: boolean = false): string {
|
||||||
return JSON.stringify(data, (_key, value) => {
|
return JSON.stringify(
|
||||||
if (typeof value == 'bigint') {
|
data,
|
||||||
// @ts-expect-error TS2339
|
(_key, value) => {
|
||||||
return JSON.rawJSON(value.toString());
|
if (typeof value == 'bigint') {
|
||||||
}
|
// @ts-expect-error TS2339
|
||||||
return value;
|
return JSON.rawJSON(value.toString());
|
||||||
});
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
pretty ? 4 : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ref, toValue, watchEffect } from 'vue';
|
import { ref, toValue, watchEffect } from 'vue';
|
||||||
import { type MaybeRefOrGetter } from 'vue';
|
import { type MaybeRefOrGetter } from 'vue';
|
||||||
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
import type { AnyTypeId } from '@/composables/dynamic.ts';
|
||||||
|
import { parseJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
export interface TelemetryDefinition {
|
export interface TelemetryDefinition {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -16,8 +17,14 @@ export function useAllTelemetry() {
|
|||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/tlm/info`);
|
const res = await fetch(`/api/tlm/info`);
|
||||||
data.value = await res.json();
|
const text_response = await res.text();
|
||||||
error.value = null;
|
if (res.ok) {
|
||||||
|
data.value = parseJsonString(text_response);
|
||||||
|
error.value = null;
|
||||||
|
} else {
|
||||||
|
data.value = null;
|
||||||
|
error.value = text_response;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
error.value = e;
|
error.value = e;
|
||||||
@@ -37,8 +44,14 @@ export function useTelemetry(name: MaybeRefOrGetter<string>) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/tlm/info/${name_value}`);
|
const res = await fetch(`/api/tlm/info/${name_value}`);
|
||||||
data.value = await res.json();
|
const text_response = await res.text();
|
||||||
error.value = null;
|
if (res.ok) {
|
||||||
|
data.value = parseJsonString(text_response);
|
||||||
|
error.value = null;
|
||||||
|
} else {
|
||||||
|
data.value = null;
|
||||||
|
error.value = text_response;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
error.value = e;
|
error.value = e;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RouteLocationRaw } from 'vue-router';
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
import { ref, type Ref, watchEffect } from 'vue';
|
import { ref, type Ref, watchEffect } from 'vue';
|
||||||
|
import { parseJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
export enum PanelHeirarchyType {
|
export enum PanelHeirarchyType {
|
||||||
LEAF = 'leaf',
|
LEAF = 'leaf',
|
||||||
@@ -61,7 +62,11 @@ export function usePanelHeirarchy(): Ref<PanelHeirarchyChildren> {
|
|||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/panel`);
|
const res = await fetch(`/api/panel`);
|
||||||
const data = await res.json();
|
if (!res.ok) {
|
||||||
|
console.error('Failed to fetch panels');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = parseJsonString(await res.text());
|
||||||
|
|
||||||
const server_panels: PanelHeirarchyFolder = {
|
const server_panels: PanelHeirarchyFolder = {
|
||||||
name: 'Server Panels',
|
name: 'Server Panels',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DynamicComponent from '@/components/DynamicComponent.vue';
|
import DynamicComponent from '@/components/DynamicComponent.vue';
|
||||||
import type { OptionalDynamicComponentData } from '@/composables/dynamic.ts';
|
import type { OptionalDynamicComponentData } from '@/composables/dynamic.ts';
|
||||||
import { ref, watchEffect } from 'vue';
|
import { ref, useTemplateRef, watchEffect } from 'vue';
|
||||||
import { parseJsonString, toJsonString } from '@/composables/json.ts';
|
import { parseJsonString, toJsonString } from '@/composables/json.ts';
|
||||||
|
|
||||||
const data = ref<OptionalDynamicComponentData>({ type: 'none' });
|
const data = ref<OptionalDynamicComponentData>({ type: 'none' });
|
||||||
@@ -20,7 +20,11 @@ const unselect = () => {
|
|||||||
|
|
||||||
async function reload_panel_list() {
|
async function reload_panel_list() {
|
||||||
const res = await fetch(`/api/panel`);
|
const res = await fetch(`/api/panel`);
|
||||||
panel_list.value = await res.json();
|
if (!res.ok) {
|
||||||
|
console.error('Failed to reload panel list');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel_list.value = parseJsonString(await res.text());
|
||||||
}
|
}
|
||||||
watchEffect(reload_panel_list);
|
watchEffect(reload_panel_list);
|
||||||
|
|
||||||
@@ -29,10 +33,9 @@ async function load(id: string) {
|
|||||||
load_screen.value = false;
|
load_screen.value = false;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const panel_data = await fetch(`/api/panel/${id}`);
|
const panel_data = await fetch(`/api/panel/${id}`);
|
||||||
const panel_json_value = await panel_data.json();
|
const panel_text_value = await panel_data.text();
|
||||||
data.value = parseJsonString(
|
const panel_json_value = parseJsonString(panel_text_value);
|
||||||
panel_json_value['data'],
|
data.value = panel_json_value['data'] as OptionalDynamicComponentData;
|
||||||
) as OptionalDynamicComponentData;
|
|
||||||
panel_name.value = panel_json_value['name'];
|
panel_name.value = panel_json_value['name'];
|
||||||
panel_id.value = id;
|
panel_id.value = id;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -53,26 +56,31 @@ async function save() {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: toJsonString({
|
body: toJsonString({
|
||||||
name: panel_name.value,
|
name: panel_name.value,
|
||||||
data: toJsonString(data.value),
|
data: data.value,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await res.json();
|
await res.text();
|
||||||
|
// TODO: Handle failures
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/api/panel', {
|
const res = await fetch('/api/panel', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: toJsonString({
|
body: toJsonString({
|
||||||
name: panel_name.value,
|
name: panel_name.value,
|
||||||
data: toJsonString(data.value),
|
data: data.value,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const uuid = await res.json();
|
const text_response = await res.text();
|
||||||
panel_id.value = uuid as string;
|
if (res.ok) {
|
||||||
|
const uuid = parseJsonString(text_response);
|
||||||
|
panel_id.value = uuid as string;
|
||||||
|
}
|
||||||
|
// TODO: Handle failures
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -81,6 +89,35 @@ async function showLoadScreen() {
|
|||||||
load_screen.value = true;
|
load_screen.value = true;
|
||||||
await reload_panel_list();
|
await reload_panel_list();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
const file = new Blob([toJsonString(data.value, true)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
a.href = url;
|
||||||
|
a.download = panel_name.value.toLowerCase().replace(' ', '_') + '.json';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upload_input = useTemplateRef<HTMLInputElement>('upload-input');
|
||||||
|
|
||||||
|
function upload() {
|
||||||
|
upload_input.value!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUpload() {
|
||||||
|
const input = upload_input.value!;
|
||||||
|
const file = input.files![0];
|
||||||
|
const file_data = await file.text();
|
||||||
|
data.value = parseJsonString(file_data);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -113,6 +150,15 @@ async function showLoadScreen() {
|
|||||||
{{ panel_id ? 'Save' : 'New' }}
|
{{ panel_id ? 'Save' : 'New' }}
|
||||||
</button>
|
</button>
|
||||||
<button @click.prevent.stop="showLoadScreen">Load</button>
|
<button @click.prevent.stop="showLoadScreen">Load</button>
|
||||||
|
<button @click.prevent.stop="download">Download</button>
|
||||||
|
<input
|
||||||
|
ref="upload-input"
|
||||||
|
class="file"
|
||||||
|
type="file"
|
||||||
|
accept="application/json"
|
||||||
|
@change="onUpload"
|
||||||
|
/>
|
||||||
|
<button @click.prevent.stop="upload">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="inspector" class="column"></div>
|
<div id="inspector" class="column"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,4 +173,8 @@ async function showLoadScreen() {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: variables.$dark-background-color;
|
background: variables.$dark-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ provide('inputs', inputs);
|
|||||||
|
|
||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
const panel_data = await fetch(`/api/panel/${id.value}`);
|
const panel_data = await fetch(`/api/panel/${id.value}`);
|
||||||
const panel_json_value = await panel_data.json();
|
const panel_text_value = await panel_data.text();
|
||||||
panel.value = parseJsonString(
|
panel.value = parseJsonString(panel_text_value)[
|
||||||
panel_json_value['data'],
|
'data'
|
||||||
) as OptionalDynamicComponentData;
|
] as OptionalDynamicComponentData;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use uuid::Uuid;
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CreateParam {
|
struct CreateParam {
|
||||||
name: String,
|
name: String,
|
||||||
data: String,
|
data: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::http::backend::setup_backend;
|
|||||||
use crate::http::websocket::setup_websocket;
|
use crate::http::websocket::setup_websocket;
|
||||||
use crate::panels::PanelService;
|
use crate::panels::PanelService;
|
||||||
use crate::telemetry::management_service::TelemetryManagementService;
|
use crate::telemetry::management_service::TelemetryManagementService;
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::{Compress, Logger};
|
||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -36,6 +36,7 @@ pub async fn setup(
|
|||||||
.service(web::scope("/backend").configure(setup_backend))
|
.service(web::scope("/backend").configure(setup_backend))
|
||||||
.service(web::scope("/ws").configure(setup_websocket))
|
.service(web::scope("/ws").configure(setup_websocket))
|
||||||
.service(web::scope("/api").configure(setup_api))
|
.service(web::scope("/api").configure(setup_api))
|
||||||
|
.wrap(Compress::default())
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
})
|
})
|
||||||
.bind("localhost:8080")?
|
.bind("localhost:8080")?
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ impl PanelService {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(&self, name: &str, data: &str) -> anyhow::Result<Uuid> {
|
pub async fn create(&self, name: &str, data: &serde_json::Value) -> anyhow::Result<Uuid> {
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let id_string = id.to_string();
|
let id_string = id.to_string();
|
||||||
|
|
||||||
|
let data = serde_json::to_string(data)?;
|
||||||
|
|
||||||
let mut transaction = self.pool.begin().await?;
|
let mut transaction = self.pool.begin().await?;
|
||||||
|
|
||||||
let _ = sqlx::query!(
|
let _ = sqlx::query!(
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ pub struct Panel {
|
|||||||
#[sqlx(flatten)]
|
#[sqlx(flatten)]
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub header: PanelRequired,
|
pub header: PanelRequired,
|
||||||
pub data: String,
|
#[sqlx(json)]
|
||||||
|
pub data: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct PanelUpdate {
|
pub struct PanelUpdate {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub data: Option<String>,
|
pub data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user