Compare commits
10 Commits
2cb1eec404
...
a110aa6376
| 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",
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"sha1",
|
"sha1",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -221,10 +221,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy 0.7.35",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -554,18 +554,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "1.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_more-impl",
|
"derive_more-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more-impl"
|
name = "derive_more-impl"
|
||||||
version = "1.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -716,7 +716,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
@@ -1003,9 +1015,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.161"
|
version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@@ -1042,9 +1054,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
@@ -1082,7 +1094,7 @@ dependencies = [
|
|||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1124,10 +1136,11 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "papaya"
|
name = "papaya"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ce63bf9dca3eab259cffd421f05661b3386aee36276f5aed9f71450b98f5c5c"
|
checksum = "dc7c76487f7eaa00a0fc1d7f88dc6b295aec478d11b0fc79f857b62c2874124c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
"seize",
|
"seize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1226,7 +1239,7 @@ version = "0.2.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"zerocopy 0.7.35",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1250,9 +1263,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.13.4"
|
version = "0.13.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive",
|
||||||
@@ -1281,9 +1294,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-derive"
|
name = "prost-derive"
|
||||||
version = "0.13.4"
|
version = "0.13.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -1317,8 +1330,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"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]]
|
[[package]]
|
||||||
@@ -1328,7 +1352,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"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]]
|
[[package]]
|
||||||
@@ -1337,7 +1371,17 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
@@ -1468,9 +1512,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.134"
|
version = "1.0.138"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
|
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1498,13 +1542,13 @@ dependencies = [
|
|||||||
"actix-ws",
|
"actix-ws",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more 1.0.0",
|
"derive_more 2.0.1",
|
||||||
"fern",
|
"fern",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"papaya",
|
"papaya",
|
||||||
"prost",
|
"prost",
|
||||||
"rand",
|
"rand 0.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -1615,18 +1659,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1681,9 +1725,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.43.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1699,9 +1743,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1787,7 +1831,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand",
|
"rand 0.8.5",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1919,6 +1963,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
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]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.95"
|
version = "0.2.95"
|
||||||
@@ -2065,6 +2118,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
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]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.35"
|
version = "0.7.35"
|
||||||
@@ -2072,7 +2134,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"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]]
|
[[package]]
|
||||||
@@ -2086,6 +2157,17 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
server = { path = "../../server" }
|
server = { path = "../../server" }
|
||||||
tonic = "0.12.3"
|
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"
|
chrono = "0.4.39"
|
||||||
tokio-util = "0.7.13"
|
tokio-util = "0.7.13"
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
|
|||||||
@@ -13,25 +13,25 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sass": "^1.80.3",
|
"sass": "^1.85.0",
|
||||||
"sass-loader": "^16.0.2",
|
"sass-loader": "^16.0.5",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.16.11",
|
"@types/node": "^22.13.4",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
"@vue/eslint-config-prettier": "^10.0.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.0.1",
|
"@vue/eslint-config-typescript": "^14.4.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.20.1",
|
||||||
"eslint-plugin-vue": "^9.29.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^7.0.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.5.1",
|
||||||
"typescript": "~5.5.4",
|
"typescript": "~5.7.3",
|
||||||
"vite": "^5.4.8",
|
"vite": "^6.1.0",
|
||||||
"vue-tsc": "^2.1.6"
|
"vue-tsc": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
import { useWebsocket, WEBSOCKET_SYMBOL } from '@/composables/websocket';
|
||||||
|
import { provide } from 'vue';
|
||||||
|
|
||||||
|
const websocket = useWebsocket();
|
||||||
|
provide(WEBSOCKET_SYMBOL, websocket);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 {
|
body {
|
||||||
color: variables.$text-color;
|
color: variables.$text-color;
|
||||||
font-size: variables.$normal-text-size;
|
font-size: variables.$normal-text-size;
|
||||||
|
font-family: variables.$text-font;
|
||||||
background-color: variables.$background-color;
|
background-color: variables.$background-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: stretch;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
min-height: 100vh;
|
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 {
|
polyline {
|
||||||
@@ -26,3 +51,11 @@ polyline {
|
|||||||
#{--indexed-color}: list.nth(variables.$colors, $i);
|
#{--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;
|
$text-color: $gray-1;
|
||||||
$background-color: $gray-7;
|
$background-color: $gray-7;
|
||||||
|
$light2-background-color: color.adjust($background-color, $lightness: 10%);
|
||||||
$light-background-color: color.adjust($background-color, $lightness: 5%);
|
$light-background-color: color.adjust($background-color, $lightness: 5%);
|
||||||
$dark-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;
|
$cursor-tick: $gray-0;
|
||||||
$time-tick: $gray-1;
|
$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">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
import CopyableDynamicSpan from '@/components/CopyableDynamicSpan.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: number;
|
value: number;
|
||||||
max_width: number;
|
max_width: number;
|
||||||
|
copyable?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update', value: string): void;
|
(e: 'update', value: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const copyable = computed(() => {
|
||||||
|
return props.copyable || false;
|
||||||
|
});
|
||||||
|
|
||||||
const display_value = computed(() => {
|
const display_value = computed(() => {
|
||||||
if (props.value == 0) {
|
if (props.value == 0) {
|
||||||
return '0';
|
return '0';
|
||||||
@@ -61,7 +67,12 @@ watch([display_value], ([display_str]) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<template v-if="copyable">
|
||||||
|
<CopyableDynamicSpan :value="display_value"></CopyableDynamicSpan>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
{{ display_value }}
|
{{ display_value }}
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<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 { useNow } from '@/composables/ticker';
|
||||||
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
||||||
import TimeText from '@/components/TimeText.vue';
|
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<{
|
const props = defineProps<{
|
||||||
duration?: number;
|
initial_duration?: number;
|
||||||
utc?: boolean;
|
utc?: boolean;
|
||||||
left_axis?: boolean;
|
left_axis?: boolean;
|
||||||
right_axis?: boolean;
|
right_axis?: boolean;
|
||||||
@@ -28,21 +33,27 @@ const props = defineProps<{
|
|||||||
const divRef = useTemplateRef<HTMLDivElement>('graph-div');
|
const divRef = useTemplateRef<HTMLDivElement>('graph-div');
|
||||||
|
|
||||||
const width = ref(0);
|
const width = ref(0);
|
||||||
const height = ref(0);
|
const raw_height = ref(0);
|
||||||
|
|
||||||
const controls_height = 32;
|
const controls_height = 32;
|
||||||
|
const min_time_label_separation = 250;
|
||||||
|
|
||||||
const resize_observer = new ResizeObserver((elements) => {
|
const resize_observer = new ResizeObserver((elements) => {
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (element.target == divRef.value) {
|
if (element.target == divRef.value) {
|
||||||
width.value = element.contentBoxSize[0].inlineSize;
|
width.value = element.contentBoxSize[0].inlineSize;
|
||||||
height.value =
|
raw_height.value = element.contentBoxSize[0].blockSize;
|
||||||
element.contentBoxSize[0].blockSize -
|
|
||||||
(props.include_controls ? controls_height : 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const height = computed(() => {
|
||||||
|
return Math.max(
|
||||||
|
raw_height.value - (props.include_controls ? controls_height : 0),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watch([divRef], ([divRef]) => {
|
watch([divRef], ([divRef]) => {
|
||||||
if (divRef) {
|
if (divRef) {
|
||||||
resize_observer.observe(divRef);
|
resize_observer.observe(divRef);
|
||||||
@@ -55,13 +66,10 @@ onUnmounted(() => {
|
|||||||
resize_observer.disconnect();
|
resize_observer.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
const now = useNow(33);
|
const update_ms = 33;
|
||||||
const window_duration = computed(() => {
|
|
||||||
if (props.duration) {
|
const now = useNow(update_ms);
|
||||||
return props.duration;
|
const window_duration = ref(props.initial_duration || 10 * 1000);
|
||||||
}
|
|
||||||
return 10 * 1000; // 10 seconds
|
|
||||||
});
|
|
||||||
|
|
||||||
const time_lines = [
|
const time_lines = [
|
||||||
1, // 1ms
|
1, // 1ms
|
||||||
@@ -79,6 +87,7 @@ const time_lines = [
|
|||||||
10000, // 10s
|
10000, // 10s
|
||||||
30000, // 30s
|
30000, // 30s
|
||||||
60000, // 1m
|
60000, // 1m
|
||||||
|
150000, // 2.5m
|
||||||
300000, // 5m
|
300000, // 5m
|
||||||
6000000, // 10m
|
6000000, // 10m
|
||||||
18000000, // 30m
|
18000000, // 30m
|
||||||
@@ -91,7 +100,7 @@ const time_lines = [
|
|||||||
1728000000, // 2d
|
1728000000, // 2d
|
||||||
6048000000, // 1w
|
6048000000, // 1w
|
||||||
];
|
];
|
||||||
time_lines.reverse();
|
// time_lines.reverse();
|
||||||
const text_offset = computed(() => 5);
|
const text_offset = computed(() => 5);
|
||||||
|
|
||||||
const legend_width = 160;
|
const legend_width = 160;
|
||||||
@@ -129,6 +138,10 @@ watch([now], ([now_value]) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDocumentShown(() => {
|
||||||
|
fetch_history.value++;
|
||||||
|
});
|
||||||
|
|
||||||
const max_x_text = computed({
|
const max_x_text = computed({
|
||||||
// getter
|
// getter
|
||||||
get() {
|
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) => {
|
const x_map = (x: number) => {
|
||||||
return (
|
return (
|
||||||
@@ -178,16 +203,25 @@ const legend_x_stride = computed(() => 0);
|
|||||||
const legend_y_stride = computed(() => 16);
|
const legend_y_stride = computed(() => 16);
|
||||||
const legend_width_output = computed(() => legend_width - 8);
|
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 line_duration = computed(() => {
|
||||||
|
const width_px = graph_width.value;
|
||||||
const diff_x = max_x.value - min_x.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 lines = computed(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (
|
for (
|
||||||
let i = Math.ceil(max_x.value / line_duration.value);
|
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--
|
i--
|
||||||
) {
|
) {
|
||||||
const x = i * line_duration.value;
|
const x = i * line_duration.value;
|
||||||
@@ -200,13 +234,9 @@ const mouse_x = ref<number | null>(null);
|
|||||||
|
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
if (props.cursor) {
|
if (props.cursor) {
|
||||||
const new_x =
|
mouse_x.value =
|
||||||
event.clientX -
|
event.clientX -
|
||||||
(event.currentTarget as Element).getBoundingClientRect().left;
|
(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);
|
const t = inv_x_map(mouse_x.value);
|
||||||
if (t < min_x.value || t > max_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 null;
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
@@ -229,7 +257,7 @@ const mouse_t = computed(() => {
|
|||||||
|
|
||||||
const show_data_at_time = computed(() => {
|
const show_data_at_time = computed(() => {
|
||||||
if (mouse_t.value === null) {
|
if (mouse_t.value === null) {
|
||||||
return max_x.value;
|
return max_x.value + update_ms;
|
||||||
} else {
|
} else {
|
||||||
return mouse_t.value;
|
return mouse_t.value;
|
||||||
}
|
}
|
||||||
@@ -237,14 +265,19 @@ const show_data_at_time = computed(() => {
|
|||||||
|
|
||||||
const should_fade = ref(false);
|
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, {
|
provide<GraphData>(GRAPH_DATA, {
|
||||||
border_top: border_top,
|
border_top: border_top,
|
||||||
min_x: min_x,
|
min_x: min_x,
|
||||||
max_x: max_x,
|
max_x: max_x,
|
||||||
|
max_temporal_resolution: max_temporal_resolution,
|
||||||
live: live,
|
live: live,
|
||||||
fetch_history: fetch_history,
|
fetch_history: fetch_history,
|
||||||
width: () =>
|
width: graph_width,
|
||||||
Math.max(width.value - border_left.value - border_right.value, 0),
|
|
||||||
height: () =>
|
height: () =>
|
||||||
Math.max(height.value - border_top.value - border_bottom.value, 0),
|
Math.max(height.value - border_top.value - border_bottom.value, 0),
|
||||||
x_map: x_map,
|
x_map: x_map,
|
||||||
@@ -269,6 +302,14 @@ provide<GraphData>(GRAPH_DATA, {
|
|||||||
:style="`height: ${controls_height}px`"
|
:style="`height: ${controls_height}px`"
|
||||||
>
|
>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
:size="15"
|
||||||
|
v-model="duration_text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -335,6 +376,7 @@ provide<GraphData>(GRAPH_DATA, {
|
|||||||
:timestamp="tick"
|
:timestamp="tick"
|
||||||
:utc="props.utc"
|
:utc="props.utc"
|
||||||
:show_millis="line_duration < 1000"
|
:show_millis="line_duration < 1000"
|
||||||
|
:key="tick"
|
||||||
></TimeText>
|
></TimeText>
|
||||||
</template>
|
</template>
|
||||||
</g>
|
</g>
|
||||||
@@ -393,6 +435,7 @@ provide<GraphData>(GRAPH_DATA, {
|
|||||||
div.full-size {
|
div.full-size {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.controls-header {
|
div.controls-header {
|
||||||
@@ -401,11 +444,11 @@ div.controls-header {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
gap: 1em 0;
|
gap: 0 1em;
|
||||||
margin: 0 1em 0 1em;
|
margin: 0 1em 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.controls-header > div.grow {
|
svg.graph {
|
||||||
flex-grow: 1;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</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 marker_radius = 3;
|
||||||
|
|
||||||
const text_offset = computed(() => 10);
|
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 graph_data = inject<GraphData>(GRAPH_DATA)!;
|
||||||
const axis_data = inject<AxisData>(AXIS_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 { data: telemetry_data } = useTelemetry(() => props.data);
|
||||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||||
const value = websocket.value.listen_to_telemetry(
|
const value = websocket.value.listen_to_telemetry(
|
||||||
telemetry_data,
|
telemetry_data,
|
||||||
min_sep,
|
live_min_sep,
|
||||||
graph_data.live,
|
graph_data.live,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,7 +103,7 @@ watch([value], ([val]) => {
|
|||||||
x: val_t,
|
x: val_t,
|
||||||
y: item_val,
|
y: item_val,
|
||||||
} as Point;
|
} as Point;
|
||||||
memo.value.insert(new_item, props.minimum_separation);
|
memo.value.insert(new_item, data_min_sep.value);
|
||||||
if (item_val < min.value) {
|
if (item_val < min.value) {
|
||||||
min.value = item_val;
|
min.value = item_val;
|
||||||
}
|
}
|
||||||
@@ -118,7 +126,7 @@ watch(
|
|||||||
const min_x = new Date(toValue(graph_data.min_x));
|
const min_x = new Date(toValue(graph_data.min_x));
|
||||||
const max_x = new Date(toValue(graph_data.max_x));
|
const max_x = new Date(toValue(graph_data.max_x));
|
||||||
const res = await fetch(
|
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[];
|
const response = (await res.json()) as TelemetryDataItem[];
|
||||||
for (const data_item of response) {
|
for (const data_item of response) {
|
||||||
@@ -136,9 +144,7 @@ watch(
|
|||||||
max.value = item_val;
|
max.value = item_val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
memo.value.reduce_to_maximum_separation(
|
memo.value.reduce_to_maximum_separation(data_min_sep.value);
|
||||||
props.minimum_separation || 0,
|
|
||||||
);
|
|
||||||
triggerRef(memo);
|
triggerRef(memo);
|
||||||
debounced_recompute();
|
debounced_recompute();
|
||||||
recompute_bounds.value++;
|
recompute_bounds.value++;
|
||||||
@@ -383,7 +389,7 @@ function onMouseExit(event: MouseEvent) {
|
|||||||
</g>
|
</g>
|
||||||
<ValueLabel
|
<ValueLabel
|
||||||
v-if="current_data_point"
|
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"
|
:x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset"
|
||||||
:y="axis_data.y_map(current_data_point.y)"
|
:y="axis_data.y_map(current_data_point.y)"
|
||||||
:value="current_data_point.y"
|
:value="current_data_point.y"
|
||||||
@@ -438,6 +444,11 @@ function onMouseExit(event: MouseEvent) {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<span>{{ telemetry_data?.data_type }}</span>
|
<span>{{ telemetry_data?.data_type }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span>{{
|
||||||
|
current_data_point?.y || 'Missing Data'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -462,6 +473,10 @@ function onMouseExit(event: MouseEvent) {
|
|||||||
opacity: 25%;
|
opacity: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade .fade_other_selected.label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fade .no-fade .fade_other_selected {
|
.fade .no-fade .fade_other_selected {
|
||||||
opacity: 100%;
|
opacity: 100%;
|
||||||
}
|
}
|
||||||
@@ -514,21 +529,8 @@ rect.legend:has(~ .legend:hover) {
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.column {
|
div.column,
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-content: flex-start;
|
|
||||||
gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.row {
|
div.row {
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
align-content: center;
|
|
||||||
gap: 0.25em;
|
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) {
|
function update_for_element(element: Element) {
|
||||||
top.value = element.getBoundingClientRect().top + window.scrollY;
|
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;
|
width.value = element.getBoundingClientRect().width;
|
||||||
height.value = element.getBoundingClientRect().height;
|
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;
|
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>) {
|
export function useTelemetry(name: MaybeRefOrGetter<string>) {
|
||||||
const data = ref<TelemetryDefinition | null>(null);
|
const data = ref<TelemetryDefinition | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { TelemetryDefinition } from '@/composables/telemetry';
|
import type { TelemetryDefinition } from '@/composables/telemetry';
|
||||||
|
import { onDocumentVisibilityChange } from '@/composables/document.ts';
|
||||||
|
|
||||||
export interface TelemetryDataItem {
|
export interface TelemetryDataItem {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -27,12 +28,14 @@ export class WebsocketHandle {
|
|||||||
websocket: WebSocket | null;
|
websocket: WebSocket | null;
|
||||||
should_be_connected: boolean;
|
should_be_connected: boolean;
|
||||||
connected: Ref<boolean>;
|
connected: Ref<boolean>;
|
||||||
|
enabled: Ref<boolean>;
|
||||||
on_telem_value: Map<string, Array<(value: TelemetryDataItem) => void>>;
|
on_telem_value: Map<string, Array<(value: TelemetryDataItem) => void>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.websocket = null;
|
this.websocket = null;
|
||||||
this.should_be_connected = false;
|
this.should_be_connected = false;
|
||||||
this.connected = ref(false);
|
this.connected = ref(false);
|
||||||
|
this.enabled = ref(true);
|
||||||
this.on_telem_value = new Map();
|
this.on_telem_value = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +122,9 @@ export class WebsocketHandle {
|
|||||||
const is_live = computed(() => toValue(live));
|
const is_live = computed(() => toValue(live));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[uuid, this.connected, minimum_separation, is_live],
|
[uuid, this.connected, this.enabled, minimum_separation, is_live],
|
||||||
([uuid_value, connected, min_sep, live_value]) => {
|
([uuid_value, connected, enabled, min_sep, live_value]) => {
|
||||||
if (connected && uuid_value && live_value) {
|
if (connected && enabled && uuid_value && live_value) {
|
||||||
this.websocket?.send(
|
this.websocket?.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
RegisterTlmListener: {
|
RegisterTlmListener: {
|
||||||
@@ -160,6 +163,14 @@ export class WebsocketHandle {
|
|||||||
|
|
||||||
return value_result;
|
return value_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.enabled.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
this.enabled.value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WEBSOCKET_SYMBOL = Symbol();
|
export const WEBSOCKET_SYMBOL = Symbol();
|
||||||
@@ -170,6 +181,13 @@ export function useWebsocket() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handle.value.connect();
|
handle.value.connect();
|
||||||
});
|
});
|
||||||
|
onDocumentVisibilityChange((visible) => {
|
||||||
|
if (visible) {
|
||||||
|
handle.value.resume();
|
||||||
|
} else {
|
||||||
|
handle.value.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
handle.value.disconnect();
|
handle.value.disconnect();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
// This function is slow
|
// 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 year = utc ? date.getUTCFullYear() : date.getFullYear();
|
||||||
const month = (
|
const month = (
|
||||||
(utc ? date.getMonth() : date.getMonth()) + 1
|
(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' : ''}`;
|
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>;
|
border_top: MaybeRefOrGetter<number>;
|
||||||
min_x: MaybeRefOrGetter<number>;
|
min_x: MaybeRefOrGetter<number>;
|
||||||
max_x: MaybeRefOrGetter<number>;
|
max_x: MaybeRefOrGetter<number>;
|
||||||
|
max_temporal_resolution: MaybeRefOrGetter<number>;
|
||||||
live: MaybeRefOrGetter<boolean>;
|
live: MaybeRefOrGetter<boolean>;
|
||||||
fetch_history: MaybeRefOrGetter<number>;
|
fetch_history: MaybeRefOrGetter<number>;
|
||||||
width: MaybeRefOrGetter<number>;
|
width: MaybeRefOrGetter<number>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import './assets/main.scss';
|
import './assets/main.scss';
|
||||||
|
import './assets/layout.scss';
|
||||||
|
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.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: '/',
|
path: '/',
|
||||||
name: 'home',
|
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]
|
[dependencies]
|
||||||
fern = "0.7.1"
|
fern = "0.7.1"
|
||||||
log = "0.4.22"
|
log = "0.4.25"
|
||||||
prost = "0.13.4"
|
prost = "0.13.5"
|
||||||
rand = "0.8.5"
|
rand = "0.9.0"
|
||||||
tonic = { version = "0.12.3" }
|
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"
|
chrono = "0.4.39"
|
||||||
actix-web = "4.9.0"
|
actix-web = "4.9.0"
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
tokio-util = "0.7.13"
|
tokio-util = "0.7.13"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
serde_json = "1.0.134"
|
serde_json = "1.0.138"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
papaya = "0.1.7"
|
papaya = "0.1.8"
|
||||||
thiserror = "2.0.9"
|
thiserror = "2.0.11"
|
||||||
derive_more = { version = "1.0.0", features = ["from"] }
|
derive_more = { version = "2.0.1", features = ["from"] }
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.95"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ async fn get_tlm_definition(
|
|||||||
Ok(web::Json(data.definition.clone()))
|
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)]
|
#[derive(Deserialize)]
|
||||||
struct HistoryQuery {
|
struct HistoryQuery {
|
||||||
from: String,
|
from: String,
|
||||||
@@ -71,5 +79,7 @@ async fn get_tlm_history(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_api(cfg: &mut web::ServiceConfig) {
|
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::{
|
||||||
use crate::http::websocket::request::{RegisterTlmListenerRequest, UnregisterTlmListenerRequest, WebsocketRequest};
|
RegisterTlmListenerRequest, UnregisterTlmListenerRequest, WebsocketRequest,
|
||||||
|
};
|
||||||
use crate::http::websocket::response::{TlmValueResponse, WebsocketResponse};
|
use crate::http::websocket::response::{TlmValueResponse, WebsocketResponse};
|
||||||
use crate::telemetry::management_service::TelemetryManagementService;
|
use crate::telemetry::management_service::TelemetryManagementService;
|
||||||
use actix_web::{rt, web, HttpRequest, HttpResponse};
|
use actix_web::{rt, web, HttpRequest, HttpResponse};
|
||||||
use actix_ws::{AggregatedMessage, ProtocolError, Session};
|
use actix_ws::{AggregatedMessage, ProtocolError, Session};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::select;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tokio::time::{sleep, Instant};
|
use tokio::time::{sleep_until, Instant};
|
||||||
use tokio::{pin, select};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tonic::codegen::tokio_stream::StreamExt;
|
use tonic::codegen::tokio_stream::StreamExt;
|
||||||
|
|
||||||
@@ -32,48 +34,29 @@ fn handle_register_tlm_listener(
|
|||||||
let mut rx = tlm_data.data.subscribe();
|
let mut rx = tlm_data.data.subscribe();
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
rt::spawn(async move {
|
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 {
|
loop {
|
||||||
select! {
|
let now = select! {
|
||||||
_ = tx.closed() => {
|
biased;
|
||||||
break;
|
_ = tx.closed() => { break; }
|
||||||
}
|
_ = token.cancelled() => { break; }
|
||||||
_ = token.cancelled() => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(_) = rx.changed() => {
|
Ok(_) = rx.changed() => {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let value = {
|
let value = {
|
||||||
let ref_val = rx.borrow_and_update();
|
let ref_val = rx.borrow_and_update();
|
||||||
ref_val.clone()
|
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 {
|
let _ = tx.send(TlmValueResponse {
|
||||||
uuid: request.uuid.clone(),
|
uuid: request.uuid.clone(),
|
||||||
value,
|
value,
|
||||||
}.into()).await;
|
}.into()).await;
|
||||||
|
now
|
||||||
}
|
}
|
||||||
() = &mut sleep => {
|
};
|
||||||
if let Some(value) = last_value {
|
select! {
|
||||||
let _ = tx.send(TlmValueResponse {
|
biased;
|
||||||
uuid: request.uuid.clone(),
|
_ = tx.closed() => { break; }
|
||||||
value: Some(value),
|
_ = token.cancelled() => { break; }
|
||||||
}.into()).await;
|
_ = sleep_until(now + minimum_separation) => {}
|
||||||
}
|
|
||||||
last_value = None;
|
|
||||||
let now = Instant::now();
|
|
||||||
last_sent_at = now;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -98,10 +81,10 @@ async fn handle_websocket_message(
|
|||||||
match request {
|
match request {
|
||||||
WebsocketRequest::RegisterTlmListener(request) => {
|
WebsocketRequest::RegisterTlmListener(request) => {
|
||||||
handle_register_tlm_listener(data, request, tx, tlm_listeners)
|
handle_register_tlm_listener(data, request, tx, tlm_listeners)
|
||||||
},
|
}
|
||||||
WebsocketRequest::UnregisterTlmListener(request) => {
|
WebsocketRequest::UnregisterTlmListener(request) => {
|
||||||
handle_unregister_tlm_listener(request, tlm_listeners)
|
handle_unregister_tlm_listener(request, tlm_listeners)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
use std::fs::File;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
pub trait FileWriteableType {
|
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 {
|
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<()>;
|
fn write_data<T: FileWriteableType>(&mut self, data: T) -> io::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ReadExt {
|
||||||
fn read_data<T: FileReadableType>(&mut self) -> io::Result<T>;
|
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<()> {
|
fn write_data<T: FileWriteableType>(&mut self, data: T) -> io::Result<()> {
|
||||||
data.write_to_file(self)
|
data.write_to_file(self)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> ReadExt for R {
|
||||||
fn read_data<T: FileReadableType>(&mut self) -> io::Result<T> {
|
fn read_data<T: FileReadableType>(&mut self) -> io::Result<T> {
|
||||||
T::read_from_file(self)
|
T::read_from_file(self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use crate::serialization::file_ext::{FileReadableType, FileWriteableType};
|
use crate::serialization::file_ext::{FileReadableType, FileWriteableType};
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
macro_rules! primitive_write_read {
|
macro_rules! primitive_write_read {
|
||||||
( $primitive:ty, $length:expr ) => {
|
( $primitive:ty, $length:expr ) => {
|
||||||
impl FileWriteableType for $primitive {
|
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())
|
file.write_all(&self.to_be_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FileReadableType for $primitive {
|
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];
|
let mut buffer = [0u8; $length];
|
||||||
file.read_exact(&mut buffer)?;
|
file.read_exact(&mut buffer)?;
|
||||||
Ok(Self::from_be_bytes(buffer))
|
Ok(Self::from_be_bytes(buffer))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::core::TelemetryDataType;
|
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::TelemetryData;
|
||||||
use crate::telemetry::data_item::TelemetryDataItem;
|
use crate::telemetry::data_item::TelemetryDataItem;
|
||||||
use crate::telemetry::data_value::TelemetryDataValue;
|
use crate::telemetry::data_value::TelemetryDataValue;
|
||||||
@@ -10,7 +10,7 @@ use log::{error, info};
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::{fs, path};
|
use std::{fs, path};
|
||||||
@@ -137,7 +137,8 @@ struct HistorySegmentFile {
|
|||||||
start: DateTime<Utc>,
|
start: DateTime<Utc>,
|
||||||
end: DateTime<Utc>,
|
end: DateTime<Utc>,
|
||||||
length: u64,
|
length: u64,
|
||||||
file: File,
|
file: BufReader<File>,
|
||||||
|
file_position: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistorySegmentFile {
|
impl HistorySegmentFile {
|
||||||
@@ -158,31 +159,16 @@ impl HistorySegmentFile {
|
|||||||
segment.start.to_rfc3339_opts(SecondsFormat::Secs, true)
|
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 {
|
let utc_offset_start = segment.start - DateTime::UNIX_EPOCH;
|
||||||
start: segment.start,
|
let utc_offset_end = segment.end - DateTime::UNIX_EPOCH;
|
||||||
end: segment.end,
|
|
||||||
length: 0,
|
|
||||||
file,
|
|
||||||
};
|
|
||||||
|
|
||||||
let utc_offset_start = result.start - DateTime::UNIX_EPOCH;
|
|
||||||
let utc_offset_end = result.end - DateTime::UNIX_EPOCH;
|
|
||||||
|
|
||||||
// Write the segment bounds
|
// Write the segment bounds
|
||||||
result
|
file.write_data::<i64>(utc_offset_start.num_seconds())?;
|
||||||
.file
|
file.write_data::<i32>(utc_offset_start.subsec_nanos())?;
|
||||||
.write_data::<i64>(utc_offset_start.num_seconds())?;
|
file.write_data::<i64>(utc_offset_end.num_seconds())?;
|
||||||
result
|
file.write_data::<i32>(utc_offset_end.subsec_nanos())?;
|
||||||
.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())?;
|
|
||||||
|
|
||||||
let data = segment.data.get_mut().unwrap_or_else(|err| {
|
let data = segment.data.get_mut().unwrap_or_else(|err| {
|
||||||
error!(
|
error!(
|
||||||
@@ -197,26 +183,35 @@ impl HistorySegmentFile {
|
|||||||
"Invalid Segment Cannot Be Saved to Disk"
|
"Invalid Segment Cannot Be Saved to Disk"
|
||||||
);
|
);
|
||||||
|
|
||||||
result.length = data.timestamps.len() as u64;
|
let length = data.timestamps.len() as u64;
|
||||||
result.file.write_data::<u64>(result.length)?;
|
file.write_data::<u64>(length)?;
|
||||||
|
|
||||||
// Write all the timestamps
|
// Write all the timestamps
|
||||||
for timestamp in &data.timestamps {
|
for timestamp in &data.timestamps {
|
||||||
let utc_offset = *timestamp - DateTime::UNIX_EPOCH;
|
let utc_offset = *timestamp - DateTime::UNIX_EPOCH;
|
||||||
result.file.write_data::<i64>(utc_offset.num_seconds())?;
|
file.write_data::<i64>(utc_offset.num_seconds())?;
|
||||||
result.file.write_data::<i32>(utc_offset.subsec_nanos())?;
|
file.write_data::<i32>(utc_offset.subsec_nanos())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all the values
|
// Write all the values
|
||||||
for value in &data.values {
|
for value in &data.values {
|
||||||
match value {
|
match value {
|
||||||
TelemetryDataValue::Float32(value) => result.file.write_data::<f32>(*value)?,
|
TelemetryDataValue::Float32(value) => file.write_data::<f32>(*value)?,
|
||||||
TelemetryDataValue::Float64(value) => result.file.write_data::<f64>(*value)?,
|
TelemetryDataValue::Float64(value) => file.write_data::<f64>(*value)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.file.flush()?;
|
file.flush()?;
|
||||||
Ok(result)
|
|
||||||
|
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(
|
fn load_to_ram(
|
||||||
@@ -229,6 +224,7 @@ impl HistorySegmentFile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.file.seek(SeekFrom::Start(Self::HEADER_LENGTH))?;
|
self.file.seek(SeekFrom::Start(Self::HEADER_LENGTH))?;
|
||||||
|
self.file_position = Self::HEADER_LENGTH as i64;
|
||||||
for _ in 0..self.length {
|
for _ in 0..self.length {
|
||||||
segment_data.timestamps.push(self.read_date_time()?);
|
segment_data.timestamps.push(self.read_date_time()?);
|
||||||
}
|
}
|
||||||
@@ -255,7 +251,7 @@ impl HistorySegmentFile {
|
|||||||
start.to_rfc3339_opts(SecondsFormat::Secs, true)
|
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
|
// Write the segment bounds
|
||||||
let start_seconds = file.read_data::<i64>()?;
|
let start_seconds = file.read_data::<i64>()?;
|
||||||
@@ -269,11 +265,13 @@ impl HistorySegmentFile {
|
|||||||
|
|
||||||
let length = file.read_data::<u64>()?;
|
let length = file.read_data::<u64>()?;
|
||||||
|
|
||||||
|
file.seek(SeekFrom::Start(0))?;
|
||||||
Ok(HistorySegmentFile {
|
Ok(HistorySegmentFile {
|
||||||
start: DateTime::UNIX_EPOCH + start,
|
start: DateTime::UNIX_EPOCH + start,
|
||||||
end: DateTime::UNIX_EPOCH + end,
|
end: DateTime::UNIX_EPOCH + end,
|
||||||
length,
|
length,
|
||||||
file,
|
file,
|
||||||
|
file_position: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +282,8 @@ impl HistorySegmentFile {
|
|||||||
maximum_resolution: TimeDelta,
|
maximum_resolution: TimeDelta,
|
||||||
telemetry_data_type: TelemetryDataType,
|
telemetry_data_type: TelemetryDataType,
|
||||||
) -> anyhow::Result<(DateTime<Utc>, Vec<TelemetryDataItem>)> {
|
) -> anyhow::Result<(DateTime<Utc>, Vec<TelemetryDataItem>)> {
|
||||||
|
self.file_position = 0;
|
||||||
|
self.file.seek(SeekFrom::Start(0))?;
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
|
||||||
let mut next_from = from;
|
let mut next_from = from;
|
||||||
@@ -319,15 +319,17 @@ impl HistorySegmentFile {
|
|||||||
fn read_date_time(&mut self) -> anyhow::Result<DateTime<Utc>> {
|
fn read_date_time(&mut self) -> anyhow::Result<DateTime<Utc>> {
|
||||||
let seconds = self.file.read_data::<i64>()?;
|
let seconds = self.file.read_data::<i64>()?;
|
||||||
let nanos = self.file.read_data::<i32>()?;
|
let nanos = self.file.read_data::<i32>()?;
|
||||||
|
self.file_position += 8 + 4;
|
||||||
let start =
|
let start =
|
||||||
TimeDelta::new(seconds, nanos as u32).context("Failed to reconstruct TimeDelta")?;
|
TimeDelta::new(seconds, nanos as u32).context("Failed to reconstruct TimeDelta")?;
|
||||||
Ok(DateTime::UNIX_EPOCH + start)
|
Ok(DateTime::UNIX_EPOCH + start)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_date_time(&mut self, index: u64) -> anyhow::Result<DateTime<Utc>> {
|
fn get_date_time(&mut self, index: u64) -> anyhow::Result<DateTime<Utc>> {
|
||||||
self.file.seek(SeekFrom::Start(
|
let desired_position = Self::HEADER_LENGTH + index * Self::TIMESTAMP_LENGTH;
|
||||||
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()
|
self.read_date_time()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,9 +339,11 @@ impl HistorySegmentFile {
|
|||||||
) -> anyhow::Result<TelemetryDataValue> {
|
) -> anyhow::Result<TelemetryDataValue> {
|
||||||
match telemetry_data_type {
|
match telemetry_data_type {
|
||||||
TelemetryDataType::Float32 => {
|
TelemetryDataType::Float32 => {
|
||||||
|
self.file_position += 4;
|
||||||
Ok(TelemetryDataValue::Float32(self.file.read_data::<f32>()?))
|
Ok(TelemetryDataValue::Float32(self.file.read_data::<f32>()?))
|
||||||
}
|
}
|
||||||
TelemetryDataType::Float64 => {
|
TelemetryDataType::Float64 => {
|
||||||
|
self.file_position += 8;
|
||||||
Ok(TelemetryDataValue::Float64(self.file.read_data::<f64>()?))
|
Ok(TelemetryDataValue::Float64(self.file.read_data::<f64>()?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,9 +358,11 @@ impl HistorySegmentFile {
|
|||||||
TelemetryDataType::Float32 => 4,
|
TelemetryDataType::Float32 => 4,
|
||||||
TelemetryDataType::Float64 => 8,
|
TelemetryDataType::Float64 => 8,
|
||||||
};
|
};
|
||||||
self.file.seek(SeekFrom::Start(
|
let desired_position =
|
||||||
Self::HEADER_LENGTH + self.length * Self::TIMESTAMP_LENGTH + index * item_length,
|
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)
|
self.read_telemetry_item(telemetry_data_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,14 +540,31 @@ impl TelemetryHistory {
|
|||||||
maximum_resolution: TimeDelta,
|
maximum_resolution: TimeDelta,
|
||||||
telemetry_history_service: &TelemetryHistoryService,
|
telemetry_history_service: &TelemetryHistoryService,
|
||||||
) -> Vec<TelemetryDataItem> {
|
) -> Vec<TelemetryDataItem> {
|
||||||
let mut result = vec![];
|
let mut disk_result = vec![];
|
||||||
|
let mut ram_result = vec![];
|
||||||
let segments = self.segments.read().await;
|
|
||||||
|
|
||||||
let mut from = from;
|
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);
|
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
|
let start = from
|
||||||
.duration_trunc(telemetry_history_service.segment_width)
|
.duration_trunc(telemetry_history_service.segment_width)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -549,12 +572,6 @@ impl TelemetryHistory {
|
|||||||
.duration_trunc(telemetry_history_service.segment_width)
|
.duration_trunc(telemetry_history_service.segment_width)
|
||||||
.unwrap();
|
.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();
|
let mut path = telemetry_history_service.data_root_folder.clone();
|
||||||
path.push(&self.data.definition.uuid);
|
path.push(&self.data.definition.uuid);
|
||||||
|
|
||||||
@@ -568,7 +585,7 @@ impl TelemetryHistory {
|
|||||||
match disk.get(from, to, maximum_resolution, self.data.definition.data_type) {
|
match disk.get(from, to, maximum_resolution, self.data.definition.data_type) {
|
||||||
Ok((new_from, new_data)) => {
|
Ok((new_from, new_data)) => {
|
||||||
from = new_from;
|
from = new_from;
|
||||||
result.extend(new_data);
|
disk_result.extend(new_data);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to get from disk segment: {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() {
|
for i in 0..segments.len() {
|
||||||
let (new_from, new_data) = segments[i].get(from, to, maximum_resolution);
|
let (new_from, new_data) = segments[i].get(from, to, maximum_resolution);
|
||||||
from = new_from;
|
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<()> {
|
pub async fn cleanup(&self, service: &TelemetryHistoryService) -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -146,6 +146,14 @@ impl TelemetryManagementService {
|
|||||||
.cloned()
|
.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 {
|
pub fn pin(&self) -> TelemetryManagementServicePin {
|
||||||
TelemetryManagementServicePin {
|
TelemetryManagementServicePin {
|
||||||
tlm_data: self.tlm_data.pin(),
|
tlm_data: self.tlm_data.pin(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rand::RngCore;
|
|||||||
impl Uuid {
|
impl Uuid {
|
||||||
pub fn random() -> Self {
|
pub fn random() -> Self {
|
||||||
let mut uuid = [0u8; 16];
|
let mut uuid = [0u8; 16];
|
||||||
rand::thread_rng().fill_bytes(&mut uuid);
|
rand::rng().fill_bytes(&mut uuid);
|
||||||
Self {
|
Self {
|
||||||
value: hex::encode(uuid),
|
value: hex::encode(uuid),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user