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
47 changed files with 262 additions and 2919 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.2"
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.2"
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.2"
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.2"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"async-compression",
@@ -1641,7 +1620,7 @@ dependencies = [
[[package]]
name = "environment"
version = "1.19.2"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"formatting",
@@ -1651,7 +1630,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.19.2"
version = "1.19.1-dev-14"
dependencies = [
"thiserror 2.0.16",
]
@@ -1741,7 +1720,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.19.2"
version = "1.19.1-dev-14"
dependencies = [
"serror",
]
@@ -1903,7 +1882,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.19.2"
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.2"
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.2"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"chrono",
@@ -2667,7 +2646,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.19.2"
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.2"
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.2"
version = "1.19.1-dev-14"
dependencies = [
"anyhow",
"arc-swap",
@@ -2894,7 +2872,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.19.2"
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.2"
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.2"
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.2"
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"
+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
};
+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.2",
"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
@@ -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
-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
-3
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={filename ? `file:///${filename}` : undefined}
options={options}
onChange={(v) => onValueChange?.(v ?? "")}
onMount={(editor) => setEditor(editor)}
@@ -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}
@@ -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>
);
};
@@ -9,7 +9,7 @@ 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 };
@@ -29,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}>
@@ -65,9 +49,10 @@ export const StatChart = ({
<div className="w-full max-w-full h-full flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : seriesData.length > 0 ? (
<InnerStatChart type={type} stats={seriesData.flatMap((s) => s.data)} seriesData={seriesData} />
) : null}
) : (
stats &&
stats.length > 0 && <InnerStatChart type={type} stats={stats} />
)}
</div>
);
};
@@ -79,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 =
@@ -116,8 +99,7 @@ export const InnerStatChart = ({
}, []);
// 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") {
@@ -131,10 +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]);
@@ -142,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: {
@@ -151,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>
),
},
@@ -164,17 +139,15 @@ export const InnerStatChart = ({
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,
@@ -120,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} />
@@ -347,11 +346,6 @@ export const ServerStats = ({
type="Disk"
className="w-full h-[250px]"
/>
<StatChart
server_id={id}
type="Load Average"
className="w-full h-[250px]"
/>
<StatChart
server_id={id}
type="Network Ingress"
@@ -501,68 +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>
);
};
+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);
-17
View File
@@ -162,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" />
),
+26 -38
View File
@@ -15,7 +15,7 @@ import {
useLoginOptions,
useUserInvalidate,
} from "@lib/hooks";
import { type FormEvent } from "react";
import { useState } from "react";
import { ThemeToggle } from "@ui/theme";
import { KOMODO_BASE_URL } from "@main";
import { KeyRound, X } from "lucide-react";
@@ -38,6 +38,7 @@ 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();
@@ -97,18 +98,7 @@ export default function Login() {
},
});
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const username = String(fd.get("username") ?? "");
const password = String(fd.get("password") ?? "");
const action = String(fd.get("action") ?? "login");
if (action === "signup") {
signup({ username, password });
} else {
login({ username, password });
}
};
const no_auth_configured =
options !== undefined &&
@@ -120,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
@@ -175,28 +166,28 @@ export default function Login() {
</div>
</CardHeader>
{options?.local && (
<form
onSubmit={handleSubmit}
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>
@@ -204,9 +195,7 @@ export default function Login() {
{show_sign_up && (
<Button
variant="outline"
type="submit"
name="action"
value="signup"
onClick={() => signup(creds)}
disabled={signupPending}
>
Sign Up
@@ -214,27 +203,26 @@ export default function Login() {
)}
<Button
variant="default"
type="submit"
name="action"
value="login"
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>
);
+4 -6
View File
@@ -63,7 +63,7 @@ const useExchangeToken = () => {
};
export const Router = () => {
const { data: user, error } = useUser();
const { data: user, isLoading, error } = useUser();
// Handle exchange token loop to avoid showing login flash
const exchangeTokenPending = useExchangeToken();
@@ -75,11 +75,9 @@ export const Router = () => {
);
}
// Only how login once error indicating logged out state actually recieved
if (error) return <Login />;
// Don't display anything if !error and !user. This is loading state.
if (!user) return null;
// Don't try displaying pages if user disabled, will fail to load with many errors.
if (isLoading && !user) return null;
if (!user || error) return <Login />;
if (!user.enabled) return <UserDisabled />;
return (
-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"