Compare commits
10 Commits
2cb1eec404
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a110aa6376 | |||
| 94ed7e05e2 | |||
| e9751c2489 | |||
| 69c0b0965d | |||
| a864c0b41c | |||
| 44523f3cdb | |||
| 62384a3430 | |||
| 4d90575f8b | |||
| 39ddf45f0d | |||
| fdd3f2c128 |
158
Cargo.lock
generated
158
Cargo.lock
generated
@@ -49,7 +49,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
@@ -221,10 +221,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -554,18 +554,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -716,7 +716,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.13.3+wasi-0.2.2",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1003,9 +1015,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.161"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
@@ -1042,9 +1054,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
@@ -1082,7 +1094,7 @@ dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -1124,10 +1136,11 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "papaya"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce63bf9dca3eab259cffd421f05661b3386aee36276f5aed9f71450b98f5c5c"
|
||||
checksum = "dc7c76487f7eaa00a0fc1d7f88dc6b295aec478d11b0fc79f857b62c2874124c"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"seize",
|
||||
]
|
||||
|
||||
@@ -1226,7 +1239,7 @@ version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1250,9 +1263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
@@ -1281,9 +1294,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -1317,8 +1330,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.0",
|
||||
"zerocopy 0.8.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1328,7 +1352,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1337,7 +1371,17 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"zerocopy 0.8.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1468,9 +1512,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.134"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1498,13 +1542,13 @@ dependencies = [
|
||||
"actix-ws",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"derive_more 1.0.0",
|
||||
"derive_more 2.0.1",
|
||||
"fern",
|
||||
"hex",
|
||||
"log",
|
||||
"papaya",
|
||||
"prost",
|
||||
"rand",
|
||||
"rand 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
@@ -1615,18 +1659,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.9"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.9"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1681,9 +1725,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.42.0"
|
||||
version = "1.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
||||
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -1699,9 +1743,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1787,7 +1831,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -1919,6 +1963,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.13.3+wasi-0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.95"
|
||||
@@ -2065,6 +2118,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
@@ -2072,7 +2134,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2086,6 +2157,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.2"
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
server = { path = "../../server" }
|
||||
tonic = "0.12.3"
|
||||
tokio = { version = "1.42.0", features = ["rt-multi-thread", "signal"] }
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread", "signal"] }
|
||||
chrono = "0.4.39"
|
||||
tokio-util = "0.7.13"
|
||||
num-traits = "0.2.19"
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"sass": "^1.80.3",
|
||||
"sass-loader": "^16.0.2",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
"sass": "^1.85.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.16.11",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.0.0",
|
||||
"@vue/eslint-config-typescript": "^14.0.1",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-vue": "^9.29.0",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.5.4",
|
||||
"vite": "^5.4.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
"@types/node": "^22.13.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.4.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"prettier": "^3.5.1",
|
||||
"typescript": "~5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useWebsocket, WEBSOCKET_SYMBOL } from '@/composables/websocket';
|
||||
import { provide } from 'vue';
|
||||
|
||||
const websocket = useWebsocket();
|
||||
provide(WEBSOCKET_SYMBOL, websocket);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
112
frontend/src/assets/layout.scss
Normal file
112
frontend/src/assets/layout.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20vh 10vw;
|
||||
}
|
||||
|
||||
.content.page {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.content.wide {
|
||||
padding-left: 5vw;
|
||||
padding-right: 5vw;
|
||||
}
|
||||
|
||||
.content.tall {
|
||||
padding-top: 5vw;
|
||||
padding-bottom: 5vw;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.grow2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-basis {
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.no-min-height {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.no-min-width {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -4,17 +4,42 @@
|
||||
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: center;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
align-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#app > div {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
stroke-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
polyline {
|
||||
@@ -26,3 +51,11 @@ polyline {
|
||||
#{--indexed-color}: list.nth(variables.$colors, $i);
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited,
|
||||
a:hover,
|
||||
a:active {
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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>
|
||||
13
frontend/src/components/FlexDivider.vue
Normal file
13
frontend/src/components/FlexDivider.vue
Normal 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>
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
31
frontend/src/components/PanelHeirarchy.vue
Normal file
31
frontend/src/components/PanelHeirarchy.vue
Normal 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>
|
||||
@@ -11,10 +11,15 @@ 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';
|
||||
import {
|
||||
getDateString,
|
||||
getDurationString,
|
||||
parseDurationString,
|
||||
} from '@/datetime';
|
||||
import { onDocumentShown } from '@/composables/document.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
duration?: number;
|
||||
initial_duration?: number;
|
||||
utc?: boolean;
|
||||
left_axis?: boolean;
|
||||
right_axis?: boolean;
|
||||
@@ -28,21 +33,27 @@ 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;
|
||||
|
||||
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);
|
||||
@@ -55,13 +66,10 @@ onUnmounted(() => {
|
||||
resize_observer.disconnect();
|
||||
});
|
||||
|
||||
const now = useNow(33);
|
||||
const window_duration = computed(() => {
|
||||
if (props.duration) {
|
||||
return props.duration;
|
||||
}
|
||||
return 10 * 1000; // 10 seconds
|
||||
});
|
||||
const update_ms = 33;
|
||||
|
||||
const now = useNow(update_ms);
|
||||
const window_duration = ref(props.initial_duration || 10 * 1000);
|
||||
|
||||
const time_lines = [
|
||||
1, // 1ms
|
||||
@@ -79,6 +87,7 @@ const time_lines = [
|
||||
10000, // 10s
|
||||
30000, // 30s
|
||||
60000, // 1m
|
||||
150000, // 2.5m
|
||||
300000, // 5m
|
||||
6000000, // 10m
|
||||
18000000, // 30m
|
||||
@@ -91,7 +100,7 @@ const time_lines = [
|
||||
1728000000, // 2d
|
||||
6048000000, // 1w
|
||||
];
|
||||
time_lines.reverse();
|
||||
// time_lines.reverse();
|
||||
const text_offset = computed(() => 5);
|
||||
|
||||
const legend_width = 160;
|
||||
@@ -129,6 +138,10 @@ watch([now], ([now_value]) => {
|
||||
}
|
||||
});
|
||||
|
||||
onDocumentShown(() => {
|
||||
fetch_history.value++;
|
||||
});
|
||||
|
||||
const max_x_text = computed({
|
||||
// getter
|
||||
get() {
|
||||
@@ -148,6 +161,18 @@ const max_x_text = computed({
|
||||
}
|
||||
},
|
||||
});
|
||||
const duration_text = computed({
|
||||
get() {
|
||||
return getDurationString(window_duration.value);
|
||||
},
|
||||
set(newValue) {
|
||||
const new_duration = parseDurationString(newValue);
|
||||
if (!Number.isNaN(new_duration)) {
|
||||
window_duration.value = Math.max(new_duration, 1);
|
||||
fetch_history.value++;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const x_map = (x: number) => {
|
||||
return (
|
||||
@@ -178,16 +203,25 @@ 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 = graph_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;
|
||||
@@ -200,13 +234,9 @@ const mouse_x = ref<number | null>(null);
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
if (props.cursor) {
|
||||
const new_x =
|
||||
mouse_x.value =
|
||||
event.clientX -
|
||||
(event.currentTarget as Element).getBoundingClientRect().left;
|
||||
if (new_x == 0) {
|
||||
console.log(event);
|
||||
}
|
||||
mouse_x.value = new_x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +250,6 @@ const mouse_t = computed(() => {
|
||||
}
|
||||
const t = inv_x_map(mouse_x.value);
|
||||
if (t < min_x.value || t > max_x.value) {
|
||||
// console.log(`x ${mouse_x.value} t ${t} min ${t < min_x.value} max ${t > max_x.value}`)
|
||||
// debugger;
|
||||
return null;
|
||||
}
|
||||
return t;
|
||||
@@ -229,7 +257,7 @@ const mouse_t = computed(() => {
|
||||
|
||||
const show_data_at_time = computed(() => {
|
||||
if (mouse_t.value === null) {
|
||||
return max_x.value;
|
||||
return max_x.value + update_ms;
|
||||
} else {
|
||||
return mouse_t.value;
|
||||
}
|
||||
@@ -237,14 +265,19 @@ const show_data_at_time = computed(() => {
|
||||
|
||||
const should_fade = ref(false);
|
||||
|
||||
const max_temporal_resolution = computed(() => {
|
||||
const delta_t = window_duration.value;
|
||||
return Math.floor(delta_t / 1000); // Aim for a maximum of 1000 data points
|
||||
});
|
||||
|
||||
provide<GraphData>(GRAPH_DATA, {
|
||||
border_top: border_top,
|
||||
min_x: min_x,
|
||||
max_x: max_x,
|
||||
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,
|
||||
@@ -269,6 +302,14 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
:style="`height: ${controls_height}px`"
|
||||
>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:size="15"
|
||||
v-model="duration_text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -335,6 +376,7 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
:timestamp="tick"
|
||||
:utc="props.utc"
|
||||
:show_millis="line_duration < 1000"
|
||||
:key="tick"
|
||||
></TimeText>
|
||||
</template>
|
||||
</g>
|
||||
@@ -393,6 +435,7 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
div.full-size {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.controls-header {
|
||||
@@ -401,11 +444,11 @@ div.controls-header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: stretch;
|
||||
gap: 1em 0;
|
||||
gap: 0 1em;
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
|
||||
div.controls-header > div.grow {
|
||||
flex-grow: 1;
|
||||
svg.graph {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
80
frontend/src/components/TelemetryInfo.vue
Normal file
80
frontend/src/components/TelemetryInfo.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<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<{
|
||||
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 (secondary.value) {
|
||||
result.push(secondary.value);
|
||||
}
|
||||
if (telemetry_definition.value) {
|
||||
result.push(telemetry_definition.value);
|
||||
}
|
||||
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>
|
||||
@@ -37,18 +37,26 @@ const legend_text_offset = 4;
|
||||
const marker_radius = 3;
|
||||
|
||||
const text_offset = computed(() => 10);
|
||||
const min_sep = computed(() =>
|
||||
Math.min(props.minimum_separation || 0, maximum_minimum_separation_live),
|
||||
);
|
||||
|
||||
const graph_data = inject<GraphData>(GRAPH_DATA)!;
|
||||
const axis_data = inject<AxisData>(AXIS_DATA)!;
|
||||
|
||||
const data_min_sep = computed(() => {
|
||||
return Math.max(
|
||||
props.minimum_separation || 0,
|
||||
toValue(graph_data.max_temporal_resolution),
|
||||
);
|
||||
});
|
||||
|
||||
const live_min_sep = computed(() =>
|
||||
Math.min(data_min_sep.value, 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,
|
||||
live_min_sep,
|
||||
graph_data.live,
|
||||
);
|
||||
|
||||
@@ -95,7 +103,7 @@ watch([value], ([val]) => {
|
||||
x: val_t,
|
||||
y: item_val,
|
||||
} as Point;
|
||||
memo.value.insert(new_item, props.minimum_separation);
|
||||
memo.value.insert(new_item, data_min_sep.value);
|
||||
if (item_val < min.value) {
|
||||
min.value = item_val;
|
||||
}
|
||||
@@ -118,7 +126,7 @@ watch(
|
||||
const min_x = new Date(toValue(graph_data.min_x));
|
||||
const max_x = new Date(toValue(graph_data.max_x));
|
||||
const res = await fetch(
|
||||
`/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${props.minimum_separation || 0}`,
|
||||
`/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`,
|
||||
);
|
||||
const response = (await res.json()) as TelemetryDataItem[];
|
||||
for (const data_item of response) {
|
||||
@@ -136,9 +144,7 @@ watch(
|
||||
max.value = item_val;
|
||||
}
|
||||
}
|
||||
memo.value.reduce_to_maximum_separation(
|
||||
props.minimum_separation || 0,
|
||||
);
|
||||
memo.value.reduce_to_maximum_separation(data_min_sep.value);
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
recompute_bounds.value++;
|
||||
@@ -383,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"
|
||||
@@ -438,6 +444,11 @@ function onMouseExit(event: MouseEvent) {
|
||||
<div class="row">
|
||||
<span>{{ telemetry_data?.data_type }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>{{
|
||||
current_data_point?.y || 'Missing Data'
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row">
|
||||
@@ -462,6 +473,10 @@ function onMouseExit(event: MouseEvent) {
|
||||
opacity: 25%;
|
||||
}
|
||||
|
||||
.fade .fade_other_selected.label {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade .no-fade .fade_other_selected {
|
||||
opacity: 100%;
|
||||
}
|
||||
@@ -514,21 +529,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;
|
||||
}
|
||||
|
||||
|
||||
93
frontend/src/components/TelemetryList.vue
Normal file
93
frontend/src/components/TelemetryList.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type TelemetryDefinition,
|
||||
useAllTelemetry,
|
||||
} from '@/composables/telemetry';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
search?: string;
|
||||
}>();
|
||||
|
||||
const selected = defineModel<TelemetryDefinition | null>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'mouseover', tlm_entry: 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);
|
||||
|
||||
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], ([mousedover_val]) => {
|
||||
emit('mouseover', mousedover_val);
|
||||
});
|
||||
</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>
|
||||
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>
|
||||
47
frontend/src/components/TextInput.vue
Normal file
47
frontend/src/components/TextInput.vue
Normal 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>
|
||||
@@ -20,7 +20,7 @@ const height = ref(0);
|
||||
|
||||
function update_for_element(element: Element) {
|
||||
top.value = element.getBoundingClientRect().top + window.scrollY;
|
||||
left.value = element.getBoundingClientRect().left + window.screenX;
|
||||
left.value = element.getBoundingClientRect().left + window.scrollX;
|
||||
width.value = element.getBoundingClientRect().width;
|
||||
height.value = element.getBoundingClientRect().height;
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,25 @@ export interface TelemetryDefinition {
|
||||
data_type: string;
|
||||
}
|
||||
|
||||
export function useAllTelemetry() {
|
||||
const data = ref<TelemetryDefinition[] | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const error = ref<any | null>(null);
|
||||
|
||||
watchEffect(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/tlm/info`);
|
||||
data.value = await res.json();
|
||||
error.value = null;
|
||||
} catch (e) {
|
||||
data.value = null;
|
||||
error.value = e;
|
||||
}
|
||||
});
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export function useTelemetry(name: MaybeRefOrGetter<string>) {
|
||||
const data = ref<TelemetryDefinition | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -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,5 +1,9 @@
|
||||
// This function is slow
|
||||
export function getDateString(date: Date, utc: boolean, millis: boolean) {
|
||||
export function getDateString(
|
||||
date: Date,
|
||||
utc: boolean,
|
||||
millis: boolean,
|
||||
): string {
|
||||
const year = utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
const month = (
|
||||
(utc ? date.getMonth() : date.getMonth()) + 1
|
||||
@@ -47,3 +51,47 @@ export function getDateString(date: Date, utc: boolean, millis: boolean) {
|
||||
});
|
||||
return `${year}/${month}/${day} ${hour}:${minute}:${second}${millis ? `.${milliseconds}` : ''}${utc ? 'Z' : ''}`;
|
||||
}
|
||||
|
||||
export function getDurationString(duration_millis: number): string {
|
||||
const millis = duration_millis % 1000;
|
||||
const duration_secs = Math.floor(duration_millis / 1000);
|
||||
const seconds = duration_secs % 60;
|
||||
const duration_minutes = Math.floor(duration_secs / 60);
|
||||
const minutes = duration_minutes % 60;
|
||||
const duration_hours = Math.floor(duration_minutes / 60);
|
||||
|
||||
const millis_str = millis.toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 3,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const seconds_str = seconds.toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const minutes_str = minutes.toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const hours_str = duration_hours.toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 1,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
return `${hours_str}:${minutes_str}:${seconds_str}.${millis_str}`;
|
||||
}
|
||||
|
||||
export function parseDurationString(duration: string): number {
|
||||
const parts = duration.split(':');
|
||||
const seconds_str = parts.length >= 1 ? parts[parts.length - 1] : '0';
|
||||
const minutes_str = parts.length >= 2 ? parts[parts.length - 2] : '0';
|
||||
const hours_str = parts.length >= 3 ? parts[parts.length - 3] : '0';
|
||||
|
||||
const seconds = Number.parseFloat(seconds_str);
|
||||
const minutes = Number.parseFloat(minutes_str);
|
||||
const hours = Number.parseFloat(hours_str);
|
||||
|
||||
return Math.floor(((hours * 60 + minutes) * 60 + seconds) * 1000);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface GraphData {
|
||||
border_top: MaybeRefOrGetter<number>;
|
||||
min_x: MaybeRefOrGetter<number>;
|
||||
max_x: MaybeRefOrGetter<number>;
|
||||
max_temporal_resolution: MaybeRefOrGetter<number>;
|
||||
live: MaybeRefOrGetter<boolean>;
|
||||
fetch_history: MaybeRefOrGetter<number>;
|
||||
width: MaybeRefOrGetter<number>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import './assets/main.scss';
|
||||
import './assets/layout.scss';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
104
frontend/src/panels/panel.ts
Normal file
104
frontend/src/panels/panel.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
export enum PanelHeirarchyType {
|
||||
LEAF = 'leaf',
|
||||
FOLDER = '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,
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: 'Chart',
|
||||
to: { name: 'chart' },
|
||||
type: PanelHeirarchyType.LEAF,
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: 'Panel Test',
|
||||
to: { name: 'panel_test' },
|
||||
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;
|
||||
}
|
||||
@@ -6,7 +6,27 @@ const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
component: () => import('../views/EmptyPanelView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/graph',
|
||||
name: 'graph',
|
||||
component: () => import('../views/GraphView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: () => import('../views/TelemetryListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chart',
|
||||
name: 'chart',
|
||||
component: () => import('../views/ChartView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/panel_test',
|
||||
name: 'panel_test',
|
||||
component: () => import('../views/PanelTest.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: calc(variables.$normal-text-size / 2);
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
49
frontend/src/views/ChartRenderView.vue
Normal file
49
frontend/src/views/ChartRenderView.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
53
frontend/src/views/ChartView.vue
Normal file
53
frontend/src/views/ChartView.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<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';
|
||||
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[][][]>([]);
|
||||
|
||||
function openSettings() {
|
||||
settingsOpen.value = true;
|
||||
}
|
||||
function closeSettings() {
|
||||
settingsOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScreenLayout :direction="Direction.Column" :type="ScreenType.WideTall">
|
||||
<template v-if="!settingsOpen">
|
||||
<div class="settings column" @click="openSettings">
|
||||
<InlineIcon icon="hamburger menu"></InlineIcon>
|
||||
</div>
|
||||
<ChartRenderView :charts="settings"></ChartRenderView>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="settings column" @click="closeSettings">
|
||||
<InlineIcon icon="close"></InlineIcon>
|
||||
</div>
|
||||
<ChartDefinitionView v-model="settings"></ChartDefinitionView>
|
||||
</template>
|
||||
</ScreenLayout>
|
||||
</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>
|
||||
63
frontend/src/views/EmptyPanelView.vue
Normal file
63
frontend/src/views/EmptyPanelView.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<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';
|
||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
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>
|
||||
<ScreenLayout :direction="Direction.Column" :type="ScreenType.Page" limit>
|
||||
<div class="row">
|
||||
<TextInput
|
||||
autofocus
|
||||
class="grow"
|
||||
v-model="searchValue"
|
||||
placeholder="Search"
|
||||
@enter="onEnter"
|
||||
></TextInput>
|
||||
</div>
|
||||
|
||||
<div class="row grow stretch no-min-height">
|
||||
<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>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
47
frontend/src/views/GraphView.vue
Normal file
47
frontend/src/views/GraphView.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import Axis from '@/components/GraphAxis.vue';
|
||||
import Line from '@/components/TelemetryLine.vue';
|
||||
import { GraphSide } from '@/graph/graph';
|
||||
import SvgGraph from '@/components/SvgGraph.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column">
|
||||
<div style="width: 100vw; height: 50vh">
|
||||
<SvgGraph
|
||||
:legend="GraphSide.Left"
|
||||
right_axis
|
||||
include_controls
|
||||
cursor
|
||||
>
|
||||
<Axis>
|
||||
<Line data="simple_producer/time_offset"></Line>
|
||||
<Line data="simple_producer/publish_offset"></Line>
|
||||
<Line data="simple_producer/await_offset"></Line>
|
||||
</Axis>
|
||||
</SvgGraph>
|
||||
</div>
|
||||
<div style="width: 100vw; height: 50vh">
|
||||
<SvgGraph
|
||||
:initial_duration="60 * 1000 * 10"
|
||||
:legend="GraphSide.Right"
|
||||
right_axis
|
||||
>
|
||||
<Axis>
|
||||
<Line data="simple_producer/sin"></Line>
|
||||
<Line data="simple_producer/cos4"></Line>
|
||||
<Line data="simple_producer/sin2"></Line>
|
||||
<Line data="simple_producer/cos"></Line>
|
||||
<Line data="simple_producer/sin3"></Line>
|
||||
<Line data="simple_producer/cos2"></Line>
|
||||
<Line data="simple_producer/sin4"></Line>
|
||||
<Line data="simple_producer/cos3"></Line>
|
||||
</Axis>
|
||||
</SvgGraph>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useWebsocket, WEBSOCKET_SYMBOL } from '@/composables/websocket';
|
||||
import { provide } from 'vue';
|
||||
import Axis from '@/components/GraphAxis.vue';
|
||||
import Line from '@/components/TelemetryLine.vue';
|
||||
import { GraphSide } from '@/graph/graph';
|
||||
import SvgGraph from '@/components/SvgGraph.vue';
|
||||
|
||||
const websocket = useWebsocket();
|
||||
provide(WEBSOCKET_SYMBOL, websocket);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 100vw; height: 50vh">
|
||||
<SvgGraph :legend="GraphSide.Left" right_axis include_controls cursor>
|
||||
<Axis>
|
||||
<Line data="simple_producer/time_offset"></Line>
|
||||
<Line data="simple_producer/publish_offset"></Line>
|
||||
<Line data="simple_producer/await_offset"></Line>
|
||||
</Axis>
|
||||
</SvgGraph>
|
||||
</div>
|
||||
<div style="width: 100vw; height: 50vh">
|
||||
<SvgGraph
|
||||
:duration="60 * 1000 * 10"
|
||||
:legend="GraphSide.Right"
|
||||
right_axis
|
||||
>
|
||||
<Axis>
|
||||
<Line
|
||||
data="simple_producer/sin"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/cos4"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/sin2"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/cos"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/sin3"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/cos2"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/sin4"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
<Line
|
||||
data="simple_producer/cos3"
|
||||
:minimum_separation="1000"
|
||||
></Line>
|
||||
</Axis>
|
||||
</SvgGraph>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
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>
|
||||
55
frontend/src/views/TelemetryListView.vue
Normal file
55
frontend/src/views/TelemetryListView.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<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 ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||
import { Direction } from '@/composables/Direction.ts';
|
||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
const searchValue = ref('');
|
||||
|
||||
const selected = ref<TelemetryDefinition | null>(null);
|
||||
const mousedover = ref<TelemetryDefinition | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScreenLayout :direction="Direction.Row" :type="ScreenType.Standard" limit>
|
||||
<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"
|
||||
v-model="selected"
|
||||
@mouseover="
|
||||
(mousedover_value) =>
|
||||
(mousedover = mousedover_value)
|
||||
"
|
||||
></TelemetryList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlexDivider></FlexDivider>
|
||||
<div class="column grow stretch no-basis">
|
||||
<TelemetryInfo
|
||||
:mouseover="mousedover"
|
||||
:selection="selected"
|
||||
></TelemetryInfo>
|
||||
</div>
|
||||
</ScreenLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
@@ -7,21 +7,21 @@ authors = ["Sergey <me@sergeysav.com>"]
|
||||
|
||||
[dependencies]
|
||||
fern = "0.7.1"
|
||||
log = "0.4.22"
|
||||
prost = "0.13.4"
|
||||
rand = "0.8.5"
|
||||
log = "0.4.25"
|
||||
prost = "0.13.5"
|
||||
rand = "0.9.0"
|
||||
tonic = { version = "0.12.3" }
|
||||
tokio = { version = "1.42.0", features = ["rt-multi-thread", "signal", "fs"] }
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread", "signal", "fs"] }
|
||||
chrono = "0.4.39"
|
||||
actix-web = "4.9.0"
|
||||
actix-ws = "0.3.0"
|
||||
tokio-util = "0.7.13"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.134"
|
||||
serde_json = "1.0.138"
|
||||
hex = "0.4.3"
|
||||
papaya = "0.1.7"
|
||||
thiserror = "2.0.9"
|
||||
derive_more = { version = "1.0.0", features = ["from"] }
|
||||
papaya = "0.1.8"
|
||||
thiserror = "2.0.11"
|
||||
derive_more = { version = "2.0.1", features = ["from"] }
|
||||
anyhow = "1.0.95"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -21,6 +21,14 @@ async fn get_tlm_definition(
|
||||
Ok(web::Json(data.definition.clone()))
|
||||
}
|
||||
|
||||
#[get("/tlm/info")]
|
||||
async fn get_all_tlm_definitions(
|
||||
data: web::Data<Arc<TelemetryManagementService>>,
|
||||
) -> Result<impl Responder, HttpServerResultError> {
|
||||
trace!("get_all_tlm_definitions");
|
||||
Ok(web::Json(data.get_all_definitions()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct HistoryQuery {
|
||||
from: String,
|
||||
@@ -71,5 +79,7 @@ async fn get_tlm_history(
|
||||
}
|
||||
|
||||
pub fn setup_api(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_tlm_definition).service(get_tlm_history);
|
||||
cfg.service(get_all_tlm_definitions)
|
||||
.service(get_tlm_definition)
|
||||
.service(get_tlm_history);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::http::websocket::request::{RegisterTlmListenerRequest, UnregisterTlmListenerRequest, WebsocketRequest};
|
||||
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};
|
||||
use actix_ws::{AggregatedMessage, ProtocolError, Session};
|
||||
use anyhow::anyhow;
|
||||
use log::{error, trace};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::{sleep, Instant};
|
||||
use tokio::{pin, select};
|
||||
use tokio::time::{sleep_until, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tonic::codegen::tokio_stream::StreamExt;
|
||||
|
||||
@@ -32,48 +34,29 @@ fn handle_register_tlm_listener(
|
||||
let mut rx = tlm_data.data.subscribe();
|
||||
let tx = tx.clone();
|
||||
rt::spawn(async move {
|
||||
let mut last_sent_at = Instant::now() - minimum_separation;
|
||||
let mut last_value = None;
|
||||
let sleep = sleep(Duration::from_millis(0));
|
||||
pin!(sleep);
|
||||
loop {
|
||||
select! {
|
||||
_ = tx.closed() => {
|
||||
break;
|
||||
}
|
||||
_ = token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
let now = select! {
|
||||
biased;
|
||||
_ = tx.closed() => { break; }
|
||||
_ = token.cancelled() => { break; }
|
||||
Ok(_) = rx.changed() => {
|
||||
let now = Instant::now();
|
||||
let value = {
|
||||
let ref_val = rx.borrow_and_update();
|
||||
ref_val.clone()
|
||||
};
|
||||
if last_sent_at + minimum_separation > now {
|
||||
last_value = value;
|
||||
sleep.as_mut().reset(last_sent_at + minimum_separation);
|
||||
continue;
|
||||
} else {
|
||||
last_value = None;
|
||||
last_sent_at = now;
|
||||
}
|
||||
let _ = tx.send(TlmValueResponse {
|
||||
uuid: request.uuid.clone(),
|
||||
value,
|
||||
}.into()).await;
|
||||
now
|
||||
}
|
||||
() = &mut sleep => {
|
||||
if let Some(value) = last_value {
|
||||
let _ = tx.send(TlmValueResponse {
|
||||
uuid: request.uuid.clone(),
|
||||
value: Some(value),
|
||||
}.into()).await;
|
||||
}
|
||||
last_value = None;
|
||||
let now = Instant::now();
|
||||
last_sent_at = now;
|
||||
}
|
||||
};
|
||||
select! {
|
||||
biased;
|
||||
_ = tx.closed() => { break; }
|
||||
_ = token.cancelled() => { break; }
|
||||
_ = sleep_until(now + minimum_separation) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,10 +81,10 @@ async fn handle_websocket_message(
|
||||
match request {
|
||||
WebsocketRequest::RegisterTlmListener(request) => {
|
||||
handle_register_tlm_listener(data, request, tx, tlm_listeners)
|
||||
},
|
||||
}
|
||||
WebsocketRequest::UnregisterTlmListener(request) => {
|
||||
handle_unregister_tlm_listener(request, tlm_listeners)
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub trait FileWriteableType {
|
||||
fn write_to_file(self, file: &mut File) -> io::Result<()>;
|
||||
fn write_to_file(self, file: &mut impl Write) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub trait FileReadableType: Sized {
|
||||
fn read_from_file(file: &mut File) -> io::Result<Self>;
|
||||
fn read_from_file(file: &mut impl Read) -> io::Result<Self>;
|
||||
}
|
||||
|
||||
pub trait FileExt {
|
||||
pub trait WriteExt {
|
||||
fn write_data<T: FileWriteableType>(&mut self, data: T) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub trait ReadExt {
|
||||
fn read_data<T: FileReadableType>(&mut self) -> io::Result<T>;
|
||||
}
|
||||
|
||||
impl FileExt for File {
|
||||
impl<W: Write> WriteExt for W {
|
||||
fn write_data<T: FileWriteableType>(&mut self, data: T) -> io::Result<()> {
|
||||
data.write_to_file(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadExt for R {
|
||||
fn read_data<T: FileReadableType>(&mut self) -> io::Result<T> {
|
||||
T::read_from_file(self)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use crate::serialization::file_ext::{FileReadableType, FileWriteableType};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
macro_rules! primitive_write_read {
|
||||
( $primitive:ty, $length:expr ) => {
|
||||
impl FileWriteableType for $primitive {
|
||||
fn write_to_file(self, file: &mut File) -> std::io::Result<()> {
|
||||
fn write_to_file(self, file: &mut impl Write) -> std::io::Result<()> {
|
||||
file.write_all(&self.to_be_bytes())
|
||||
}
|
||||
}
|
||||
impl FileReadableType for $primitive {
|
||||
fn read_from_file(file: &mut File) -> std::io::Result<Self> {
|
||||
fn read_from_file(file: &mut impl Read) -> std::io::Result<Self> {
|
||||
let mut buffer = [0u8; $length];
|
||||
file.read_exact(&mut buffer)?;
|
||||
Ok(Self::from_be_bytes(buffer))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::core::TelemetryDataType;
|
||||
use crate::serialization::file_ext::FileExt;
|
||||
use crate::serialization::file_ext::{ReadExt, WriteExt};
|
||||
use crate::telemetry::data::TelemetryData;
|
||||
use crate::telemetry::data_item::TelemetryDataItem;
|
||||
use crate::telemetry::data_value::TelemetryDataValue;
|
||||
@@ -10,7 +10,7 @@ use log::{error, info};
|
||||
use std::cmp::min;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{fs, path};
|
||||
@@ -137,7 +137,8 @@ struct HistorySegmentFile {
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
length: u64,
|
||||
file: File,
|
||||
file: BufReader<File>,
|
||||
file_position: i64,
|
||||
}
|
||||
|
||||
impl HistorySegmentFile {
|
||||
@@ -158,31 +159,16 @@ impl HistorySegmentFile {
|
||||
segment.start.to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||
));
|
||||
|
||||
let file = File::create(file)?;
|
||||
let mut file = BufWriter::new(File::create(file)?);
|
||||
|
||||
let mut result = Self {
|
||||
start: segment.start,
|
||||
end: segment.end,
|
||||
length: 0,
|
||||
file,
|
||||
};
|
||||
|
||||
let utc_offset_start = result.start - DateTime::UNIX_EPOCH;
|
||||
let utc_offset_end = result.end - DateTime::UNIX_EPOCH;
|
||||
let utc_offset_start = segment.start - DateTime::UNIX_EPOCH;
|
||||
let utc_offset_end = segment.end - DateTime::UNIX_EPOCH;
|
||||
|
||||
// Write the segment bounds
|
||||
result
|
||||
.file
|
||||
.write_data::<i64>(utc_offset_start.num_seconds())?;
|
||||
result
|
||||
.file
|
||||
.write_data::<i32>(utc_offset_start.subsec_nanos())?;
|
||||
result
|
||||
.file
|
||||
.write_data::<i64>(utc_offset_end.num_seconds())?;
|
||||
result
|
||||
.file
|
||||
.write_data::<i32>(utc_offset_end.subsec_nanos())?;
|
||||
file.write_data::<i64>(utc_offset_start.num_seconds())?;
|
||||
file.write_data::<i32>(utc_offset_start.subsec_nanos())?;
|
||||
file.write_data::<i64>(utc_offset_end.num_seconds())?;
|
||||
file.write_data::<i32>(utc_offset_end.subsec_nanos())?;
|
||||
|
||||
let data = segment.data.get_mut().unwrap_or_else(|err| {
|
||||
error!(
|
||||
@@ -197,26 +183,35 @@ impl HistorySegmentFile {
|
||||
"Invalid Segment Cannot Be Saved to Disk"
|
||||
);
|
||||
|
||||
result.length = data.timestamps.len() as u64;
|
||||
result.file.write_data::<u64>(result.length)?;
|
||||
let length = data.timestamps.len() as u64;
|
||||
file.write_data::<u64>(length)?;
|
||||
|
||||
// Write all the timestamps
|
||||
for timestamp in &data.timestamps {
|
||||
let utc_offset = *timestamp - DateTime::UNIX_EPOCH;
|
||||
result.file.write_data::<i64>(utc_offset.num_seconds())?;
|
||||
result.file.write_data::<i32>(utc_offset.subsec_nanos())?;
|
||||
file.write_data::<i64>(utc_offset.num_seconds())?;
|
||||
file.write_data::<i32>(utc_offset.subsec_nanos())?;
|
||||
}
|
||||
|
||||
// Write all the values
|
||||
for value in &data.values {
|
||||
match value {
|
||||
TelemetryDataValue::Float32(value) => result.file.write_data::<f32>(*value)?,
|
||||
TelemetryDataValue::Float64(value) => result.file.write_data::<f64>(*value)?,
|
||||
TelemetryDataValue::Float32(value) => file.write_data::<f32>(*value)?,
|
||||
TelemetryDataValue::Float64(value) => file.write_data::<f64>(*value)?,
|
||||
}
|
||||
}
|
||||
|
||||
result.file.flush()?;
|
||||
Ok(result)
|
||||
file.flush()?;
|
||||
|
||||
let mut file = BufReader::new(file.into_inner()?);
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(Self {
|
||||
start: segment.start,
|
||||
end: segment.end,
|
||||
length: 0,
|
||||
file,
|
||||
file_position: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_to_ram(
|
||||
@@ -229,6 +224,7 @@ impl HistorySegmentFile {
|
||||
};
|
||||
|
||||
self.file.seek(SeekFrom::Start(Self::HEADER_LENGTH))?;
|
||||
self.file_position = Self::HEADER_LENGTH as i64;
|
||||
for _ in 0..self.length {
|
||||
segment_data.timestamps.push(self.read_date_time()?);
|
||||
}
|
||||
@@ -255,7 +251,7 @@ impl HistorySegmentFile {
|
||||
start.to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||
));
|
||||
|
||||
let mut file = File::open(file)?;
|
||||
let mut file = BufReader::new(File::open(file)?);
|
||||
|
||||
// Write the segment bounds
|
||||
let start_seconds = file.read_data::<i64>()?;
|
||||
@@ -269,11 +265,13 @@ impl HistorySegmentFile {
|
||||
|
||||
let length = file.read_data::<u64>()?;
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(HistorySegmentFile {
|
||||
start: DateTime::UNIX_EPOCH + start,
|
||||
end: DateTime::UNIX_EPOCH + end,
|
||||
length,
|
||||
file,
|
||||
file_position: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -284,6 +282,8 @@ impl HistorySegmentFile {
|
||||
maximum_resolution: TimeDelta,
|
||||
telemetry_data_type: TelemetryDataType,
|
||||
) -> anyhow::Result<(DateTime<Utc>, Vec<TelemetryDataItem>)> {
|
||||
self.file_position = 0;
|
||||
self.file.seek(SeekFrom::Start(0))?;
|
||||
let mut result = vec![];
|
||||
|
||||
let mut next_from = from;
|
||||
@@ -319,15 +319,17 @@ impl HistorySegmentFile {
|
||||
fn read_date_time(&mut self) -> anyhow::Result<DateTime<Utc>> {
|
||||
let seconds = self.file.read_data::<i64>()?;
|
||||
let nanos = self.file.read_data::<i32>()?;
|
||||
self.file_position += 8 + 4;
|
||||
let start =
|
||||
TimeDelta::new(seconds, nanos as u32).context("Failed to reconstruct TimeDelta")?;
|
||||
Ok(DateTime::UNIX_EPOCH + start)
|
||||
}
|
||||
|
||||
fn get_date_time(&mut self, index: u64) -> anyhow::Result<DateTime<Utc>> {
|
||||
self.file.seek(SeekFrom::Start(
|
||||
Self::HEADER_LENGTH + index * Self::TIMESTAMP_LENGTH,
|
||||
))?;
|
||||
let desired_position = Self::HEADER_LENGTH + index * Self::TIMESTAMP_LENGTH;
|
||||
let seek_amount = desired_position as i64 - self.file_position;
|
||||
self.file_position += seek_amount;
|
||||
self.file.seek_relative(seek_amount)?;
|
||||
self.read_date_time()
|
||||
}
|
||||
|
||||
@@ -337,9 +339,11 @@ impl HistorySegmentFile {
|
||||
) -> anyhow::Result<TelemetryDataValue> {
|
||||
match telemetry_data_type {
|
||||
TelemetryDataType::Float32 => {
|
||||
self.file_position += 4;
|
||||
Ok(TelemetryDataValue::Float32(self.file.read_data::<f32>()?))
|
||||
}
|
||||
TelemetryDataType::Float64 => {
|
||||
self.file_position += 8;
|
||||
Ok(TelemetryDataValue::Float64(self.file.read_data::<f64>()?))
|
||||
}
|
||||
}
|
||||
@@ -354,9 +358,11 @@ impl HistorySegmentFile {
|
||||
TelemetryDataType::Float32 => 4,
|
||||
TelemetryDataType::Float64 => 8,
|
||||
};
|
||||
self.file.seek(SeekFrom::Start(
|
||||
Self::HEADER_LENGTH + self.length * Self::TIMESTAMP_LENGTH + index * item_length,
|
||||
))?;
|
||||
let desired_position =
|
||||
Self::HEADER_LENGTH + self.length * Self::TIMESTAMP_LENGTH + index * item_length;
|
||||
let seek_amount = desired_position as i64 - self.file_position;
|
||||
self.file_position += seek_amount;
|
||||
self.file.seek_relative(seek_amount)?;
|
||||
self.read_telemetry_item(telemetry_data_type)
|
||||
}
|
||||
|
||||
@@ -534,14 +540,31 @@ impl TelemetryHistory {
|
||||
maximum_resolution: TimeDelta,
|
||||
telemetry_history_service: &TelemetryHistoryService,
|
||||
) -> Vec<TelemetryDataItem> {
|
||||
let mut result = vec![];
|
||||
|
||||
let segments = self.segments.read().await;
|
||||
let mut disk_result = vec![];
|
||||
let mut ram_result = vec![];
|
||||
|
||||
let mut from = from;
|
||||
let mut to = to;
|
||||
let initial_to = to;
|
||||
let mut ram_from_result = from;
|
||||
|
||||
{
|
||||
let segments = self.segments.read().await;
|
||||
let first_ram_segment = segments.front().map(|x| x.start);
|
||||
if let Some(first_ram_segment) = first_ram_segment {
|
||||
let mut ram_from = first_ram_segment;
|
||||
for i in 0..segments.len() {
|
||||
let (new_from, new_data) = segments[i].get(ram_from, to, maximum_resolution);
|
||||
ram_from = new_from;
|
||||
ram_result.extend(new_data);
|
||||
}
|
||||
from = min(from, first_ram_segment);
|
||||
to = min(to, first_ram_segment);
|
||||
ram_from_result = ram_from;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let start = from
|
||||
.duration_trunc(telemetry_history_service.segment_width)
|
||||
.unwrap();
|
||||
@@ -549,12 +572,6 @@ impl TelemetryHistory {
|
||||
.duration_trunc(telemetry_history_service.segment_width)
|
||||
.unwrap();
|
||||
|
||||
let end = if let Some(first_ram_segment) = first_ram_segment {
|
||||
min(end, first_ram_segment)
|
||||
} else {
|
||||
end
|
||||
};
|
||||
|
||||
let mut path = telemetry_history_service.data_root_folder.clone();
|
||||
path.push(&self.data.definition.uuid);
|
||||
|
||||
@@ -568,7 +585,7 @@ impl TelemetryHistory {
|
||||
match disk.get(from, to, maximum_resolution, self.data.definition.data_type) {
|
||||
Ok((new_from, new_data)) => {
|
||||
from = new_from;
|
||||
result.extend(new_data);
|
||||
disk_result.extend(new_data);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to get from disk segment: {err}");
|
||||
@@ -579,13 +596,21 @@ impl TelemetryHistory {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Go through the ram segments a second time to capture any data added since we dealt
|
||||
// with the disk data
|
||||
from = ram_from_result;
|
||||
to = initial_to;
|
||||
let segments = self.segments.read().await;
|
||||
for i in 0..segments.len() {
|
||||
let (new_from, new_data) = segments[i].get(from, to, maximum_resolution);
|
||||
from = new_from;
|
||||
result.extend(new_data);
|
||||
ram_result.extend(new_data);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
disk_result.extend(ram_result);
|
||||
disk_result
|
||||
}
|
||||
|
||||
pub async fn cleanup(&self, service: &TelemetryHistoryService) -> anyhow::Result<()> {
|
||||
|
||||
@@ -146,6 +146,14 @@ impl TelemetryManagementService {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn get_all_definitions(&self) -> Vec<TelemetryDefinition> {
|
||||
let tlm_data = self.tlm_data.pin();
|
||||
tlm_data
|
||||
.values()
|
||||
.map(|x| x.data.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn pin(&self) -> TelemetryManagementServicePin {
|
||||
TelemetryManagementServicePin {
|
||||
tlm_data: self.tlm_data.pin(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use rand::RngCore;
|
||||
impl Uuid {
|
||||
pub fn random() -> Self {
|
||||
let mut uuid = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut uuid);
|
||||
rand::rng().fill_bytes(&mut uuid);
|
||||
Self {
|
||||
value: hex::encode(uuid),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user