Compare commits

..
Author SHA1 Message Date
mbecker20 af8410ae64 fix invalid tokens JSON 2025-08-23 17:55:43 -07:00
mbecker20 0a2ee6ea43 deploy 1.19.1-dev-14 2025-08-23 17:18:47 -07:00
mbecker20 fafae5d9d1 build multi registry configuration 2025-08-23 17:18:25 -07:00
mbecker20 009221e288 deploy 1.19.1-dev-13 2025-08-23 15:51:25 -07:00
mbecker20 b8022f279f backend for build multi registry push support 2025-08-23 15:51:01 -07:00
2ca87c51f5 Add: Server Version Mismatch Warnings & Alert System (#748)
* start 1.19.1

* deploy 1.19.1-dev-1

* feat: implement version mismatch warnings in server UI
- Replace orange warning colors with yellow for better visibility
- Add version mismatch detection that shows warnings instead of OK status
Implement responsive "VERSION MISMATCH" badge layout
- Update server dashboard to include warning counts
- Add backend version comparison logic for GetServersSummary

* feat: add warning count to server summary and update backup documentation link

* feat: add server version mismatch alert handling and update server summary invalidation logic

* fix: correct version mismatch alert config and disabled server display

- Use send_version_mismatch_alerts instead of send_unreachable_alerts
- Show 'Unknown' instead of 'Disabled' for disabled server versions
- Remove commented VersionAlert and Alerts UI components
- Update version to 1.19.0

* cleanup

* Update TypeScript types after merge

* cleanup

* cleanup

* cleanup

* Add "ServerVersionMismatch" to alert types

* Adjust color classes for warning states and revert server update invalidation logic

---------

Co-authored-by: mbecker20 <max@mogh.tech>
2025-08-23 14:29:26 -07:00
mbecker20 12601d49f4 fmt 2025-08-23 13:11:59 -07:00
mbecker20 284452e674 stack file dependency toml parsing aliases 2025-08-23 12:22:10 -07:00
mbecker20 15ed767f65 deploy 1.19.1-dev-12 2025-08-23 12:19:53 -07:00
mbecker20 9ec9f6e4a8 fix skip serializing if None 2025-08-23 12:19:33 -07:00
mbecker20 ff7bd5c96e deploy 1.19.1-dev-11 2025-08-23 12:13:45 -07:00
mbecker20 4573881c9f rename additional_files => config_files for clarity 2025-08-23 12:13:14 -07:00
mbecker20 4623f3ead2 deploy 1.19.1-dev-10 2025-08-23 12:02:52 -07:00
mbecker20 3c2bbb0b52 default additional file requires is None 2025-08-23 12:02:21 -07:00
mbecker20 b0f89a1b84 UI default file dependency None 2025-08-23 11:59:15 -07:00
mbecker20 88c0e7e2e7 deploy 1.19.1-dev-9 2025-08-23 11:54:19 -07:00
mbecker20 70af4a0b86 implement additional file dependency configuration 2025-08-23 11:53:55 -07:00
mbecker20 530b066717 deploy 1.19.1-dev-8 2025-08-23 10:53:16 -07:00
mbecker20 7fbcee5dd1 get FE compile 2025-08-23 10:53:04 -07:00
mbecker20 bfef4725be Support complex file depency action resolution 2025-08-23 10:48:07 -07:00
05a9750f79 Add Enter Key Support for Dialog Confirmations (#750)
* start 1.19.1

* deploy 1.19.1-dev-1

* Implement usePromptHotkeys for enhanced dialog interactions and UX

* Refactor usePromptHotkeys to enhance confirm button detection and improve UX

* Remove forceConfirmDialog prop from ActionWithDialog and related logic for cleaner implementation

* Add dialog descriptions to ConfirmUpdate and ActionWithDialog for better clarity and resolve warnings

* fix

* Restore forceConfirmDialog prop to ActionWithDialog for enhanced confirmation handling

* cleanup

* Remove conditional className logic from ConfirmButton

---------

Co-authored-by: mbecker20 <max@mogh.tech>
2025-08-22 18:19:15 -07:00
mbecker20 b006bef72c deploy 1.19.1-dev-7 2025-08-22 01:08:57 -07:00
mbecker20 af17636137 env file args won't double pass env file 2025-08-22 01:08:32 -07:00
mbecker20 d0571dcce0 deploy 1.19.1-dev-6 2025-08-21 17:48:48 -07:00
mbecker20 cc9a534a8a clean up SendAlert doc 2025-08-21 15:48:42 -07:00
Ravi Wolter-KrishanandGitHub 112ae60264 Update configuration.md - fix typo: "affect" -> "effect" (#747) 2025-08-21 14:50:25 -07:00
mbecker20 fbc8fb3a58 bump deps 2025-08-21 14:48:56 -07:00
Brian BradleyandGitHub ba85a5526d Add RunStackService API implementing docker compose run (#732)
* Add RunStackService API implementing `docker compose run`

* Add working Procedure configuration

* Remove `km execute run` alias. Remove redundant ``#[serde(default)]` on `Option`.

* Refactor command from `String` to `Vec<String>`

* Implement proper shell escaping
2025-08-21 14:42:47 -07:00
mbecker20 c5bc94b4d8 gen types and fix responses formatting 2025-08-21 14:42:20 -07:00
mbecker20 17f0dd4209 improve cli ergonomics 2025-08-21 14:38:41 -07:00
mbecker20 8528e4c1f7 deploy 1.19.1-dev-5 2025-08-21 14:21:44 -07:00
mbecker20 10f82cdf2f fix clippy if let string 2025-08-21 14:21:04 -07:00
mbecker20 ae7fb7f87f SendAlert via Action / CLI 2025-08-21 14:18:24 -07:00
mbecker20 78451fc3e7 server enabled actually defaults false 2025-08-21 09:17:24 -07:00
mbecker20 8b06ffb140 simple configure action args as JSON 2025-08-20 01:36:56 -07:00
mbecker20 dab9aae61b deploy 1.19.1-dev-4 2025-08-20 00:56:18 -07:00
mbecker20 7407f62cc5 add .ini 2025-08-19 23:30:38 -07:00
mbecker20 627c56dbda deploy 1.19.1-dev-3 2025-08-19 18:42:07 -07:00
mbecker20 829e8f360b gen types 2025-08-19 18:41:08 -07:00
Marcel PfennigandGitHub 38857cbc0b Enhanced Server Stats Dashboard with Performance Optimizations (#746)
* Improve the layout of server mini stats in the dashboard.

- Server stats and tags made siblings for clearer responsibilities
- Changed margin to padding
- Unreachable indicator made into an overlay of the stats

* feat: optimize dashboard server stats with lazy loading and smart server availability checks

- Add enabled prop to ServerStatsMini for conditional data fetching
- Implement server availability check (only fetch stats for Ok servers, not NotOk/Disabled)
- Prevent 500 errors by avoiding API calls to offline servers
- Increase polling interval from 10s to 15s and add 5s stale time
- Add useMemo for expensive calculations to reduce re-renders
- Add conditional overlay rendering for unreachable servers
- Only render stats when showServerStats preference is enabled

* fix: show disabled servers with overlay instead of hiding component

- Maintain consistent layout by showing disabled state overlay
- Prevent UX inconsistency where disabled servers disappeared entirely

* fix: show button height

* feat: add enhance card animations

* cleanup
2025-08-19 18:33:07 -07:00
Marcel PfennigandGitHub 5bbd5510a1 Fix: Example code blocks got interpreted as rust code, leading to compilation errors (#743) 2025-08-19 18:31:23 -07:00
mbecker20 8b1a3230c3 fix tsc 2025-08-19 15:52:58 -07:00
mbecker20 4fe84c17fb Fe support additional file language detection 2025-08-19 15:51:11 -07:00
mbecker20 420c9c0569 deploy 1.19.1-dev-2 2025-08-19 15:36:38 -07:00
mbecker20 6ee7a29f51 support stack additional files 2025-08-19 15:36:00 -07:00
mbecker20 b7dab131fa Global Auto Update rustdoc 2025-08-18 16:39:05 -07:00
mbecker20 0c30229e3e deploy 1.19.1-dev-1 2025-08-18 16:39:05 -07:00
mbecker20 bcbe75ca5d start 1.19.1 2025-08-18 16:39:05 -07:00
64 changed files with 434 additions and 3427 deletions
Generated
+71 -94
View File
@@ -118,12 +118,10 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-compression"
version = "0.4.28"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
dependencies = [
"compression-codecs",
"compression-core",
"flate2",
"futures-core",
"memchr",
@@ -269,9 +267,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.161.0"
version = "1.160.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239393d2a430bd37a7a39d8467674b816f1c864e4bfe874b01a0c9e0d49be4ff"
checksum = "4300d66be2c4e275d3a037576dacce6efaf9af24bfbd2460a848ea39299c979c"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -746,7 +744,7 @@ version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"cexpr",
"clang-sys",
"itertools 0.12.1",
@@ -771,9 +769,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.3"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
[[package]]
name = "bitvec"
@@ -863,7 +861,7 @@ dependencies = [
"getrandom 0.2.16",
"getrandom 0.3.3",
"hex",
"indexmap 2.11.0",
"indexmap 2.10.0",
"js-sys",
"once_cell",
"rand 0.9.2",
@@ -907,7 +905,7 @@ dependencies = [
[[package]]
name = "cache"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"tokio",
@@ -915,9 +913,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.34"
version = "1.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
dependencies = [
"jobserver",
"libc",
@@ -1074,38 +1072,19 @@ dependencies = [
[[package]]
name = "command"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"komodo_client",
"run_command",
"svi",
]
[[package]]
name = "compression-codecs"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905"
dependencies = [
"compression-core",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
]
[[package]]
name = "compression-core"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01"
[[package]]
name = "config"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"colored",
"indexmap 2.11.0",
"indexmap 2.10.0",
"regex",
"serde",
"serde_json",
@@ -1214,7 +1193,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"crossterm_winapi",
"parking_lot 0.12.4",
"rustix",
@@ -1342,7 +1321,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "database"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"async-compression",
@@ -1641,7 +1620,7 @@ dependencies = [
[[package]]
name = "environment"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"formatting",
@@ -1651,7 +1630,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"thiserror 2.0.16",
]
@@ -1741,7 +1720,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"serror",
]
@@ -1903,7 +1882,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"cache",
@@ -1944,7 +1923,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.11.0",
"indexmap 2.10.0",
"slab",
"tokio",
"tokio-util",
@@ -1963,7 +1942,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.3.1",
"indexmap 2.11.0",
"indexmap 2.10.0",
"slab",
"tokio",
"tokio-util",
@@ -2479,9 +2458,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
@@ -2511,7 +2490,7 @@ dependencies = [
[[package]]
name = "interpolate"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"komodo_client",
@@ -2520,11 +2499,11 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.10"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"cfg-if",
"libc",
]
@@ -2607,9 +2586,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.34"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -2642,7 +2621,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"chrono",
@@ -2667,7 +2646,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2679,7 +2658,7 @@ dependencies = [
"derive_variants",
"envy",
"futures",
"indexmap 2.11.0",
"indexmap 2.10.0",
"ipnetwork",
"mongo_indexed",
"partial_derive2",
@@ -2696,13 +2675,12 @@ dependencies = [
"tokio-util",
"tracing",
"typeshare",
"urlencoding",
"uuid",
]
[[package]]
name = "komodo_core"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"arc-swap",
@@ -2734,7 +2712,7 @@ dependencies = [
"git",
"hex",
"hmac",
"indexmap 2.11.0",
"indexmap 2.10.0",
"interpolate",
"jsonwebtoken",
"komodo_client",
@@ -2772,7 +2750,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"arc-swap",
@@ -2894,7 +2872,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"komodo_client",
@@ -3059,9 +3037,9 @@ dependencies = [
[[package]]
name = "mongodb"
version = "3.2.5"
version = "3.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e3788f35159bbcf461227af84711950525343b64451fdd90de4e237a0b8a13"
checksum = "d0f8c69f13acf07eae386a2974f48ffd9187ea2aba8defbea9aa34e7e272c5f3"
dependencies = [
"async-trait",
"base64 0.13.1",
@@ -3108,9 +3086,9 @@ dependencies = [
[[package]]
name = "mongodb-internal-macros"
version = "3.2.5"
version = "3.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdfb13b7c0436b3e395f4ce5f4366431b9c9db47adc4cb33afc6b5b913d44969"
checksum = "b9202de265a3a8bbb43f9fe56db27c93137d4f9fb04c093f47e9c7de0c61ac7d"
dependencies = [
"macro_magic",
"proc-macro2",
@@ -3120,9 +3098,9 @@ dependencies = [
[[package]]
name = "mungos"
version = "3.2.2"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f77d8548efe9a10b49e1c92fa719d2e4e27cc6b40936f05fbb0f6a7ac31d3c0f"
checksum = "d2cc7db433516e852091e2ede8cd755a717223a359b519b22710e56225a5d6a9"
dependencies = [
"anyhow",
"envy",
@@ -3130,7 +3108,6 @@ dependencies = [
"mongodb",
"serde",
"serde_derive",
"urlencoding",
]
[[package]]
@@ -3139,7 +3116,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
@@ -3282,7 +3259,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
]
[[package]]
@@ -3637,7 +3614,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "periphery_client"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"komodo_client",
@@ -3970,7 +3947,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
]
[[package]]
@@ -3995,9 +3972,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.2"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -4007,9 +3984,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.10"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@@ -4018,15 +3995,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.7"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
@@ -4168,7 +4145,7 @@ dependencies = [
[[package]]
name = "response"
version = "1.19.3"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"axum",
@@ -4282,7 +4259,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"errno",
"libc",
"linux-raw-sys",
@@ -4500,7 +4477,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
@@ -4513,7 +4490,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -4592,7 +4569,7 @@ version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"indexmap 2.11.0",
"indexmap 2.10.0",
"itoa",
"memchr",
"ryu",
@@ -4671,7 +4648,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.11.0",
"indexmap 2.10.0",
"schemars 0.9.0",
"schemars 1.0.4",
"serde",
@@ -4699,7 +4676,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
dependencies = [
"indexmap 2.11.0",
"indexmap 2.10.0",
"itoa",
"ryu",
"serde",
@@ -5028,7 +5005,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -5280,7 +5257,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap 2.11.0",
"indexmap 2.10.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5361,7 +5338,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.11.0",
"indexmap 2.10.0",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -5378,7 +5355,7 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
"bytes",
"futures-core",
"futures-util",
@@ -5641,9 +5618,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
version = "2.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4"
dependencies = [
"form_urlencoded",
"idna",
@@ -6265,9 +6242,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
[[package]]
name = "winreg"
@@ -6294,7 +6271,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.3",
"bitflags 2.9.2",
]
[[package]]
+7 -7
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.19.3"
version = "1.19.1-dev-14"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -43,7 +43,7 @@ derive_variants = "1.0.0"
mongo_indexed = "2.0.2"
resolver_api = "3.0.0"
toml_pretty = "1.2.0"
mungos = "3.2.2"
mungos = "3.2.1"
svi = "1.2.0"
# ASYNC
@@ -65,10 +65,9 @@ axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.11.0", features = ["serde"] }
indexmap = { version = "2.10.0", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
serde_yaml_ng = "0.10.0"
serde_json = "1.0.143"
serde_qs = "0.15.0"
@@ -113,7 +112,7 @@ sysinfo = "0.37.0"
# CLOUD
aws-config = "1.8.5"
aws-sdk-ec2 = "1.161.0"
aws-sdk-ec2 = "1.160.0"
aws-credential-types = "1.2.5"
## CRON
@@ -123,7 +122,7 @@ chrono = "0.4.41"
croner = "3.0.0"
# MISC
async-compression = { version = "0.4.28", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.27", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.1.4"
typeshare = "1.0.4"
@@ -131,6 +130,7 @@ octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.2"
regex = "1.11.1"
bytes = "1.10.1"
bson = "2.15.0"
shell-escape = "0.1.5"
+3 -3
View File
@@ -549,20 +549,20 @@ async fn poll_update_until_complete(
} else {
format!("{}/updates/{}", cli_config().host, update.id)
};
println!("Link: '{}'", link.bold());
info!("Link: '{}'", link.bold());
let client = super::komodo_client().await?;
let timer = tokio::time::Instant::now();
let update = client.poll_update_until_complete(&update.id).await?;
if update.success {
println!(
info!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION SUCCESSFUL".green(),
);
} else {
eprintln!(
warn!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION FAILED".red(),
+2 -2
View File
@@ -29,12 +29,12 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
"{level} | **{name}** ({region}) | Server version now matches core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
"{level} | **{name}** ({region}) | Version mismatch detected ⚠️\nServer: **{server_version}** | Core: **{core_version}**\n{link}"
)
}
}
+2 -2
View File
@@ -275,12 +275,12 @@ fn standard_alert_content(alert: &Alert) -> String {
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
"{level} | {name} ({region}) | Server version now matches core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}\n{link}"
)
}
}
+2 -2
View File
@@ -34,12 +34,12 @@ pub async fn send_alert(
let text = match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
"{level} | {name} ({region}) | Server version now matches core version ✅"
)
}
_ => {
format!(
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}"
)
}
};
+20 -91
View File
@@ -1,9 +1,7 @@
use std::{collections::HashSet, str::FromStr};
use std::collections::HashSet;
use anyhow::Context;
use database::mungos::mongodb::bson::{
doc, oid::ObjectId, to_bson, to_document,
};
use database::mungos::mongodb::bson::{doc, to_document};
use formatting::format_serror;
use interpolate::Interpolator;
use komodo_client::{
@@ -366,21 +364,23 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
// host before restart.
maybe_pull_stack(&stack, Some(&mut update)).await?;
let mut update =
restart_services(stack.name, Vec::new(), user).await?;
if update.success {
// Need to update 'info.deployed_contents' with the
// latest contents so next check doesn't read the same diff.
update_deployed_contents_with_latest(
&stack.id,
stack.info.remote_contents,
&mut update,
)
.await;
}
Ok(update)
// The existing update is initialized to DeployStack,
// but also has not been created on database.
// Setup a new update here.
let req = ExecuteRequest::RestartStack(RestartStack {
stack: stack.name,
services: Vec::new(),
});
let update = init_execution_update(&req, user).await?;
let ExecuteRequest::RestartStack(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs {
user: user.clone(),
update,
})
.await
}
DeployIfChangedAction::Services { deploy, restart } => {
match (deploy.is_empty(), restart.is_empty()) {
@@ -401,22 +401,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
// PullStack in order to ensure latest repo contents on the
// host before restart. Only necessary if no "deploys" (deploy already pulls stack).
maybe_pull_stack(&stack, Some(&mut update)).await?;
let mut update =
restart_services(stack.name, restart, user).await?;
if update.success {
// Need to update 'info.deployed_contents' with the
// latest contents so next check doesn't read the same diff.
update_deployed_contents_with_latest(
&stack.id,
stack.info.remote_contents,
&mut update,
)
.await;
}
Ok(update)
restart_services(stack.name, restart, user).await
}
// Only deploy
(false, true) => {
@@ -428,8 +413,6 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
"Execute Deploys",
format!("Deploying: {}", deploy.join(", "),),
);
// This already updates 'stack.info.deployed_services',
// restart doesn't require this again.
let deploy_update =
deploy_services(stack.name.clone(), deploy, user)
.await?;
@@ -513,59 +496,6 @@ async fn restart_services(
.await
}
/// This can safely be called in [DeployStackIfChanged]
/// when there are ONLY changes to config files requiring restart,
/// AFTER the restart has been successfully completed.
///
/// In the case the if changed action is not FullDeploy,
/// the only file diff possible is to config files.
/// Also note either full or service deploy will already update 'deployed_contents'
/// making this method unnecessary in those cases.
///
/// Changes to config files after restart is applied should
/// be taken as the deployed contents, otherwise next changed check
/// will restart service again for no reason.
async fn update_deployed_contents_with_latest(
id: &str,
contents: Option<Vec<StackRemoteFileContents>>,
update: &mut Update,
) {
let Some(contents) = contents else {
return;
};
let contents = contents
.into_iter()
.map(|f| FileContents {
path: f.path,
contents: f.contents,
})
.collect::<Vec<_>>();
if let Err(e) = (async {
let contents = to_bson(&contents)
.context("Failed to serialize contents to bson")?;
let id =
ObjectId::from_str(id).context("Id is not valid ObjectId")?;
db_client()
.stacks
.update_one(
doc! { "_id": id },
doc! { "$set": { "info.deployed_contents": contents } },
)
.await
.context("Failed to update stack 'deployed_contents'")?;
anyhow::Ok(())
})
.await
{
update.push_error_log(
"Update content cache",
format_serror(&e.into()),
);
update.finalize();
let _ = update_update(update.clone()).await;
}
}
enum DeployIfChangedAction {
/// Changes to any compose or env files
/// always lead to this.
@@ -1033,7 +963,6 @@ impl Resolve<ExecuteArgs> for RunStackService {
command: self.command,
no_tty: self.no_tty,
no_deps: self.no_deps,
detach: self.detach,
service_ports: self.service_ports,
env: self.env,
workdir: self.workdir,
+9 -11
View File
@@ -186,7 +186,7 @@ async fn write_dockerfile_contents_git(
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut repo_args: RepoExecutionArgs = if !build
let mut clone_args: RepoExecutionArgs = if !build
.config
.files_on_host
&& !build.config.linked_repo.is_empty()
@@ -196,8 +196,8 @@ async fn write_dockerfile_contents_git(
} else {
(&build).into()
};
let root = repo_args.unique_path(&core_config().repo_directory)?;
repo_args.destination = Some(root.display().to_string());
let root = clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(root.display().to_string());
let build_path = build
.config
@@ -220,11 +220,11 @@ async fn write_dockerfile_contents_git(
})?;
}
let access_token = if let Some(account) = &repo_args.account {
git_token(&repo_args.provider, account, |https| repo_args.https = https)
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", repo_args.provider),
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
@@ -235,7 +235,7 @@ async fn write_dockerfile_contents_git(
if !root.join(".git").exists() {
git::init_folder_as_repo(
&root,
&repo_args,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
@@ -249,11 +249,9 @@ async fn write_dockerfile_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
clone_args,
&core_config().repo_directory,
access_token,
)
@@ -300,7 +298,7 @@ async fn write_dockerfile_contents_git(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&branch,
&build.config.branch,
)
.await;
+1 -3
View File
@@ -284,8 +284,6 @@ async fn write_stack_file_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
@@ -336,7 +334,7 @@ async fn write_stack_file_contents_git(
&format!("{username}: Write Stack File"),
&root,
&file_path,
&branch,
&stack.config.branch,
)
.await;
+1 -3
View File
@@ -323,8 +323,6 @@ async fn write_sync_file_contents_git(
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
@@ -375,7 +373,7 @@ async fn write_sync_file_contents_git(
&format!("{}: Commit Resource File", args.user.username),
&root,
&resource_path.join(&file_path),
&branch,
&sync.config.branch,
)
.await;
-8
View File
@@ -220,14 +220,6 @@ impl Resolve<WriteArgs> for DeleteUser {
.delete_one(query)
.await
.context("Failed to delete user from database")?;
// Also remove user id from all user groups
if let Err(e) = db
.user_groups
.update_many(doc! {}, doc! { "$pull": { "users": &user.id } })
.await
{
warn!("Failed to remove deleted user from user groups | {e:?}");
};
Ok(user)
}
}
+2 -3
View File
@@ -187,8 +187,8 @@ impl GoogleOauthClient {
Ok(body)
} else {
let text = res.text().await.context(format!(
"method: POST | status: {status} | failed to get response text"
))?;
"method: POST | status: {status} | failed to get response text"
))?;
Err(anyhow!("method: POST | status: {status} | text: {text}"))
}
}
@@ -207,6 +207,5 @@ pub struct GoogleUser {
#[serde(rename = "sub")]
pub id: String,
pub email: String,
#[serde(default)]
pub picture: String,
}
-1
View File
@@ -250,7 +250,6 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.logging.opentelemetry_service_name),
},
pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config),
unsafe_unsanitized_startup_config: env.komodo_unsafe_unsanitized_startup_config.unwrap_or(config.unsafe_unsanitized_startup_config),
internet_interface: env.komodo_internet_interface.unwrap_or(config.internet_interface),
ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),
ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),
+4 -8
View File
@@ -45,14 +45,10 @@ async fn app() -> anyhow::Result<()> {
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
match (
config.pretty_startup_config,
config.unsafe_unsanitized_startup_config,
) {
(true, true) => info!("{:#?}", config),
(true, false) => info!("{:#?}", config.sanitized()),
(false, true) => info!("{:?}", config),
(false, false) => info!("{:?}", config.sanitized()),
if core_config().pretty_startup_config {
info!("{:#?}", config.sanitized());
} else {
info!("{:?}", config.sanitized());
}
// Init jwt client to crash on failure
-1
View File
@@ -21,7 +21,6 @@ pub async fn record_server_stats(ts: i64) {
ts,
sid: status.id.clone(),
cpu_perc: stats.cpu_perc,
load_average: stats.load_average.clone(),
mem_total_gb: stats.mem_total_gb,
mem_used_gb: stats.mem_used_gb,
disk_total_gb,
+10 -14
View File
@@ -81,7 +81,7 @@ pub async fn update_deployment_cache(
// If image already has tag, leave it,
// otherwise default the tag to latest
if image.contains(':') {
image.to_string()
image
} else {
format!("{image}:latest")
}
@@ -92,9 +92,6 @@ pub async fn update_deployment_cache(
..
}) = &container
{
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image name and find available updates.
let image = image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
@@ -253,21 +250,20 @@ pub async fn update_stack_cache(
}
}.is_match(&container.name)
}).cloned();
// If image already has tag, leave it,
// otherwise default the tag to latest
let image = image.clone();
let image = if image.contains(':') {
image.to_string()
image
} else {
format!("{image}:latest")
image + ":latest"
};
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image tag and find available update.
let image =
image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
+3 -29
View File
@@ -853,15 +853,9 @@ pub async fn delete<T: KomodoResource>(
);
update.push_simple_log("Deleted Toml", toml);
tokio::join!(
async {
if let Err(e) = T::post_delete(&resource, &mut update).await {
update
.push_error_log("post delete", format_serror(&e.into()));
}
},
delete_from_alerters::<T>(&resource.id)
);
if let Err(e) = T::post_delete(&resource, &mut update).await {
update.push_error_log("post delete", format_serror(&e.into()));
}
refresh_all_resources_cache().await;
@@ -871,26 +865,6 @@ pub async fn delete<T: KomodoResource>(
Ok(resource)
}
async fn delete_from_alerters<T: KomodoResource>(id: &str) {
let target_bson = doc! {
"type": T::resource_type().as_ref(),
"id": id,
};
if let Err(e) = db_client()
.alerters
.update_many(Document::new(), doc! {
"$pull": {
"config.resources": &target_bson,
"config.except_resources": target_bson,
}
})
.await
.context("Failed to clear deleted resource from alerter whitelist / blacklist")
{
warn!("{e:#}");
}
}
// =======
#[instrument(level = "debug")]
-1
View File
@@ -300,7 +300,6 @@ async fn get_repo_state_from_db(id: &str) -> RepoState {
"$or": [
{ "operation": "CloneRepo" },
{ "operation": "PullRepo" },
{ "operation": "BuildRepo" },
],
})
.with_options(
+1 -1
View File
@@ -242,7 +242,7 @@ async fn ensure_first_server_and_builder() {
return;
};
if let Err(e) = (CreateBuilder {
name: config.first_server_name.clone(),
name: String::from("Local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
-4
View File
@@ -712,7 +712,6 @@ impl Resolve<super::Args> for ComposeRun {
command,
no_tty,
no_deps,
detach,
service_ports,
env,
workdir,
@@ -784,9 +783,6 @@ impl Resolve<super::Args> for ComposeRun {
}
let mut run_flags = String::from(" --rm");
if detach.unwrap_or_default() {
run_flags.push_str(" -d");
}
if no_tty.unwrap_or_default() {
run_flags.push_str(" --no-tty");
}
+1 -1
View File
@@ -347,7 +347,7 @@ async fn execute_command_on_terminal(
);
let full_command = format!(
"printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$?; printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n"
"printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$? printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n"
);
terminal
+1 -2
View File
@@ -43,9 +43,8 @@ pub async fn validate_files(
"Validate Files",
format_serror(
&anyhow!(
"Missing files: {}", res.missing_files.join(", ")
"Ensure the run_directory and all file paths are correct."
)
.context("Ensure the run_directory and all file paths are correct.")
.context("A file doesn't exist after writing stack.")
.into(),
),
+1 -9
View File
@@ -2,8 +2,7 @@ use std::{cmp::Ordering, sync::OnceLock};
use async_timing_util::wait_until_timelength;
use komodo_client::entities::stats::{
SingleDiskUsage, SystemInformation, SystemLoadAverage,
SystemProcess, SystemStats,
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
use tokio::sync::RwLock;
@@ -96,15 +95,8 @@ impl StatsClient {
network_egress_bytes += network.transmitted();
}
let load_avg = System::load_average();
SystemStats {
cpu_perc: self.system.global_cpu_usage(),
load_average: SystemLoadAverage {
one: load_avg.one,
five: load_avg.five,
fifteen: load_avg.fifteen,
},
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
-1
View File
@@ -28,7 +28,6 @@ resolver_api.workspace = true
# external
tokio-tungstenite.workspace = true
derive_builder.workspace = true
urlencoding.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
thiserror.workspace = true
-3
View File
@@ -379,9 +379,6 @@ pub struct RunStackService {
/// Do not start linked services
#[arg(long = "no-deps", action = SetTrue)]
pub no_deps: Option<bool>,
/// Detach container on run
#[arg(long = "detach", action = SetTrue)]
pub detach: Option<bool>,
/// Map service ports to the host
#[arg(long = "service-ports", action = SetTrue)]
pub service_ports: Option<bool>,
@@ -125,8 +125,6 @@ pub struct Env {
pub komodo_logging_opentelemetry_service_name: Option<String>,
/// Override `pretty_startup_config`
pub komodo_pretty_startup_config: Option<bool>,
/// Override `unsafe_unsanitized_startup_config`
pub komodo_unsafe_unsanitized_startup_config: Option<bool>,
/// Override `transparent_mode`
pub komodo_transparent_mode: Option<bool>,
@@ -511,12 +509,6 @@ pub struct CoreConfig {
#[serde(default)]
pub pretty_startup_config: bool,
/// Unsafe: logs unsanitized config on startup,
/// in order to verify everything is being
/// passed correctly.
#[serde(default)]
pub unsafe_unsanitized_startup_config: bool,
// ===========
// = Pruning =
// ===========
@@ -737,7 +729,6 @@ impl Default for CoreConfig {
github_webhook_app: Default::default(),
logging: Default::default(),
pretty_startup_config: Default::default(),
unsafe_unsanitized_startup_config: Default::default(),
keep_stats_for_days: default_prune_days(),
keep_alerts_for_days: default_prune_days(),
resource_poll_interval: default_poll_interval(),
@@ -781,8 +772,6 @@ impl CoreConfig {
keep_alerts_for_days: config.keep_alerts_for_days,
logging: config.logging,
pretty_startup_config: config.pretty_startup_config,
unsafe_unsanitized_startup_config: config
.unsafe_unsanitized_startup_config,
transparent_mode: config.transparent_mode,
ui_write_disabled: config.ui_write_disabled,
disable_confirm_dialog: config.disable_confirm_dialog,
+2 -11
View File
@@ -511,17 +511,8 @@ impl RepoExecutionArgs {
&self,
access_token: Option<&str>,
) -> anyhow::Result<String> {
let access_token_at = match access_token {
Some(token) => match token.split_once(':') {
Some((username, token)) => format!(
"{}:{}@",
urlencoding::encode(username.trim()),
urlencoding::encode(token.trim())
),
None => {
format!("token:{}@", urlencoding::encode(token.trim()))
}
},
let access_token_at = match &access_token {
Some(token) => format!("token:{token}@"),
None => String::new(),
};
let protocol = if self.https { "https" } else { "http" };
-17
View File
@@ -45,9 +45,6 @@ pub struct SystemStatsRecord {
// basic stats
/// Cpu usage percentage
pub cpu_perc: f32,
/// Load average (1m, 5m, 15m)
#[serde(default)]
pub load_average: SystemLoadAverage,
/// Memory used in GB
pub mem_used_gb: f64,
/// Total memory in GB
@@ -75,9 +72,6 @@ pub struct SystemStatsRecord {
pub struct SystemStats {
/// Cpu usage percentage
pub cpu_perc: f32,
/// Load average (1m, 5m, 15m)
#[serde(default)]
pub load_average: SystemLoadAverage,
/// [1.15.9+]
/// Free memory in GB.
/// This is really the 'Free' memory, not the 'Available' memory.
@@ -181,14 +175,3 @@ pub struct SystemProcess {
/// Process disk write in KB/s
pub disk_write_kb: f64,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SystemLoadAverage {
/// 1m load average
pub one: f64,
/// 5m load average
pub five: f64,
/// 15m load average
pub fifteen: f64,
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.19.3",
"version": "1.19.1",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
-15
View File
@@ -2402,15 +2402,6 @@ export interface SystemInformation {
export type GetSystemInformationResponse = SystemInformation;
export interface SystemLoadAverage {
/** 1m load average */
one: number;
/** 5m load average */
five: number;
/** 15m load average */
fifteen: number;
}
/** Info for a single disk mounted on the system. */
export interface SingleDiskUsage {
/** The mount point of the disk */
@@ -2472,8 +2463,6 @@ export enum Timelength {
export interface SystemStats {
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/**
* [1.15.9+]
* Free memory in GB.
@@ -6108,8 +6097,6 @@ export interface SystemStatsRecord {
sid: string;
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/** Memory used in GB */
mem_used_gb: number;
/** Total memory in GB */
@@ -7713,8 +7700,6 @@ export interface RunStackService {
no_tty?: boolean;
/** Do not start linked services */
no_deps?: boolean;
/** Detach container on run */
detach?: boolean;
/** Map service ports to the host */
service_ports?: boolean;
/** Extra environment variables for the run */
-3
View File
@@ -272,9 +272,6 @@ pub struct ComposeRun {
/// Do not start linked services
#[serde(default)]
pub no_deps: Option<bool>,
/// Detach container on run
#[serde(default)]
pub detach: Option<bool>,
/// Map service ports to the host
#[serde(default)]
pub service_ports: Option<bool>,
+3 -3
View File
@@ -9,14 +9,14 @@
## All fields with a "Default" provided are optional. If they are
## left out of the file, the "Default" value will be used.
## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`,
## This file is bundled into the official image, `ghcr.io/moghtech/komodo`,
## as the default config at `/config/.default.config.toml`.
## Komodo Core can start with no external config file mounted.
## Komodo can start with no external config file mounted.
## Most fields can also be configured using environment variables.
## Environment variables will override values set in this file.
## Can also use JSON or YAML if preferred. You can convert here:
## Can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+1 -1
View File
@@ -9,7 +9,7 @@
## Most fields can also be configured using cli arguments and environment variables.
## These will will override values set in this file. (cli args > env > config files).
## You can also use JSON or YAML if preferred. You can convert here:
## You can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+4 -4
View File
@@ -16,7 +16,7 @@
## Most fields can also be configured using environment variables.
## Environment variables will override values set in this file.
## You can also use JSON or YAML if preferred. You can convert here:
## You can also use JSON or YAML if preffered. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
@@ -75,10 +75,10 @@ disable_container_exec = false
stats_polling_rate = "5-sec"
## How often Periphery polls the host for container stats,
## Env: PERIPHERY_CONTAINER_STATS_POLLING_RATE
## Env: PERIPHERY_STATS_POLLING_RATE
## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html
## Default: 30-sec
container_stats_polling_rate = "30-sec"
## Default: 5-sec
container_stats_polling_rate = "1-min"
## Whether stack actions should use `docker-compose ...`
## instead of `docker compose ...`.
+2 -2
View File
@@ -71,7 +71,7 @@ permissions = [
## Administration
Users can be given Admin privileges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the "Make Admin" button when on a User page `/users/${user_id}`.
Users can be given Admin priviledges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the "Make Admin" button when on a User page `/users/${user_id}`.
These users have unrestricted access to all Komodo Resources. Additionally, these users can update other (non-admin) user's permissions on resources.
@@ -82,4 +82,4 @@ Users also have some configurable global permissions, these are:
- create server permission
- create build permission
Only users with these permissions (as well as admins) can add additional servers to Komodo, and can create additional builds, respectively.
Only users with these permissions (as well as admins) can add additional servers to Komodo, and can create additional builds, respectively.
-4
View File
@@ -14,10 +14,6 @@ Komodo also supports self hosted Oauth2 providers like [Authentik](https://docs.
- `<KOMODO_HOST>/auth/google/callback` for Google.
- `<KOMODO_HOST>/auth/oidc/callback` for OIDC.
### Authentik
Check out the [Authentik official support documentation](https://integrations.goauthentik.io/infrastructure/komodo/).
### Keycloak
- Create an [OIDC client](https://www.keycloak.org/docs/latest/server_admin/index.html#proc-creating-oidc-client_server_administration_guide) in Keycloak.
- Note down the `Client ID` that you enter (e.g.: "komodo"), you will need it for Komodo configuration
+3 -5
View File
@@ -5,12 +5,10 @@ To run Komodo, you will need Docker. See [the docker install docs](https://docs.
### Deploy with Docker Compose
- [**Using MongoDB**](./mongo.mdx)
- Lower CPU usage, Higher RAM usage.
- Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59).
- [**Using FerretDB** (Postgres)](./ferretdb.mdx)
:::info
Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59).
Users with these systems should use FerretDB instead.
:::
- Lower RAM usage, Higher CPU usage.
:::info
**FerretDB v1** users:
-1
View File
@@ -40,7 +40,6 @@
"jotai": "2.12.5",
"lucide-react": "0.511.0",
"monaco-editor": "0.52.2",
"monaco-yaml": "5.4.0",
"prettier": "3.5.3",
"react": "19.1.0",
"react-charts": "3.0.0-beta.57",
-14
View File
@@ -2517,14 +2517,6 @@ export interface SystemInformation {
container_exec_disabled: boolean;
}
export type GetSystemInformationResponse = SystemInformation;
export interface SystemLoadAverage {
/** 1m load average */
one: number;
/** 5m load average */
five: number;
/** 15m load average */
fifteen: number;
}
/** Info for a single disk mounted on the system. */
export interface SingleDiskUsage {
/** The mount point of the disk */
@@ -2584,8 +2576,6 @@ export declare enum Timelength {
export interface SystemStats {
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/**
* [1.15.9+]
* Free memory in GB.
@@ -5862,8 +5852,6 @@ export interface SystemStatsRecord {
sid: string;
/** Cpu usage percentage */
cpu_perc: number;
/** Load average (1m, 5m, 15m) */
load_average?: SystemLoadAverage;
/** Memory used in GB */
mem_used_gb: number;
/** Total memory in GB */
@@ -7306,8 +7294,6 @@ export interface RunStackService {
no_tty?: boolean;
/** Do not start linked services */
no_deps?: boolean;
/** Detach container on run */
detach?: boolean;
/** Map service ports to the host */
service_ports?: boolean;
/** Extra environment variables for the run */
File diff suppressed because it is too large Load Diff
+4 -12
View File
@@ -170,7 +170,6 @@ interface SectionProps {
actions?: ReactNode;
// otherwise items-start
itemsCenterTitleRow?: boolean;
className?: string;
}
export const Section = ({
@@ -181,9 +180,8 @@ export const Section = ({
actions,
children,
itemsCenterTitleRow,
className,
}: SectionProps) => (
<div className={cn("flex flex-col gap-4", className)}>
<div className="flex flex-col gap-4">
{(title || icon || titleRight || titleOther || actions) && (
<div
className={cn(
@@ -224,7 +222,6 @@ export const NewLayout = ({
}) => {
const [open, set] = useState(false);
const [loading, setLoading] = useState(false);
return (
<Dialog
open={open}
@@ -251,14 +248,9 @@ export const NewLayout = ({
variant="secondary"
onClick={async () => {
setLoading(true);
try {
await onConfirm();
set(false);
} catch (error) {
console.error("Error creating resource:", error);
} finally {
setLoading(false);
}
await onConfirm();
setLoading(false);
set(false);
}}
disabled={!enabled || loading}
>
-11
View File
@@ -56,14 +56,12 @@ export const MonacoEditor = ({
onValueChange,
language: _language,
readOnly,
filename,
minHeight,
className,
}: {
value: string | undefined;
onValueChange?: (value: string) => void;
language: MonacoLanguage | undefined;
filename?: string;
readOnly?: boolean;
minHeight?: number;
className?: string;
@@ -172,7 +170,6 @@ export const MonacoEditor = ({
language={language}
value={value}
theme={theme}
defaultPath={defaultPath(filename)}
options={options}
onChange={(v) => onValueChange?.(v ?? "")}
onMount={(editor) => setEditor(editor)}
@@ -181,14 +178,6 @@ export const MonacoEditor = ({
);
};
const defaultPath = (filename?: string) => {
if (!filename) return undefined;
// Extract only the filename part of path,
// avoiding critical issue when path starts with '/'
const split = filename.split("/");
return split[split.length - 1];
};
const MIN_DIFF_HEIGHT = 100;
const MAX_DIFF_HEIGHT = 400;
@@ -1132,7 +1132,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
command: undefined,
no_tty: undefined,
no_deps: undefined,
detach: undefined,
service_ports: undefined,
env: undefined,
workdir: undefined,
@@ -1152,7 +1151,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
);
const [no_tty, setNoTty] = useState(!!params.no_tty);
const [no_deps, setNoDeps] = useState(!!params.no_deps);
const [detach, setDetach] = useState(!!params.detach);
const [service_ports, setServicePorts] = useState(!!params.service_ports);
const [workdir, setWorkdir] = useState(params.workdir ?? "");
const [user, setUser] = useState(params.user ?? "");
@@ -1177,7 +1175,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
);
setNoTty(!!params.no_tty);
setNoDeps(!!params.no_deps);
setDetach(!!params.detach);
setServicePorts(!!params.service_ports);
setWorkdir(params.workdir ?? "");
setUser(params.user ?? "");
@@ -1219,7 +1216,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
user: user || undefined,
entrypoint: entrypoint || undefined,
pull: pull ? true : undefined,
detach: detach ? true : undefined,
env,
} as any);
setOpen(false);
@@ -1277,10 +1273,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
<Switch checked={no_deps} onCheckedChange={setNoDeps} />
<span className="text-sm">No Dependencies</span>
</label>
<label className="flex items-center gap-2">
<Switch checked={detach} onCheckedChange={setDetach} />
<span className="text-sm">Detach</span>
</label>
<label className="flex items-center gap-2">
<Switch
checked={service_ports}
@@ -7,7 +7,7 @@ import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
import { file_contents_empty, sync_no_changes } from "@lib/utils";
import { usePermissions } from "@lib/hooks";
import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
import { useFullResourceSync, usePendingView } from ".";
export const RefreshSync = ({ id }: { id: string }) => {
const inv = useInvalidate();
@@ -34,10 +34,11 @@ export const ExecuteSync = ({ id }: { id: string }) => {
{ refetchInterval: 5000 }
).data?.syncing;
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const [_pendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
if (
view !== "Execute" ||
pendingView === "Commit" ||
!sync ||
sync_no_changes(sync) ||
!sync.info?.remote_contents
@@ -72,12 +73,11 @@ export const ExecuteSync = ({ id }: { id: string }) => {
export const CommitSync = ({ id }: { id: string }) => {
const { mutate, isPending } = useWrite("CommitSync");
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const { canWrite } = usePermissions({ type: "ResourceSync", id });
const [_pendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
if (view !== "Commit" || !canWrite || !sync) {
return null;
}
if (pendingView === "Execute" || !canWrite || !sync) return null;
const freshSync =
!sync.config?.files_on_host &&
@@ -1,4 +1,4 @@
import { atomWithStorage, useRead, useUser } from "@lib/hooks";
import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Card } from "@ui/card";
import { Clock, FolderSync } from "lucide-react";
@@ -45,16 +45,23 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
};
type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit";
const syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(
"sync-tabs-v4",
"Config"
const pendingViewAtom = atomWithStorage<"Execute" | "Commit">(
"sync-view-v1",
"Execute"
);
export const usePendingView = () => {
return useAtom(pendingViewAtom) as [
"Execute" | "Commit",
(view: "Execute" | "Commit") => void,
];
};
export const useResourceSyncTabsView = (
sync: Types.ResourceSync | undefined
) => {
const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);
const ConfigInfoPending = ({ id }: { id: string }) => {
const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">(
"sync-tabs-v3",
"Config"
);
const sync = useFullResourceSync(id);
const hideInfo = sync?.config?.files_on_host
? false
@@ -68,28 +75,13 @@ export const useResourceSyncTabsView = (
const view =
_view === "Info" && hideInfo
? "Config"
: (_view === "Execute" || _view === "Commit") && !showPending
: _view === "Pending" && !showPending
? sync?.config?.files_on_host ||
sync?.config?.repo ||
sync?.config?.linked_repo
? "Info"
: "Config"
: _view === "Commit" && !sync?.config?.managed
? "Execute"
: _view;
return {
view,
setView,
hideInfo,
showPending,
};
};
const ConfigInfoPending = ({ id }: { id: string }) => {
const sync = useFullResourceSync(id);
const { view, setView, hideInfo, showPending } =
useResourceSyncTabsView(sync);
: _view;
const title = (
<TabsList className="justify-start w-fit">
@@ -104,21 +96,12 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
Info
</TabsTrigger>
<TabsTrigger
value="Execute"
value="Pending"
className="w-[110px]"
disabled={!showPending}
>
Execute
Pending
</TabsTrigger>
{sync?.config?.managed && (
<TabsTrigger
value="Commit"
className="w-[110px]"
disabled={!showPending}
>
Commit
</TabsTrigger>
)}
</TabsList>
);
return (
@@ -129,10 +112,7 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
<TabsContent value="Info">
<ResourceSyncInfo id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Execute">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Commit">
<TabsContent value="Pending">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
</Tabs>
@@ -184,7 +164,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
},
GroupActions: () => (
<GroupActions type="ResourceSync" actions={["RunSync", "CommitSync"]} />
<GroupActions type="ResourceSync" actions={["RunSync"]} />
),
Table: ({ resources }) => (
@@ -10,7 +10,8 @@ import { cn, sanitizeOnlySpan } from "@lib/utils";
import { ConfirmButton } from "@components/util";
import { SquarePlay } from "lucide-react";
import { usePermissions } from "@lib/hooks";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
import { useFullResourceSync, usePendingView } from ".";
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
import { ResourceDiff } from "komodo_client/dist/types";
export const ResourceSyncPending = ({
@@ -23,16 +24,31 @@ export const ResourceSyncPending = ({
const syncing = useRead("GetResourceSyncActionState", { sync: id }).data
?.syncing;
const sync = useFullResourceSync(id);
const { view } = useResourceSyncTabsView(sync);
const { canExecute } = usePermissions({ type: "ResourceSync", id });
const [_pendingView, setPendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
const { mutate, isPending } = useExecute("RunSync");
const loading = isPending || syncing;
return (
<Section titleOther={titleOther} className="min-h-[500px]">
<div className="flex items-center gap-4 pl-1 py-2 flex-wrap">
<div className="text-muted-foreground">{view} Mode:</div>
<Section
titleOther={titleOther}
>
<div className="flex items-center gap-4 py-2 flex-wrap">
{sync?.config?.managed && (
<Tabs value={pendingView} onValueChange={setPendingView as any}>
<TabsList className="justify-start w-fit">
<TabsTrigger value="Execute" className="w-[110px]">
Execute
</TabsTrigger>
<TabsTrigger value="Commit" className="w-[110px]">
Commit
</TabsTrigger>
</TabsList>
</Tabs>
)}
<div className="text-muted-foreground">{pendingView} Mode:</div>
<div className="flex items-center gap-1 flex-wrap">
{view === "Execute" && (
{pendingView === "Execute" && (
<>
Update resources in the
<div className="font-bold">UI</div>
@@ -40,7 +56,7 @@ export const ResourceSyncPending = ({
<div className="font-bold">file changes.</div>
</>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<>
Update resources in the
<div className="font-bold">file</div>
@@ -73,7 +89,7 @@ export const ResourceSyncPending = ({
) : undefined}
{/* Pending Deploy */}
{view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
{pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
<Card>
<CardHeader
className={cn(
@@ -102,10 +118,13 @@ export const ResourceSyncPending = ({
<div className="flex items-center gap-4 font-mono">
<div
className={text_color_class_by_intention(
diff_type_intention(update.data.type, view === "Commit")
diff_type_intention(
update.data.type,
pendingView === "Commit"
)
)}
>
{view === "Commit"
{pendingView === "Commit"
? reverse_pending_type(update.data.type)
: update.data.type}{" "}
{update.target.type}
@@ -120,7 +139,7 @@ export const ResourceSyncPending = ({
/>
)}
</div>
{canExecute && view === "Execute" && (
{canExecute && pendingView === "Execute" && (
<ConfirmButton
title="Execute Change"
icon={<SquarePlay className="w-4 h-4" />}
@@ -149,7 +168,7 @@ export const ResourceSyncPending = ({
)}
{update.data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={update.data.data.current}
modified={update.data.data.proposed}
@@ -157,7 +176,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={update.data.data.proposed}
modified={update.data.data.current}
@@ -186,11 +205,13 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, view === "Commit")
diff_type_intention(data.type, pendingView === "Commit")
)
)}
>
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
Variable
</CardHeader>
<CardContent>
@@ -203,7 +224,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -211,7 +232,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -240,11 +261,13 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, view === "Commit")
diff_type_intention(data.type, pendingView === "Commit")
)
)}
>
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
User Group
</CardHeader>
<CardContent>
@@ -257,7 +280,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{view === "Execute" && (
{pendingView === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -265,7 +288,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{view === "Commit" && (
{pendingView === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -401,27 +401,6 @@ export const ServerComponents: RequiredResourceComponents = {
</div>
);
},
LoadAvg: ({ id }) => {
const server = useServer(id);
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: server ? server.info.state !== "Disabled" : false,
refetchInterval: 5000,
}
).data;
if (!stats?.load_average) return null;
const one = stats.load_average?.one;
return (
<div className="flex gap-2 items-center">
<Cpu className="w-4 h-4" />
{one.toFixed(2)}
</div>
);
},
Mem: ({ id }) => {
const server = useServer(id);
const stats = useRead(
@@ -1,190 +0,0 @@
import { ResourceLink } from "@components/resources/common";
import { ServerComponents } from "@components/resources/server";
import { DataTable, SortableHeader } from "@ui/data-table";
import { useRead } from "@lib/hooks";
import { useMemo } from "react";
export const ServerMonitoringTable = ({ search = "" }: { search?: string }) => {
const servers = useRead("ListServers", {}).data;
const searchSplit = useMemo(
() => search.toLowerCase().split(" ").filter((t) => t),
[search]
);
const filtered = useMemo(
() =>
servers?.filter((s) =>
searchSplit.length === 0
? true
: searchSplit.every((t) => s.name.toLowerCase().includes(t))
) ?? [],
[servers, searchSplit]
);
return (
<div className="flex flex-col gap-4">
<DataTable<any, any>
tableKey="servers-monitoring-v1"
data={filtered}
columns={[
{
accessorKey: "name",
size: 250,
header: ({ column }) => (
<SortableHeader column={column} title="System" />
),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<ResourceLink type="Server" id={row.original.id} />
</div>
),
},
{
header: "CPU",
size: 200,
cell: ({ row }) => <CpuCell id={row.original.id} />,
},
{
header: "Memory",
size: 200,
cell: ({ row }) => <MemCell id={row.original.id} />,
},
{
header: "Disk",
size: 200,
cell: ({ row }) => <DiskCell id={row.original.id} />,
},
{
header: "Load Avg",
size: 160,
cell: ({ row }) => <LoadAvgCell id={row.original.id} />,
},
{
header: "Net",
size: 100,
cell: ({ row }) => <NetCell id={row.original.id} />,
},
{
header: "Agent",
size: 160,
cell: ({ row }) => <ServerComponents.Info.Version id={row.original.id} />,
},
]}
/>
</div>
);
};
const useStats = (id: string) =>
useRead("GetSystemStats", { server: id }, { refetchInterval: 10_000 }).data;
const useServerThresholds = (id: string) => {
const config = useRead("GetServer", { server: id }).data?.config as any;
return {
cpuWarning: config?.cpu_warning ?? 75,
cpuCritical: config?.cpu_critical ?? 90,
memWarning: config?.mem_warning ?? 75,
memCritical: config?.mem_critical ?? 90,
diskWarning: config?.disk_warning ?? 75,
diskCritical: config?.disk_critical ?? 90,
};
};
const Bar = ({ valuePerc, intent }: { valuePerc?: number; intent: "Good" | "Warning" | "Critical" }) => {
const w = Math.max(0, Math.min(100, valuePerc ?? 0)) / 100;
const color = intent === "Good" ? "bg-green-500" : intent === "Warning" ? "bg-orange-500" : "bg-red-500";
return (
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span className={`absolute inset-0 w-full h-full origin-left ${color}`} style={{ transform: `scaleX(${w})` }} />
</span>
);
};
const CpuCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const cpu = stats?.cpu_perc ?? 0;
const { cpuWarning: warning, cpuCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{cpu.toFixed(2)}%</span>
<Bar valuePerc={cpu} intent={intent} />
</div>
);
};
const MemCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const used = stats?.mem_used_gb ?? 0;
const total = stats?.mem_total_gb ?? 0;
const perc = total > 0 ? (used / total) * 100 : 0;
const { memWarning: warning, memCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
perc < warning ? "Good" : perc < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{perc.toFixed(1)}%</span>
<Bar valuePerc={perc} intent={intent} />
</div>
);
};
const DiskCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const used = stats?.disks?.reduce((acc, d) => acc + (d.used_gb || 0), 0) ?? 0;
const total = stats?.disks?.reduce((acc, d) => acc + (d.total_gb || 0), 0) ?? 0;
const perc = total > 0 ? (used / total) * 100 : 0;
const { diskWarning: warning, diskCritical: critical } = useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
perc < warning ? "Good" : perc < critical ? "Warning" : "Critical";
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{perc.toFixed(1)}%</span>
<Bar valuePerc={perc} intent={intent} />
</div>
);
};
const formatRate = (bytes?: number) => {
const b = bytes ?? 0;
const kb = 1024;
const mb = kb * 1024;
const gb = mb * 1024;
if (b >= gb) return `${(b / gb).toFixed(2)} GB/s`;
if (b >= mb) return `${(b / mb).toFixed(2)} MB/s`;
if (b >= kb) return `${(b / kb).toFixed(2)} KB/s`;
return `${b.toFixed(0)} B/s`;
};
const NetCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const ingress = stats?.network_ingress_bytes ?? 0;
const egress = stats?.network_egress_bytes ?? 0;
return (
<span className="tabular-nums whitespace-nowrap">
{formatRate(ingress + egress)}
</span>
);
};
const LoadAvgCell = ({ id }: { id: string }) => {
const stats = useStats(id);
const one = stats?.load_average?.one;
const five = stats?.load_average?.five;
const fifteen = stats?.load_average?.fifteen;
if (one === undefined || five === undefined || fifteen === undefined) {
return (
<div className="w-full flex items-center gap-[.35em] tabular-nums text-muted-foreground tracking-tight">
<span>N/A</span>
</div>
);
}
return (
<div className="w-full flex items-center gap-[.35em] tabular-nums tracking-tight">
<span>{one.toFixed(2)}</span>
<span>{five.toFixed(2)}</span>
<span>{fifteen.toFixed(2)}</span>
</div>
);
};
@@ -3,19 +3,13 @@ import { useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { useMemo } from "react";
import { useStatsGranularity } from "./hooks";
import { Loader2, OctagonAlert } from "lucide-react";
import { Loader2 } from "lucide-react";
import { AxisOptions, Chart } from "react-charts";
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
import { useTheme } from "@ui/theme";
import { fmt_utc_date } from "@lib/formatting";
type StatType =
| "Cpu"
| "Memory"
| "Disk"
| "Network Ingress"
| "Network Egress"
| "Load Average";
type StatType = "Cpu" | "Memory" | "Disk" | "Network Ingress" | "Network Egress";
type StatDatapoint = { date: number; value: number };
@@ -35,34 +29,18 @@ export const StatChart = ({
granularity,
});
const seriesData = useMemo(() => {
if (!data?.stats) return [] as { label: string; data: StatDatapoint[] }[];
const records = [...data.stats].reverse();
if (type === "Load Average") {
const one = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.one ?? 0,
}));
const five = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.five ?? 0,
}));
const fifteen = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: s.load_average?.fifteen ?? 0,
}));
return [
{ label: "1m", data: one },
{ label: "5m", data: five },
{ label: "15m", data: fifteen },
];
}
const single = records.map((stat) => ({
date: convertTsMsToLocalUnixTsInMs(stat.ts),
value: getStat(stat, type),
}));
return [{ label: type, data: single }];
}, [data, type]);
const stats = useMemo(
() =>
data?.stats
.map((stat) => {
return {
date: convertTsMsToLocalUnixTsInMs(stat.ts),
value: getStat(stat, type),
};
})
.reverse(),
[data]
);
return (
<div className={className}>
@@ -72,11 +50,8 @@ export const StatChart = ({
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : (
<InnerStatChart
type={type}
stats={seriesData.flatMap((s) => s.data)}
seriesData={seriesData}
/>
stats &&
stats.length > 0 && <InnerStatChart type={type} stats={stats} />
)}
</div>
);
@@ -89,11 +64,9 @@ const BYTES_PER_KB = 1024.0;
export const InnerStatChart = ({
type,
stats,
seriesData,
}: {
type: StatType;
stats: StatDatapoint[] | undefined;
seriesData?: { label: string; data: StatDatapoint[] }[];
}) => {
const { theme: _theme } = useTheme();
const theme =
@@ -123,13 +96,10 @@ export const InnerStatChart = ({
cursor: (_value?: Date) => false,
},
};
}, [min, max, diff]);
}, []);
// Determine the dynamic scaling for network-related types
const allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) =>
s.data.map((d) => d.value)
);
const maxStatValue = Math.max(...(allValues.length ? allValues : [0]));
const maxStatValue = Math.max(...(stats?.map((d) => d.value) ?? [0]));
const { unit, maxUnitValue } = useMemo(() => {
if (type === "Network Ingress" || type === "Network Egress") {
@@ -143,13 +113,6 @@ export const InnerStatChart = ({
return { unit: "TB", maxUnitValue: BYTES_PER_GB * 1024 }; // Larger scale for high values
}
}
if (type === "Load Average") {
// Leave unitless; set max slightly above observed
return {
unit: "",
maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,
};
}
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
}, [type, maxStatValue]);
@@ -157,8 +120,7 @@ export const InnerStatChart = ({
(): AxisOptions<StatDatapoint>[] => [
{
getValue: (datum) => datum.value,
elementType: type === "Load Average" ? "line" : "area",
stacked: type !== "Load Average",
elementType: "area",
min: 0,
max: maxUnitValue,
formatters: {
@@ -166,9 +128,7 @@ export const InnerStatChart = ({
<div className="text-lg font-mono">
{(type === "Network Ingress" || type === "Network Egress") && unit
? `${(value ?? 0) / (maxUnitValue / 1024)} ${unit}`
: type === "Load Average"
? `${(value ?? 0).toFixed(2)}`
: `${value?.toFixed(2)}%`}
: `${value?.toFixed(2)}%`}
</div>
),
},
@@ -176,30 +136,18 @@ export const InnerStatChart = ({
],
[type, maxUnitValue, unit]
);
if ((seriesData?.[0]?.data.length ?? 0) < 2) {
return (
<div className="w-full h-full flex gap-4 justify-center items-center">
<OctagonAlert className="w-6 h-6" />
<h1>Not enough data yet, choose a smaller interval.</h1>
</div>
);
}
return (
<Chart
options={{
data: seriesData ?? [{ label: type, data: stats ?? [] }],
data: [
{
label: type,
data: stats ?? [],
},
],
primaryAxis: timeAxis,
secondaryAxes: valueAxis,
defaultColors:
type === "Load Average"
? [
hex_color_by_intention("Good"),
hex_color_by_intention("Neutral"),
hex_color_by_intention("Unknown"),
]
: [getColor(type)],
defaultColors: [getColor(type)],
dark: theme === "dark",
padding: {
left: 10,
@@ -1,7 +1,13 @@
import { Section } from "@components/layouts";
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
import { Progress } from "@ui/progress";
import { Cpu, Database, Loader2, MemoryStick, Search } from "lucide-react";
import {
Cpu,
Database,
Loader2,
MemoryStick,
Search,
} from "lucide-react";
import { useLocalStorage, usePermissions, useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { DataTable, SortableHeader } from "@ui/data-table";
@@ -114,7 +120,6 @@ export const ServerStats = ({
{/* Current Overview */}
<Section title="Current">
<div className="flex flex-col xl:flex-row gap-4">
<LOAD_AVERAGE id={id} stats={stats} />
<CPU stats={stats} />
<RAM stats={stats} />
<DISK stats={stats} />
@@ -330,11 +335,6 @@ export const ServerStats = ({
}
>
<div className="flex flex-col gap-8">
<StatChart
server_id={id}
type="Load Average"
className="w-full h-[250px]"
/>
<StatChart server_id={id} type="Cpu" className="w-full h-[250px]" />
<StatChart
server_id={id}
@@ -495,82 +495,6 @@ const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
);
};
const LOAD_AVERAGE = ({
id,
stats,
}: {
id: string;
stats: Types.SystemStats | undefined;
}) => {
if (!stats?.load_average) return null;
const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {};
const cores = useRead("GetSystemInformation", { server: id }).data
?.core_count;
const pct = (load: number) =>
cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined;
const textColor = (load: number) => {
const p = pct(load);
if (p === undefined) return "text-muted-foreground";
return p <= 50
? "text-green-600"
: p <= 80
? "text-yellow-600"
: "text-red-600";
};
return (
<Card className="w-full">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle>Load Average</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Current Load */}
<div className="space-y-2">
<div className="flex items-baseline justify-between">
<span
className={`text-3xl font-bold tabular-nums ${textColor(one)}`}
>
{one.toFixed(2)}
</span>
<span className="text-sm text-muted-foreground">
{cores && cores > 0
? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`
: "N/A"}
</span>
</div>
<Progress value={pct(one) ?? 0} className="h-2" />
</div>
{/* Time Intervals */}
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4 text-sm">
{[
["1m", one],
["5m", five],
["15m", fifteen],
].map(([label, value]) => (
<div className="space-y-1" key={label as string}>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{label}</span>
<span
className={`font-medium tabular-nums ${textColor(value as number)}`}
>
{(value as number).toFixed(2)}
</span>
</div>
<Progress value={pct(value as number) ?? 0} className="h-1" />
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};
const RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const used = stats?.mem_used_gb;
const total = stats?.mem_total_gb;
@@ -879,7 +879,6 @@ export const StackConfig = ({
value={
show_default ? DEFAULT_STACK_FILE_CONTENTS : file_contents
}
filename="compose.yaml"
onValueChange={(file_contents) => set({ file_contents })}
language="yaml"
readOnly={disabled}
@@ -1005,7 +1004,7 @@ const ConfigFiles = ({
/>
<RequiresSelector
requires={requires ?? Types.StackFileRequires.None}
requires={requires ?? Types.StackFileRequires.Redeploy}
set={(requires) => {
values[i] = { ...values[i], requires };
set({ config_files: [...values] });
@@ -1047,7 +1046,7 @@ const ServicesSelector = ({
disabled={disabled}
>
<div className="flex gap-2 items-center">
<div className="text-xs text-muted-foreground">Services:</div>
<div className="text-xs text-muted-foreground">Dependents:</div>
{selected_services.length === 0
? "All"
: selected_services.join(", ")}
@@ -263,7 +263,6 @@ export const StackInfo = ({
value={edits[content.path] ?? content.contents}
language={language_from_path(content.path)}
readOnly={!canEdit}
filename={content.path}
onValueChange={editFileCallback(content.path)}
/>
</CardContent>
+50 -108
View File
@@ -93,6 +93,24 @@ import {
} from "@ui/select";
import { useServer } from "./resources/server";
export const WithLoading = ({
children,
isLoading,
loading,
isError,
error,
}: {
children: ReactNode;
isLoading: boolean;
loading?: ReactNode;
isError: boolean;
error?: ReactNode;
}) => {
if (isLoading) return <>{loading ?? "loading"}</>;
if (isError) return <>{error ?? null}</>;
return <>{children}</>;
};
export const ActionButton = forwardRef<
HTMLButtonElement,
{
@@ -898,23 +916,6 @@ export const DockerContainersSection = ({
{
accessorKey: "ports.0",
size: 200,
sortingFn: (a, b) => {
const getMinHostPort = (row: typeof a) => {
const ports = row.original.ports ?? [];
if (!ports.length) return Number.POSITIVE_INFINITY;
const nums = ports
.map((p) => p.PublicPort)
.filter((p): p is number => typeof p === "number")
.map((n) => Number(n));
if (!nums.length || nums.some((n) => Number.isNaN(n))) {
return Number.POSITIVE_INFINITY;
}
return Math.min(...nums);
};
const pa = getMinHostPort(a);
const pb = getMinHostPort(b);
return pa === pb ? 0 : pa > pb ? 1 : -1;
},
header: ({ column }) => (
<SortableHeader column={column} title="Ports" />
),
@@ -1202,35 +1203,6 @@ export const TemplateQueryBehaviorSelector = () => {
);
};
export type ServerAddress = {
raw: string;
protocol: "http:" | "https:";
hostname: string;
};
export const useServerAddress = (
server_id: string | undefined
): ServerAddress | null => {
const server = useServer(server_id);
if (!server) return null;
const base = server.info.external_address || server.info.address;
const parsed = (() => {
try {
return new URL(base);
} catch {
return new URL("http://" + base);
}
})();
return {
raw: base,
protocol: parsed.protocol === "https:" ? "https:" : "http:",
hostname: parsed.hostname,
};
};
export const ContainerPortLink = ({
host_port,
ports,
@@ -1240,29 +1212,19 @@ export const ContainerPortLink = ({
ports: Types.Port[];
server_id: string | undefined;
}) => {
const server_address = useServerAddress(server_id);
if (!server_address) return null;
const isHttps = server_address.protocol === "https:";
const link = host_port === "443" && isHttps
? `https://${server_address.hostname}`
: `http://${server_address.hostname}:${host_port}`;
const uniqueHostPorts = Array.from(
new Set(
ports
.map((p) => p.PublicPort)
.filter((p): p is number => typeof p === "number")
.map((n) => Number(n))
.filter((n) => !Number.isNaN(n))
)
).sort((a, b) => a - b);
const display_text =
uniqueHostPorts.length <= 1
? String(uniqueHostPorts[0] ?? host_port)
: `${uniqueHostPorts[0]}-${uniqueHostPorts[uniqueHostPorts.length - 1]}`;
const server = useServer(server_id);
// Get the server address with periphery port removed
const server_address = server?.info.external_address
? server.info.external_address
: server?.info.address
.split(":")
// take just protocol and dns (indexes 0 and 1)
.filter((_, i) => i < 2)
.join(":");
const link =
host_port === "443"
? server_address
: server_address?.replace("https", "http") + ":" + host_port;
return (
<Tooltip>
<TooltipTrigger>
@@ -1274,7 +1236,7 @@ export const ContainerPortLink = ({
<EthernetPort
className={cn("w-4 h-4", stroke_color_class_by_intention("Good"))}
/>
{display_text}
{host_port}
</a>
</TooltipTrigger>
<TooltipContent className="flex flex-col gap-2 w-fit">
@@ -1286,18 +1248,12 @@ export const ContainerPortLink = ({
<LinkIcon className="w-3 h-3" />
{link}
</a>
{ports.slice(0, 10).map((port, i) => (
{ports.map((port, i) => (
<div key={i} className="flex gap-2 text-sm text-muted-foreground">
<span>-</span>
<div>-</div>
<div>{fmt_port_mount(port)}</div>
</div>
))}
{ports.length > 10 && (
<div className="flex gap-2 text-sm text-muted-foreground">
<span>+</span>
<div>{ports.length - 10} more…</div>
</div>
)}
</TooltipContent>
</Tooltip>
);
@@ -1310,38 +1266,24 @@ export const ContainerPortsTableView = ({
ports: Types.Port[];
server_id: string | undefined;
}) => {
const portsMap = useContainerPortsMap(ports);
const sortedNumericPorts = Object.keys(portsMap)
.map(Number)
.filter((port) => !Number.isNaN(port))
.sort((a, b) => a - b);
type Group = { start: number; end: number; ports: Types.Port[] };
const groupedPorts = sortedNumericPorts.reduce<Group[]>((acc, port) => {
const lastGroup = acc[acc.length - 1];
const currentPorts = portsMap[String(port)] || [];
if (lastGroup && port === lastGroup.end + 1) {
lastGroup.end = port;
lastGroup.ports.push(...currentPorts);
} else {
acc.push({ start: port, end: port, ports: currentPorts });
}
return acc;
}, []);
const map = useContainerPortsMap(ports);
const host_ports = Object.keys(map);
return (
<div className="flex items-center gap-x-1 flex-wrap">
{groupedPorts.map((group, i) => (
<Fragment key={group.start}>
{i > 0 && <span className="text-muted-foreground">|</span>}
<ContainerPortLink
host_port={String(group.start)}
ports={group.ports}
server_id={server_id}
/>
</Fragment>
))}
{host_ports.map((host_port, i) => {
return (
<Fragment key={host_port}>
<ContainerPortLink
host_port={host_port}
ports={map[host_port]}
server_id={server_id}
/>
{i !== host_ports.length - 1 && (
<div className="text-muted-foreground">|</div>
)}
</Fragment>
);
})}
</div>
);
};
-21
View File
@@ -805,24 +805,3 @@ export const useContainerPortsMap = (ports: Types.Port[]) => {
return map;
}, [ports]);
};
/**
* A custom React hook that debounces a value, delaying its update until after
* a specified period of inactivity. This is useful for performance optimization
* in scenarios like search inputs, form validation, or API calls.
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
+1 -5
View File
@@ -4,7 +4,6 @@ import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import yamlWorker from "monaco-yaml/yaml.worker?worker";
self.MonacoEnvironment = {
getWorker(_, label) {
@@ -20,9 +19,6 @@ self.MonacoEnvironment = {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
if (label === "yaml") {
return new yamlWorker();
}
return new editorWorker();
},
};
@@ -36,6 +32,6 @@ import "./theme";
import "./yaml";
import "./toml";
import "./fancy_toml";
import "./shell";
import "./shell"
import "./key_value";
import "./string_list";
+3 -16
View File
@@ -1,5 +1,4 @@
import * as monaco from "monaco-editor";
import { configureMonacoYaml } from "monaco-yaml";
// This is the one provided by Microsoft.
// https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/yaml/yaml.ts
@@ -253,23 +252,11 @@ const yaml_language = <monaco.languages.IMonarchLanguage>{
},
};
configureMonacoYaml(monaco, {
enableSchemaRequest: true,
schemas: [
{
fileMatch: ["**/*compose.yml", "**/*compose.yaml"],
uri: new URL(
"/schema/compose-spec.json",
window.location.href
).toString(),
},
],
});
monaco.languages.register({ id: "yaml", aliases: ["yml"] });
monaco.languages.register({ id: "yaml" });
monaco.languages.setMonarchTokensProvider("yaml", yaml_language);
monaco.languages.setLanguageConfiguration("yaml", yaml_conf);
/// V1
// const yaml_conf: monaco.languages.LanguageConfiguration = {
// comments: {
@@ -358,4 +345,4 @@ monaco.languages.setLanguageConfiguration("yaml", yaml_conf);
// monaco.languages.register({ id: "yaml" });
// monaco.languages.setMonarchTokensProvider("yaml", yaml_language);
// monaco.languages.setLanguageConfiguration("yaml", yaml_conf);
// monaco.languages.setLanguageConfiguration("yaml", yaml_conf);
+33 -87
View File
@@ -6,66 +6,33 @@ import {
StatusBadge,
} from "@components/util";
import { container_state_intention } from "@lib/color";
import { useDebounce, useRead } from "@lib/hooks";
import { useRead } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Input } from "@ui/input";
import { MultiSelect } from "@ui/multi-select";
import { Box, Search, RotateCcw } from "lucide-react";
import { Button } from "@ui/button";
import { Box, Search } from "lucide-react";
import { Fragment, useCallback, useMemo, useState } from "react";
export default function ContainersPage() {
const [search, setSearch] = useState("");
const [selectedServers, setSelectedServers] = useState<string[]>([]);
const debouncedSearch = useDebounce(search, 300);
const searchSplit = debouncedSearch
const searchSplit = search
.toLowerCase()
.split(" ")
.filter((term) => term);
const servers = useRead("ListServers", {}).data;
const serverOptions = useMemo(
() =>
servers?.map((server) => ({
label: server.name,
value: server.id,
})) || [],
[servers]
);
const serverName = useCallback(
(id: string) => servers?.find((server) => server.id === id)?.name,
[servers]
);
const _containers = useRead("ListAllDockerContainers", {}).data;
const containers = useMemo(
() =>
_containers?.filter((c) => {
if (searchSplit.length > 0) {
const lower = c.name.toLowerCase();
const searchMatch = searchSplit.every((search) =>
lower.includes(search)
);
if (!searchMatch) return false;
}
if (selectedServers.length > 0) {
return selectedServers.includes(c.server_id!);
}
return true;
if (searchSplit.length === 0) return true;
const lower = c.name.toLowerCase();
return searchSplit.every((search) => lower.includes(search));
}),
[_containers, searchSplit, selectedServers]
[_containers, searchSplit]
);
const clearAllServers = useCallback(() => {
setSelectedServers([]);
}, []);
return (
<Page
title="Containers"
@@ -77,45 +44,18 @@ export default function ContainersPage() {
icon={<Box className="w-8 h-8" />}
>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-wrap">
{/* Server Filter Multi-Select */}
<div className="w-[280px]">
<MultiSelect
options={serverOptions}
value={selectedServers}
onChange={setSelectedServers}
placeholder="Filter by server..."
className="w-full h-10"
/>
</div>
{/* Reset Server Filter Button */}
{selectedServers.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={clearAllServers}
className="h-10 px-3"
>
<RotateCcw className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
{/* Search Input */}
<div className="flex items-center justify-between">
<div></div>
<div className="relative">
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="search..."
className="pl-8 w-[200px] lg:w-[300px] py-0 h-10"
className="pl-8 w-[200px] lg:w-[300px]"
/>
</div>
</div>
<DataTable
data={containers ?? []}
tableKey="containers-page-v1"
@@ -222,23 +162,6 @@ export default function ContainersPage() {
{
accessorKey: "ports.0",
size: 200,
sortingFn: (a, b) => {
const getMinHostPort = (row: typeof a) => {
const ports = row.original.ports ?? [];
if (!ports.length) return Number.POSITIVE_INFINITY;
const nums = ports
.map((p) => p.PublicPort)
.filter((p): p is number => typeof p === "number")
.map((n) => Number(n));
if (!nums.length || nums.some((n) => Number.isNaN(n))) {
return Number.POSITIVE_INFINITY;
}
return Math.min(...nums);
};
const pa = getMinHostPort(a);
const pb = getMinHostPort(b);
return pa === pb ? 0 : pa > pb ? 1 : -1;
},
header: ({ column }) => (
<SortableHeader column={column} title="Ports" />
),
@@ -249,6 +172,29 @@ export default function ContainersPage() {
/>
),
},
// {
// accessorKey: "volumes.0",
// minSize: 300,
// header: ({ column }) => (
// <SortableHeader column={column} title="Volumes" />
// ),
// cell: ({ row }) => (
// <div className="flex items-center gap-x-2 flex-wrap">
// {row.original.volumes.map((volume, i) => (
// <Fragment key={volume}>
// <DockerResourceLink
// type="volume"
// server_id={row.original.server_id!}
// name={volume}
// />
// {i !== row.original.volumes.length - 1 && (
// <div className="text-muted-foreground">|</div>
// )}
// </Fragment>
// ))}
// </div>
// ),
// },
]}
/>
</div>
+27 -47
View File
@@ -15,7 +15,7 @@ import {
useLoginOptions,
useUserInvalidate,
} from "@lib/hooks";
import { useRef } from "react";
import { useState } from "react";
import { ThemeToggle } from "@ui/theme";
import { KOMODO_BASE_URL } from "@main";
import { KeyRound, X } from "lucide-react";
@@ -38,9 +38,9 @@ const login_with_oauth = (provider: OauthProvider) => {
export default function Login() {
const options = useLoginOptions().data;
const [creds, set] = useState({ username: "", password: "" });
const userInvalidate = useUserInvalidate();
const { toast } = useToast();
const formRef = useRef<HTMLFormElement>(null);
// If signing in another user, need to redirect away from /login manually
const maybeNavigate = location.pathname.startsWith("/login")
@@ -64,13 +64,13 @@ export default function Login() {
const message = e?.response?.data?.error as string | undefined;
if (message) {
toast({
title: `Failed to sign up user. '${message}'`,
title: `Failed to login user. '${message}'`,
variant: "destructive",
});
console.error(e);
} else {
toast({
title: "Failed to sign up user. See console log for details.",
title: "Failed to login user. See console log for details.",
variant: "destructive",
});
console.error(e);
@@ -98,25 +98,7 @@ export default function Login() {
},
});
const getFormCredentials = () => {
if (!formRef.current) return undefined;
const fd = new FormData(formRef.current);
const username = String(fd.get("username") ?? "");
const password = String(fd.get("password") ?? "");
return { username, password };
};
const handleLogin = () => {
const creds = getFormCredentials();
if (!creds) return;
login(creds);
};
const handleSignUp = () => {
const creds = getFormCredentials();
if (!creds) return;
signup(creds);
};
const no_auth_configured =
options !== undefined &&
@@ -128,6 +110,7 @@ export default function Login() {
return (
<div className="flex flex-col min-h-screen">
<div className="container flex justify-end items-center h-16">
{/* <img src="/komodo-512x512.png" className="w-[32px]" /> */}
<ThemeToggle />
</div>
<div
@@ -183,28 +166,28 @@ export default function Login() {
</div>
</CardHeader>
{options?.local && (
<form
ref={formRef}
autoComplete="on"
>
<>
<CardContent className="flex flex-col justify-center w-full gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
name="username"
autoComplete="username"
autoCapitalize="none"
autoCorrect="off"
value={creds.username}
onChange={({ target }) =>
set((c) => ({ ...c, username: target.value }))
}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
value={creds.password}
onChange={({ target }) =>
set((c) => ({ ...c, password: target.value }))
}
onKeyDown={(e) => e.key === "Enter" && login(creds)}
/>
</div>
</CardContent>
@@ -212,9 +195,7 @@ export default function Login() {
{show_sign_up && (
<Button
variant="outline"
type="button"
value="signup"
onClick={handleSignUp}
onClick={() => signup(creds)}
disabled={signupPending}
>
Sign Up
@@ -222,27 +203,26 @@ export default function Login() {
)}
<Button
variant="default"
type="button"
value="login"
onClick={handleLogin}
onClick={() => login(creds)}
disabled={loginPending}
>
Log In
</Button>
</CardFooter>
</form>
</>
)}
{no_auth_configured && (
<CardContent className="w-full gap-2 text-muted-foreground text-sm">
No login methods have been configured. See the
<a
href="https://github.com/moghtech/komodo/blob/main/config/core.config.toml"
target="_blank"
rel="noreferrer"
className="text-sm py-0 px-1 underline"
>
example config
</a>
<Button variant="link" className="text-sm py-0 px-1">
<a
href="https://github.com/moghtech/komodo/blob/main/config/core.config.toml"
target="_blank"
className="flex text-sm"
>
example config
</a>
</Button>
for information on configuring auth.
</CardContent>
)}
+12 -43
View File
@@ -10,17 +10,14 @@ import {
useSetTitle,
useTemplatesQueryBehavior,
useUser,
useLocalStorage,
} from "@lib/hooks";
import { Types } from "komodo_client";
import { Input } from "@ui/input";
import { Button } from "@ui/button";
import { useState } from "react";
import { Search, Eye, EyeOff } from "lucide-react";
import { Search } from "lucide-react";
import { NotFound, TemplateQueryBehaviorSelector } from "@components/util";
import { Switch } from "@ui/switch";
import { UsableResource } from "@types";
import { ServerMonitoringTable } from "@components/resources/server/monitoring-table";
export default function Resources({ _type }: { _type?: UsableResource }) {
const is_admin = useUser().data?.admin ?? false;
@@ -31,10 +28,6 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
const name = type === "ResourceSync" ? "Resource Sync" : type;
useSetTitle(name + "s");
const [search, set] = useState("");
const [monitoring, setMonitoring] = useLocalStorage<boolean>(
"servers-monitoring-toggle-v1",
false
);
const [filter_update_available, toggle_filter_update_available] =
useFilterByUpdateAvailable();
const query =
@@ -63,7 +56,12 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
return <NotFound type={undefined} />;
}
const targets = filtered?.map((resource) => ({ type, id: resource.id }));
const targets = filtered?.map(
(resource): Types.ResourceTarget => ({
type,
id: resource.id,
})
);
return (
<Page
@@ -74,36 +72,13 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
</div>
}
icon={<Components.BigIcon />}
actions={
<div className="flex items-center gap-2">
{type === "Server" && (
<Button
variant="outline"
className="flex items-center gap-2"
onClick={() => setMonitoring(!monitoring)}
>
{monitoring ? (
<>
<EyeOff className="w-4 h-4" />
Hide Server Stats
</>
) : (
<>
<Eye className="w-4 h-4" />
Show Server Stats
</>
)}
</Button>
)}
<ExportButton targets={targets} />
</div>
}
actions={<ExportButton targets={targets} />}
>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-4 items-center justify-between">
<div className="flex gap-4">
{(is_admin || !disable_non_admin_create) && <Components.New />}
{!(type === "Server" && monitoring) && <Components.GroupActions />}
<Components.GroupActions />
</div>
<div className="flex items-center gap-4 flex-wrap">
{(type === "Stack" || type === "Deployment") && (
@@ -115,10 +90,8 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
<Switch checked={filter_update_available} />
</div>
)}
{!(type === "Server" && monitoring) && (
<TemplateQueryBehaviorSelector />
)}
{!(type === "Server" && monitoring) && <TagsFilter />}
<TemplateQueryBehaviorSelector />
<TagsFilter />
<div className="relative">
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
<Input
@@ -130,11 +103,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
</div>
</div>
</div>
{type === "Server" && monitoring ? (
<ServerMonitoringTable search={search} />
) : (
<Components.Table resources={filtered ?? []} />
)}
<Components.Table resources={filtered ?? []} />
</div>
</Page>
);
+36 -56
View File
@@ -3,7 +3,7 @@ import { LOGIN_TOKENS, useAuth, useUser } from "@lib/hooks";
import UpdatePage from "@pages/update";
import { Loader2 } from "lucide-react";
import { lazy, Suspense } from "react";
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
import { BrowserRouter, Route, Routes } from "react-router-dom";
// Lazy import pages
const Resources = lazy(() => import("@pages/resources"));
@@ -63,6 +63,8 @@ const useExchangeToken = () => {
};
export const Router = () => {
const { data: user, isLoading, error } = useUser();
// Handle exchange token loop to avoid showing login flash
const exchangeTokenPending = useExchangeToken();
if (exchangeTokenPending) {
@@ -73,6 +75,11 @@ export const Router = () => {
);
}
if (isLoading && !user) return null;
if (!user || error) return <Login />;
if (!user.enabled) return <UserDisabled />;
return (
<Suspense
fallback={
@@ -84,36 +91,34 @@ export const Router = () => {
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Layout />}>
<Route path="" element={<Home />} />
<Route path="settings" element={<Settings />} />
<Route path="tree" element={<Tree />} />
<Route path="containers" element={<ContainersPage />} />
<Route path="resources" element={<AllResources />} />
<Route path="schedules" element={<SchedulesPage />} />
<Route path="alerts" element={<AlertsPage />} />
<Route path="user-groups/:id" element={<UserGroupPage />} />
<Route path="users/:id" element={<UserPage />} />
<Route path="updates">
<Route path="" element={<UpdatesPage />} />
<Route path=":id" element={<UpdatePage />} />
</Route>
<Route path=":type">
<Route path="" element={<Resources />} />
<Route path=":id" element={<Resource />} />
<Route
path=":id/service/:service"
element={<StackServicePage />}
/>
<Route
path=":id/container/:container"
element={<ContainerPage />}
/>
<Route path=":id/network/:network" element={<NetworkPage />} />
<Route path=":id/image/:image" element={<ImagePage />} />
<Route path=":id/volume/:volume" element={<VolumePage />} />
</Route>
<Route path="/" element={<Layout />}>
<Route path="" element={<Home />} />
<Route path="settings" element={<Settings />} />
<Route path="tree" element={<Tree />} />
<Route path="containers" element={<ContainersPage />} />
<Route path="resources" element={<AllResources />} />
<Route path="schedules" element={<SchedulesPage />} />
<Route path="alerts" element={<AlertsPage />} />
<Route path="user-groups/:id" element={<UserGroupPage />} />
<Route path="users/:id" element={<UserPage />} />
<Route path="updates">
<Route path="" element={<UpdatesPage />} />
<Route path=":id" element={<UpdatePage />} />
</Route>
<Route path=":type">
<Route path="" element={<Resources />} />
<Route path=":id" element={<Resource />} />
<Route
path=":id/service/:service"
element={<StackServicePage />}
/>
<Route
path=":id/container/:container"
element={<ContainerPage />}
/>
<Route path=":id/network/:network" element={<NetworkPage />} />
<Route path=":id/image/:image" element={<ImagePage />} />
<Route path=":id/volume/:volume" element={<VolumePage />} />
</Route>
</Route>
</Routes>
@@ -124,29 +129,4 @@ export const Router = () => {
// return <RouterProvider router={ROUTER} />;
};
const RequireAuth = () => {
const { data: user, error } = useUser();
const location = useLocation();
if (error) {
if (location.pathname === "/") {
return <Navigate to="/login" replace />;
}
const backto = encodeURIComponent(location.pathname + location.search);
return <Navigate to={`/login?backto=${backto}`} replace />;
}
if (!user) {
return (
<div className="w-screen h-screen flex justify-center items-center">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (!user.enabled) return <UserDisabled />;
return <Outlet />;
};
-161
View File
@@ -1,161 +0,0 @@
import * as React from "react";
import { Check, ChevronsUpDown, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/ui/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover";
import { Skeleton } from "@/ui/skeleton";
interface MultiSelectProps {
options?: { label: string; value: string }[];
value: string[];
onChange: (selected: string[]) => void;
placeholder?: string;
className?: string;
isLoading?: boolean;
disabled?: boolean;
}
function MultiSelect({
options,
value,
onChange,
placeholder = "Select items...",
className,
isLoading = false,
disabled = false,
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false);
const handleUnselect = (item: string) => {
onChange(value.filter((i) => i !== item));
};
const handleSelect = (item: string) => {
if (value.includes(item)) {
handleUnselect(item);
} else {
onChange([...value, item]);
}
};
return (
<div className={cn("w-full", className)}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
className={cn(
"flex h-full w-full transition-all items-center justify-between rounded-md border border-input bg-background text-sm",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
"hover:bg-accent hover:text-accent-foreground"
)}
disabled={disabled}
aria-expanded={open}
>
<div className="flex justify-between flex-1 overflow-hidden">
<div
className="flex gap-1 flex-1 py-2 px-3 overflow-x-auto"
style={{
scrollbarWidth: "thin",
scrollbarColor: "hsl(var(--border)) transparent",
}}
>
{value.length === 0 ? (
<span className="text-muted-foreground truncate">
{placeholder}
</span>
) : (
value.map((item) => {
const option = options?.find((opt) => opt.value === item);
return (
<Badge key={item} variant="default" className="text-xs">
{option?.label}
<span
role="button"
tabIndex={0}
className="ml-1 hover:bg-destructive transition-all hover:text-destructive-foreground rounded-full p-0.5"
onKeyDown={(e) =>
e.key === "Enter" && handleUnselect(item)
}
onClick={() => handleUnselect(item)}
>
<X className="h-3 w-3" />
</span>
</Badge>
);
})
)}
</div>
<hr className="border-l border-border bg-red-300 h-6 mx-0.5 my-auto" />
<span
role="button"
onClick={(e) => {
e.stopPropagation();
setOpen((prev) => !prev);
}}
tabIndex={0}
className={cn(
"p-1 mx-1.5 my-auto h-full outline-none",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"hover:bg-accent/50 rounded-sm cursor-pointer"
)}
>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput autoFocus={false} placeholder="Search items..." />
<CommandList>
<CommandEmpty className="p-0">
{isLoading ? (
<div className="p-2">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton
key={index}
className="h-4 w-full mb-1 last:mb-0"
/>
))}
</div>
) : (
<div className="text-center text-sm py-4 text-muted-foreground">
No items found.
</div>
)}
</CommandEmpty>
<CommandGroup>
{options?.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => handleSelect(option.value)}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(option.value)
? "opacity-100"
: "opacity-0"
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
}
export {MultiSelect}
-15
View File
@@ -1,15 +0,0 @@
import { cn } from "@lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }
-91
View File
@@ -2197,11 +2197,6 @@ json5@^2.2.3:
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonc-parser@^3.0.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4"
integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
@@ -2293,49 +2288,6 @@ monaco-editor@0.52.2:
resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz"
integrity sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==
monaco-languageserver-types@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz#8f3414c1ebba786b9c9db45857cc853e50e768e8"
integrity sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==
dependencies:
monaco-types "^0.1.0"
vscode-languageserver-protocol "^3.0.0"
vscode-uri "^3.0.0"
monaco-marker-data-provider@^1.0.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/monaco-marker-data-provider/-/monaco-marker-data-provider-1.2.4.tgz#56fbeede5bd830ec2ee15b2aea9a7df62fa447a7"
integrity sha512-4DsPgsAqpTyUDs3humXRBPUJoihTv+L6v9aupQWD80X2YXaCXUd11mWYeSCYHuPgdUmjFaNWCEOjQ6ewf/QA1Q==
dependencies:
monaco-types "^0.1.0"
monaco-types@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/monaco-types/-/monaco-types-0.1.0.tgz#3a3066aba499cb5923cd60efc736f3f14a169e10"
integrity sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==
monaco-worker-manager@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a"
integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==
monaco-yaml@5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.4.0.tgz#c0ed6226c710c32c509e7c316c9a45f004862088"
integrity sha512-tuBVDy1KAPrgO905GHTItu8AaA5bIzF5S4X0JVRAE/D66FpRhkDUk7tKi5bwKMVTTugtpMLsXN4ewh4CgE/FtQ==
dependencies:
jsonc-parser "^3.0.0"
monaco-languageserver-types "^0.4.0"
monaco-marker-data-provider "^1.0.0"
monaco-types "^0.1.0"
monaco-worker-manager "^2.0.0"
path-browserify "^1.0.0"
prettier "^3.0.0"
vscode-languageserver-textdocument "^1.0.0"
vscode-languageserver-types "^3.0.0"
vscode-uri "^3.0.0"
yaml "^2.0.0"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@@ -2428,11 +2380,6 @@ parse-srcset@^1.0.2:
resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz"
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
path-browserify@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@@ -2539,11 +2486,6 @@ prettier@3.5.3:
resolved "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz"
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
prettier@^3.0.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
@@ -3022,34 +2964,6 @@ vite@6.0.7:
optionalDependencies:
fsevents "~2.3.3"
vscode-jsonrpc@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
vscode-languageserver-protocol@^3.0.0:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
dependencies:
vscode-jsonrpc "8.2.0"
vscode-languageserver-types "3.17.5"
vscode-languageserver-textdocument@^1.0.0:
version "1.0.12"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631"
integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==
vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.0.0:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
vscode-uri@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
@@ -3080,11 +2994,6 @@ yallist@^3.0.2:
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yaml@^2.0.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79"
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
yaml@^2.3.4:
version "2.8.0"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz"
+2 -3
View File
@@ -55,9 +55,8 @@ pub async fn copy(
}
}
if !buffer.is_empty() {
bulk_update_retry_too_big(&target_db, &collection, &buffer, true)
.await
.context("Failed to flush documents")?;
bulk_update_retry_too_big(&target_db, &collection, &buffer, true).await.context("Failed to flush documents")?;
}
anyhow::Ok(count)
}