Compare commits

..
4 Commits
Author SHA1 Message Date
a65fd4dca7 1.19.3 (#792)
* start. 1.19.3

* deploy 1.19.3-dev-1

* repo state from db includes BuildRepo success

* clean up version mismatch text

* feat(containers): debounced search input and added filter by server name (#796)

* Fix cleaning Alerter resource whitelist / blacklist on resource delete re #581

* fmt

* Fix signup button not working correctly (#801)

* Improve route protection and authentication flow (#798)

* Improve route protection and authentication flow

* Cleanup

* fix: inconsistent behaviour of new resource create button (#800)

* fix monaco crashing with absolute path config files

* deploy 1.19.3-dev-2

* proofread config

* Fix #427

* deploy 1.19.3-dev-3

* poll logs use println

* Sync: Only show commit / execute when viewing pending tab

* Improve sync UX

* deploy 1.19.3-dev-4

* bold link

* remove claims about database resource usage.

* 1.19.3

---------

Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Antonio Sarro <tech@antoniosarro.dev>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
2025-09-05 13:41:58 -07:00
mbecker20 0873104b5a fmt 2025-08-31 19:13:14 -07:00
9a7b6ebd51 1.19.2 (#764)
* 1.19.2-dev-0

* deploy 1.19.2-dev-1

* Add option to make run command detachable (#766)

* improve missing files log to include the missing paths

* bump mungos for urlencoding mongo creds

* Update permissioning.md - typo: "priviledges" -> "privileges" (#770)

* Add support for monaco-yaml and docker compose spec validatiaon (#772)

* deploy 1.19.2-dev-2

* on delete user, remove from all user groups

* fix Google login issues around `picture`

* unsafe_unsanitized_startup_config

* improve git provider support re #355

* should fix #468

* should fix exit code re #597

* deploy 1.19.2-dev-3

* fix container ports sorting (#776)

* missing serde default

* deploy 1.19.2-dev-4

* ensure git tokens trimmed in remote url

* Add link to Authentik support docs

* Fix incorrect commit branch when using linked repo re #634

* Better display container port ranges (#786)

* ensure build and sync also commit to correct branch. re #634

* deploy 1.19.2-dev-5

* Improve login form (#788)

* Use proper form for login, add autocomplete and names to input fields

* Do not return null if loading

* Remove unused function

* Cleanup and streamline

* improve login screen flash on reload

* first builder given same name as first server

* 1.19.2

---------

Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Brian Bradley <brian.bradley.p@gmail.com>
Co-authored-by: Ravi Wolter-Krishan <rkn@gedikas.net>
Co-authored-by: Christopher Hoage <iam@chrishoage.com>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
2025-08-31 19:08:45 -07:00
mbecker20 a4153fa28b fix UI showing Redeploy when its actually None 2025-08-24 14:30:43 -07:00
60 changed files with 2939 additions and 403 deletions
Generated
+94 -71
View File
@@ -118,10 +118,12 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-compression"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75"
dependencies = [
"compression-codecs",
"compression-core",
"flate2",
"futures-core",
"memchr",
@@ -267,9 +269,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.160.0"
version = "1.161.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4300d66be2c4e275d3a037576dacce6efaf9af24bfbd2460a848ea39299c979c"
checksum = "239393d2a430bd37a7a39d8467674b816f1c864e4bfe874b01a0c9e0d49be4ff"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -744,7 +746,7 @@ version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cexpr",
"clang-sys",
"itertools 0.12.1",
@@ -769,9 +771,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]]
name = "bitvec"
@@ -861,7 +863,7 @@ dependencies = [
"getrandom 0.2.16",
"getrandom 0.3.3",
"hex",
"indexmap 2.10.0",
"indexmap 2.11.0",
"js-sys",
"once_cell",
"rand 0.9.2",
@@ -905,7 +907,7 @@ dependencies = [
[[package]]
name = "cache"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"tokio",
@@ -913,9 +915,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.33"
version = "1.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
dependencies = [
"jobserver",
"libc",
@@ -1072,19 +1074,38 @@ dependencies = [
[[package]]
name = "command"
version = "1.19.1"
version = "1.19.3"
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.1"
version = "1.19.3"
dependencies = [
"colored",
"indexmap 2.10.0",
"indexmap 2.11.0",
"regex",
"serde",
"serde_json",
@@ -1193,7 +1214,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossterm_winapi",
"parking_lot 0.12.4",
"rustix",
@@ -1321,7 +1342,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "database"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"async-compression",
@@ -1620,7 +1641,7 @@ dependencies = [
[[package]]
name = "environment"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"formatting",
@@ -1630,7 +1651,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"thiserror 2.0.16",
]
@@ -1720,7 +1741,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"serror",
]
@@ -1882,7 +1903,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"cache",
@@ -1923,7 +1944,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.10.0",
"indexmap 2.11.0",
"slab",
"tokio",
"tokio-util",
@@ -1942,7 +1963,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.3.1",
"indexmap 2.10.0",
"indexmap 2.11.0",
"slab",
"tokio",
"tokio-util",
@@ -2458,9 +2479,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
@@ -2490,7 +2511,7 @@ dependencies = [
[[package]]
name = "interpolate"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"komodo_client",
@@ -2499,11 +2520,11 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cfg-if",
"libc",
]
@@ -2586,9 +2607,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -2621,7 +2642,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"chrono",
@@ -2646,7 +2667,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2658,7 +2679,7 @@ dependencies = [
"derive_variants",
"envy",
"futures",
"indexmap 2.10.0",
"indexmap 2.11.0",
"ipnetwork",
"mongo_indexed",
"partial_derive2",
@@ -2675,12 +2696,13 @@ dependencies = [
"tokio-util",
"tracing",
"typeshare",
"urlencoding",
"uuid",
]
[[package]]
name = "komodo_core"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"arc-swap",
@@ -2712,7 +2734,7 @@ dependencies = [
"git",
"hex",
"hmac",
"indexmap 2.10.0",
"indexmap 2.11.0",
"interpolate",
"jsonwebtoken",
"komodo_client",
@@ -2750,7 +2772,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"arc-swap",
@@ -2872,7 +2894,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"komodo_client",
@@ -3037,9 +3059,9 @@ dependencies = [
[[package]]
name = "mongodb"
version = "3.2.4"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f8c69f13acf07eae386a2974f48ffd9187ea2aba8defbea9aa34e7e272c5f3"
checksum = "f6e3788f35159bbcf461227af84711950525343b64451fdd90de4e237a0b8a13"
dependencies = [
"async-trait",
"base64 0.13.1",
@@ -3086,9 +3108,9 @@ dependencies = [
[[package]]
name = "mongodb-internal-macros"
version = "3.2.4"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9202de265a3a8bbb43f9fe56db27c93137d4f9fb04c093f47e9c7de0c61ac7d"
checksum = "fdfb13b7c0436b3e395f4ce5f4366431b9c9db47adc4cb33afc6b5b913d44969"
dependencies = [
"macro_magic",
"proc-macro2",
@@ -3098,9 +3120,9 @@ dependencies = [
[[package]]
name = "mungos"
version = "3.2.1"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc7db433516e852091e2ede8cd755a717223a359b519b22710e56225a5d6a9"
checksum = "f77d8548efe9a10b49e1c92fa719d2e4e27cc6b40936f05fbb0f6a7ac31d3c0f"
dependencies = [
"anyhow",
"envy",
@@ -3108,6 +3130,7 @@ dependencies = [
"mongodb",
"serde",
"serde_derive",
"urlencoding",
]
[[package]]
@@ -3116,7 +3139,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
@@ -3259,7 +3282,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
@@ -3614,7 +3637,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "periphery_client"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"komodo_client",
@@ -3947,7 +3970,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
@@ -3972,9 +3995,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
@@ -3984,9 +4007,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
@@ -3995,15 +4018,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "reqwest"
@@ -4145,7 +4168,7 @@ dependencies = [
[[package]]
name = "response"
version = "1.19.1"
version = "1.19.3"
dependencies = [
"anyhow",
"axum",
@@ -4259,7 +4282,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"errno",
"libc",
"linux-raw-sys",
@@ -4477,7 +4500,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
@@ -4490,7 +4513,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -4569,7 +4592,7 @@ version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"indexmap 2.10.0",
"indexmap 2.11.0",
"itoa",
"memchr",
"ryu",
@@ -4648,7 +4671,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.10.0",
"indexmap 2.11.0",
"schemars 0.9.0",
"schemars 1.0.4",
"serde",
@@ -4676,7 +4699,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
dependencies = [
"indexmap 2.10.0",
"indexmap 2.11.0",
"itoa",
"ryu",
"serde",
@@ -5005,7 +5028,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -5257,7 +5280,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap 2.10.0",
"indexmap 2.11.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5338,7 +5361,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.10.0",
"indexmap 2.11.0",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -5355,7 +5378,7 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"bytes",
"futures-core",
"futures-util",
@@ -5618,9 +5641,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.6"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -6242,9 +6265,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
[[package]]
name = "winreg"
@@ -6271,7 +6294,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
+7 -7
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.19.1"
version = "1.19.3"
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.1"
mungos = "3.2.2"
svi = "1.2.0"
# ASYNC
@@ -65,9 +65,10 @@ axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.10.0", features = ["serde"] }
indexmap = { version = "2.11.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"
@@ -112,7 +113,7 @@ sysinfo = "0.37.0"
# CLOUD
aws-config = "1.8.5"
aws-sdk-ec2 = "1.160.0"
aws-sdk-ec2 = "1.161.0"
aws-credential-types = "1.2.5"
## CRON
@@ -122,7 +123,7 @@ chrono = "0.4.41"
croner = "3.0.0"
# MISC
async-compression = { version = "0.4.27", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.28", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.1.4"
typeshare = "1.0.4"
@@ -130,7 +131,6 @@ octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
regex = "1.11.2"
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)
};
info!("Link: '{}'", link.bold());
println!("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 {
info!(
println!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION SUCCESSFUL".green(),
);
} else {
warn!(
eprintln!(
"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}) | Server version now matches core version ✅\n{link}"
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}** ({region}) | Version mismatch detected ⚠️\nServer: **{server_version}** | Core: **{core_version}**\n{link}"
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{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}) | Server version now matches core version ✅\n{link}"
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}\n{link}"
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {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}) | Server version now matches core version ✅"
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
)
}
_ => {
format!(
"{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}"
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
)
}
};
+1
View File
@@ -1033,6 +1033,7 @@ 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,
+11 -9
View File
@@ -186,7 +186,7 @@ async fn write_dockerfile_contents_git(
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut clone_args: RepoExecutionArgs = if !build
let mut repo_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 = clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(root.display().to_string());
let root = repo_args.unique_path(&core_config().repo_directory)?;
repo_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) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
let access_token = if let Some(account) = &repo_args.account {
git_token(&repo_args.provider, account, |https| repo_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", repo_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,
&clone_args,
&repo_args,
access_token.as_deref(),
&mut update.logs,
)
@@ -249,9 +249,11 @@ 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(
clone_args,
repo_args,
&core_config().repo_directory,
access_token,
)
@@ -298,7 +300,7 @@ async fn write_dockerfile_contents_git(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&build.config.branch,
&branch,
)
.await;
+3 -1
View File
@@ -284,6 +284,8 @@ 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,
@@ -334,7 +336,7 @@ async fn write_stack_file_contents_git(
&format!("{username}: Write Stack File"),
&root,
&file_path,
&stack.config.branch,
&branch,
)
.await;
+3 -1
View File
@@ -323,6 +323,8 @@ 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,
@@ -373,7 +375,7 @@ async fn write_sync_file_contents_git(
&format!("{}: Commit Resource File", args.user.username),
&root,
&resource_path.join(&file_path),
&sync.config.branch,
&branch,
)
.await;
+8
View File
@@ -220,6 +220,14 @@ 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)
}
}
+3 -2
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,5 +207,6 @@ pub struct GoogleUser {
#[serde(rename = "sub")]
pub id: String,
pub email: String,
#[serde(default)]
pub picture: String,
}
+1
View File
@@ -250,6 +250,7 @@ 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),
+8 -4
View File
@@ -45,10 +45,14 @@ async fn app() -> anyhow::Result<()> {
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
if core_config().pretty_startup_config {
info!("{:#?}", config.sanitized());
} else {
info!("{:?}", config.sanitized());
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()),
}
// Init jwt client to crash on failure
+14 -10
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
image.to_string()
} else {
format!("{image}:latest")
}
@@ -92,6 +92,9 @@ 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)
@@ -250,20 +253,21 @@ 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
image.to_string()
} else {
image + ":latest"
format!("{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
};
+29 -3
View File
@@ -853,9 +853,15 @@ pub async fn delete<T: KomodoResource>(
);
update.push_simple_log("Deleted Toml", toml);
if let Err(e) = T::post_delete(&resource, &mut update).await {
update.push_error_log("post delete", format_serror(&e.into()));
}
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)
);
refresh_all_resources_cache().await;
@@ -865,6 +871,26 @@ 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,6 +300,7 @@ 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: String::from("Local"),
name: config.first_server_name.clone(),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
+4
View File
@@ -712,6 +712,7 @@ impl Resolve<super::Args> for ComposeRun {
command,
no_tty,
no_deps,
detach,
service_ports,
env,
workdir,
@@ -783,6 +784,9 @@ 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
+2 -1
View File
@@ -43,8 +43,9 @@ pub async fn validate_files(
"Validate Files",
format_serror(
&anyhow!(
"Ensure the run_directory and all file paths are correct."
"Missing files: {}", res.missing_files.join(", ")
)
.context("Ensure the run_directory and all file paths are correct.")
.context("A file doesn't exist after writing stack.")
.into(),
),
+2 -1
View File
@@ -2,7 +2,8 @@ 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, SystemLoadAverage,
SystemProcess, SystemStats,
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
use tokio::sync::RwLock;
+1
View File
@@ -28,6 +28,7 @@ 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,6 +379,9 @@ 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,6 +125,8 @@ 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>,
@@ -509,6 +511,12 @@ 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 =
// ===========
@@ -729,6 +737,7 @@ 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(),
@@ -772,6 +781,8 @@ 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,
+11 -2
View File
@@ -511,8 +511,17 @@ impl RepoExecutionArgs {
&self,
access_token: Option<&str>,
) -> anyhow::Result<String> {
let access_token_at = match &access_token {
Some(token) => format!("token:{token}@"),
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()))
}
},
None => String::new(),
};
let protocol = if self.https { "https" } else { "http" };
+1 -1
View File
@@ -191,4 +191,4 @@ pub struct SystemLoadAverage {
pub five: f64,
/// 15m load average
pub fifteen: f64,
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.19.1",
"version": "1.19.3",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
+2
View File
@@ -7713,6 +7713,8 @@ 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,6 +272,9 @@ 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`,
## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`,
## as the default config at `/config/.default.config.toml`.
## Komodo can start with no external config file mounted.
## Komodo Core 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 preffered. You can convert here:
## Can also use JSON or YAML if preferred. 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 preffered. You can convert here:
## You can also use JSON or YAML if preferred. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+1 -1
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 preffered. You can convert here:
## You can also use JSON or YAML if preferred. You can convert here:
## - YAML: https://it-tools.tech/toml-to-yaml
## - JSON: https://it-tools.tech/toml-to-json
+2 -2
View File
@@ -71,7 +71,7 @@ permissions = [
## Administration
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}`.
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}`.
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,6 +14,10 @@ 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
+5 -3
View File
@@ -5,10 +5,12 @@ 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)
- Lower RAM usage, Higher CPU usage.
:::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.
:::
:::info
**FerretDB v1** users:
+1
View File
@@ -40,6 +40,7 @@
"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",
+2
View File
@@ -7306,6 +7306,8 @@ 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
+12 -4
View File
@@ -170,6 +170,7 @@ interface SectionProps {
actions?: ReactNode;
// otherwise items-start
itemsCenterTitleRow?: boolean;
className?: string;
}
export const Section = ({
@@ -180,8 +181,9 @@ export const Section = ({
actions,
children,
itemsCenterTitleRow,
className,
}: SectionProps) => (
<div className="flex flex-col gap-4">
<div className={cn("flex flex-col gap-4", className)}>
{(title || icon || titleRight || titleOther || actions) && (
<div
className={cn(
@@ -222,6 +224,7 @@ export const NewLayout = ({
}) => {
const [open, set] = useState(false);
const [loading, setLoading] = useState(false);
return (
<Dialog
open={open}
@@ -248,9 +251,14 @@ export const NewLayout = ({
variant="secondary"
onClick={async () => {
setLoading(true);
await onConfirm();
setLoading(false);
set(false);
try {
await onConfirm();
set(false);
} catch (error) {
console.error("Error creating resource:", error);
} finally {
setLoading(false);
}
}}
disabled={!enabled || loading}
>
+11
View File
@@ -56,12 +56,14 @@ 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;
@@ -170,6 +172,7 @@ export const MonacoEditor = ({
language={language}
value={value}
theme={theme}
defaultPath={defaultPath(filename)}
options={options}
onChange={(v) => onValueChange?.(v ?? "")}
onMount={(editor) => setEditor(editor)}
@@ -178,6 +181,14 @@ 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,6 +1132,7 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
command: undefined,
no_tty: undefined,
no_deps: undefined,
detach: undefined,
service_ports: undefined,
env: undefined,
workdir: undefined,
@@ -1151,6 +1152,7 @@ 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 ?? "");
@@ -1175,6 +1177,7 @@ 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 ?? "");
@@ -1216,6 +1219,7 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
user: user || undefined,
entrypoint: entrypoint || undefined,
pull: pull ? true : undefined,
detach: detach ? true : undefined,
env,
} as any);
setOpen(false);
@@ -1273,6 +1277,10 @@ 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, usePendingView } from ".";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
export const RefreshSync = ({ id }: { id: string }) => {
const inv = useInvalidate();
@@ -34,11 +34,10 @@ export const ExecuteSync = ({ id }: { id: string }) => {
{ refetchInterval: 5000 }
).data?.syncing;
const sync = useFullResourceSync(id);
const [_pendingView] = usePendingView();
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
const { view } = useResourceSyncTabsView(sync);
if (
pendingView === "Commit" ||
view !== "Execute" ||
!sync ||
sync_no_changes(sync) ||
!sync.info?.remote_contents
@@ -73,11 +72,12 @@ 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 (pendingView === "Execute" || !canWrite || !sync) return null;
if (view !== "Commit" || !canWrite || !sync) {
return null;
}
const freshSync =
!sync.config?.files_on_host &&
@@ -1,4 +1,4 @@
import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks";
import { atomWithStorage, useRead, useUser } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Card } from "@ui/card";
import { Clock, FolderSync } from "lucide-react";
@@ -45,23 +45,16 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
};
const pendingViewAtom = atomWithStorage<"Execute" | "Commit">(
"sync-view-v1",
"Execute"
type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit";
const syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(
"sync-tabs-v4",
"Config"
);
export const usePendingView = () => {
return useAtom(pendingViewAtom) as [
"Execute" | "Commit",
(view: "Execute" | "Commit") => void,
];
};
const ConfigInfoPending = ({ id }: { id: string }) => {
const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">(
"sync-tabs-v3",
"Config"
);
const sync = useFullResourceSync(id);
export const useResourceSyncTabsView = (
sync: Types.ResourceSync | undefined
) => {
const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);
const hideInfo = sync?.config?.files_on_host
? false
@@ -75,13 +68,28 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
const view =
_view === "Info" && hideInfo
? "Config"
: _view === "Pending" && !showPending
: (_view === "Execute" || _view === "Commit") && !showPending
? sync?.config?.files_on_host ||
sync?.config?.repo ||
sync?.config?.linked_repo
? "Info"
: "Config"
: _view;
: _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);
const title = (
<TabsList className="justify-start w-fit">
@@ -96,12 +104,21 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
Info
</TabsTrigger>
<TabsTrigger
value="Pending"
value="Execute"
className="w-[110px]"
disabled={!showPending}
>
Pending
Execute
</TabsTrigger>
{sync?.config?.managed && (
<TabsTrigger
value="Commit"
className="w-[110px]"
disabled={!showPending}
>
Commit
</TabsTrigger>
)}
</TabsList>
);
return (
@@ -112,7 +129,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
<TabsContent value="Info">
<ResourceSyncInfo id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Pending">
<TabsContent value="Execute">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
<TabsContent value="Commit">
<ResourceSyncPending id={id} titleOther={title} />
</TabsContent>
</Tabs>
@@ -164,7 +184,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
},
GroupActions: () => (
<GroupActions type="ResourceSync" actions={["RunSync"]} />
<GroupActions type="ResourceSync" actions={["RunSync", "CommitSync"]} />
),
Table: ({ resources }) => (
@@ -10,8 +10,7 @@ import { cn, sanitizeOnlySpan } from "@lib/utils";
import { ConfirmButton } from "@components/util";
import { SquarePlay } from "lucide-react";
import { usePermissions } from "@lib/hooks";
import { useFullResourceSync, usePendingView } from ".";
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
import { useFullResourceSync, useResourceSyncTabsView } from ".";
import { ResourceDiff } from "komodo_client/dist/types";
export const ResourceSyncPending = ({
@@ -24,31 +23,16 @@ 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}
>
<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>
<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>
<div className="flex items-center gap-1 flex-wrap">
{pendingView === "Execute" && (
{view === "Execute" && (
<>
Update resources in the
<div className="font-bold">UI</div>
@@ -56,7 +40,7 @@ export const ResourceSyncPending = ({
<div className="font-bold">file changes.</div>
</>
)}
{pendingView === "Commit" && (
{view === "Commit" && (
<>
Update resources in the
<div className="font-bold">file</div>
@@ -89,7 +73,7 @@ export const ResourceSyncPending = ({
) : undefined}
{/* Pending Deploy */}
{pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
{view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
<Card>
<CardHeader
className={cn(
@@ -118,13 +102,10 @@ 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,
pendingView === "Commit"
)
diff_type_intention(update.data.type, view === "Commit")
)}
>
{pendingView === "Commit"
{view === "Commit"
? reverse_pending_type(update.data.type)
: update.data.type}{" "}
{update.target.type}
@@ -139,7 +120,7 @@ export const ResourceSyncPending = ({
/>
)}
</div>
{canExecute && pendingView === "Execute" && (
{canExecute && view === "Execute" && (
<ConfirmButton
title="Execute Change"
icon={<SquarePlay className="w-4 h-4" />}
@@ -168,7 +149,7 @@ export const ResourceSyncPending = ({
)}
{update.data.type === "Update" && (
<>
{pendingView === "Execute" && (
{view === "Execute" && (
<MonacoDiffEditor
original={update.data.data.current}
modified={update.data.data.proposed}
@@ -176,7 +157,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{pendingView === "Commit" && (
{view === "Commit" && (
<MonacoDiffEditor
original={update.data.data.proposed}
modified={update.data.data.current}
@@ -205,13 +186,11 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, pendingView === "Commit")
diff_type_intention(data.type, view === "Commit")
)
)}
>
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
Variable
</CardHeader>
<CardContent>
@@ -224,7 +203,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{pendingView === "Execute" && (
{view === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -232,7 +211,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{pendingView === "Commit" && (
{view === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -261,13 +240,11 @@ export const ResourceSyncPending = ({
className={cn(
"font-mono pb-2",
text_color_class_by_intention(
diff_type_intention(data.type, pendingView === "Commit")
diff_type_intention(data.type, view === "Commit")
)
)}
>
{pendingView === "Commit"
? reverse_pending_type(data.type)
: data.type}{" "}
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
User Group
</CardHeader>
<CardContent>
@@ -280,7 +257,7 @@ export const ResourceSyncPending = ({
)}
{data.type === "Update" && (
<>
{pendingView === "Execute" && (
{view === "Execute" && (
<MonacoDiffEditor
original={data.data.current}
modified={data.data.proposed}
@@ -288,7 +265,7 @@ export const ResourceSyncPending = ({
readOnly
/>
)}
{pendingView === "Commit" && (
{view === "Commit" && (
<MonacoDiffEditor
original={data.data.proposed}
modified={data.data.current}
@@ -3,13 +3,19 @@ import { useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { useMemo } from "react";
import { useStatsGranularity } from "./hooks";
import { Loader2 } from "lucide-react";
import { Loader2, OctagonAlert } 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"
| "Load Average";
type StatDatapoint = { date: number; value: number };
@@ -35,15 +41,15 @@ export const StatChart = ({
if (type === "Load Average") {
const one = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: (s.load_average?.one ?? 0),
value: s.load_average?.one ?? 0,
}));
const five = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: (s.load_average?.five ?? 0),
value: s.load_average?.five ?? 0,
}));
const fifteen = records.map((s) => ({
date: convertTsMsToLocalUnixTsInMs(s.ts),
value: (s.load_average?.fifteen ?? 0),
value: s.load_average?.fifteen ?? 0,
}));
return [
{ label: "1m", data: one },
@@ -65,9 +71,13 @@ 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}
) : (
<InnerStatChart
type={type}
stats={seriesData.flatMap((s) => s.data)}
seriesData={seriesData}
/>
)}
</div>
);
};
@@ -113,10 +123,12 @@ 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 allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) =>
s.data.map((d) => d.value)
);
const maxStatValue = Math.max(...(allValues.length ? allValues : [0]));
const { unit, maxUnitValue } = useMemo(() => {
@@ -133,7 +145,10 @@ export const InnerStatChart = ({
}
if (type === "Load Average") {
// Leave unitless; set max slightly above observed
return { unit: "", maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2 };
return {
unit: "",
maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,
};
}
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
}, [type, maxStatValue]);
@@ -161,6 +176,16 @@ 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={{
@@ -1,13 +1,7 @@
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";
@@ -336,6 +330,11 @@ 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}
@@ -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,16 +495,28 @@ const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
);
};
const LOAD_AVERAGE = ({ id, stats }: { id: string; 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 cores = useRead("GetSystemInformation", { server: id }).data
?.core_count;
const pct = (load: number) => (cores && cores > 0) ? Math.min((load / cores) * 100, 100) : undefined;
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 p <= 50
? "text-green-600"
: p <= 80
? "text-yellow-600"
: "text-red-600";
};
return (
@@ -524,15 +530,18 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
{/* 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-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"}
{cores && cores > 0
? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`
: "N/A"}
</span>
</div>
<Progress
value={pct(one) ?? 0}
className="h-2"
/>
<Progress value={pct(one) ?? 0} className="h-2" />
</div>
{/* Time Intervals */}
@@ -546,14 +555,13 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
<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)}`}>
<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"
/>
<Progress value={pct(value as number) ?? 0} className="h-1" />
</div>
))}
</div>
@@ -879,6 +879,7 @@ 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}
@@ -1004,7 +1005,7 @@ const ConfigFiles = ({
/>
<RequiresSelector
requires={requires ?? Types.StackFileRequires.Redeploy}
requires={requires ?? Types.StackFileRequires.None}
set={(requires) => {
values[i] = { ...values[i], requires };
set({ config_files: [...values] });
@@ -263,6 +263,7 @@ 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>
+108 -50
View File
@@ -93,24 +93,6 @@ 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,
{
@@ -916,6 +898,23 @@ 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" />
),
@@ -1203,6 +1202,35 @@ 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,
@@ -1212,19 +1240,29 @@ export const ContainerPortLink = ({
ports: Types.Port[];
server_id: string | undefined;
}) => {
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;
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]}`;
return (
<Tooltip>
<TooltipTrigger>
@@ -1236,7 +1274,7 @@ export const ContainerPortLink = ({
<EthernetPort
className={cn("w-4 h-4", stroke_color_class_by_intention("Good"))}
/>
{host_port}
{display_text}
</a>
</TooltipTrigger>
<TooltipContent className="flex flex-col gap-2 w-fit">
@@ -1248,12 +1286,18 @@ export const ContainerPortLink = ({
<LinkIcon className="w-3 h-3" />
{link}
</a>
{ports.map((port, i) => (
{ports.slice(0, 10).map((port, i) => (
<div key={i} className="flex gap-2 text-sm text-muted-foreground">
<div>-</div>
<span>-</span>
<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>
);
@@ -1266,24 +1310,38 @@ export const ContainerPortsTableView = ({
ports: Types.Port[];
server_id: string | undefined;
}) => {
const map = useContainerPortsMap(ports);
const host_ports = Object.keys(map);
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;
}, []);
return (
<div className="flex items-center gap-x-1 flex-wrap">
{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>
);
})}
{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>
))}
</div>
);
};
+21
View File
@@ -805,3 +805,24 @@ 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;
}
+5 -1
View File
@@ -4,6 +4,7 @@ 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) {
@@ -19,6 +20,9 @@ self.MonacoEnvironment = {
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
if (label === "yaml") {
return new yamlWorker();
}
return new editorWorker();
},
};
@@ -32,6 +36,6 @@ import "./theme";
import "./yaml";
import "./toml";
import "./fancy_toml";
import "./shell"
import "./shell";
import "./key_value";
import "./string_list";
+16 -3
View File
@@ -1,4 +1,5 @@
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
@@ -252,11 +253,23 @@ const yaml_language = <monaco.languages.IMonarchLanguage>{
},
};
monaco.languages.register({ id: "yaml" });
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.setMonarchTokensProvider("yaml", yaml_language);
monaco.languages.setLanguageConfiguration("yaml", yaml_conf);
/// V1
// const yaml_conf: monaco.languages.LanguageConfiguration = {
// comments: {
@@ -345,4 +358,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);
+87 -33
View File
@@ -6,33 +6,66 @@ import {
StatusBadge,
} from "@components/util";
import { container_state_intention } from "@lib/color";
import { useRead } from "@lib/hooks";
import { useDebounce, useRead } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Input } from "@ui/input";
import { Box, Search } from "lucide-react";
import { MultiSelect } from "@ui/multi-select";
import { Box, Search, RotateCcw } from "lucide-react";
import { Button } from "@ui/button";
import { Fragment, useCallback, useMemo, useState } from "react";
export default function ContainersPage() {
const [search, setSearch] = useState("");
const searchSplit = search
const [selectedServers, setSelectedServers] = useState<string[]>([]);
const debouncedSearch = useDebounce(search, 300);
const searchSplit = debouncedSearch
.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) return true;
const lower = c.name.toLowerCase();
return searchSplit.every((search) => lower.includes(search));
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;
}),
[_containers, searchSplit]
[_containers, searchSplit, selectedServers]
);
const clearAllServers = useCallback(() => {
setSelectedServers([]);
}, []);
return (
<Page
title="Containers"
@@ -44,18 +77,45 @@ 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">
<div></div>
<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="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]"
className="pl-8 w-[200px] lg:w-[300px] py-0 h-10"
/>
</div>
</div>
<DataTable
data={containers ?? []}
tableKey="containers-page-v1"
@@ -162,6 +222,23 @@ 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" />
),
@@ -172,29 +249,6 @@ 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>
+47 -27
View File
@@ -15,7 +15,7 @@ import {
useLoginOptions,
useUserInvalidate,
} from "@lib/hooks";
import { useState } from "react";
import { useRef } 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 login user. '${message}'`,
title: `Failed to sign up user. '${message}'`,
variant: "destructive",
});
console.error(e);
} else {
toast({
title: "Failed to login user. See console log for details.",
title: "Failed to sign up user. See console log for details.",
variant: "destructive",
});
console.error(e);
@@ -98,7 +98,25 @@ 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 &&
@@ -110,7 +128,6 @@ 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
@@ -166,28 +183,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"
value={creds.username}
onChange={({ target }) =>
set((c) => ({ ...c, username: target.value }))
}
name="username"
autoComplete="username"
autoCapitalize="none"
autoCorrect="off"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
value={creds.password}
onChange={({ target }) =>
set((c) => ({ ...c, password: target.value }))
}
onKeyDown={(e) => e.key === "Enter" && login(creds)}
autoComplete="current-password"
/>
</div>
</CardContent>
@@ -195,7 +212,9 @@ export default function Login() {
{show_sign_up && (
<Button
variant="outline"
onClick={() => signup(creds)}
type="button"
value="signup"
onClick={handleSignUp}
disabled={signupPending}
>
Sign Up
@@ -203,26 +222,27 @@ export default function Login() {
)}
<Button
variant="default"
onClick={() => login(creds)}
type="button"
value="login"
onClick={handleLogin}
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
<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>
<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>
for information on configuring auth.
</CardContent>
)}
+56 -36
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, Route, Routes } from "react-router-dom";
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
// Lazy import pages
const Resources = lazy(() => import("@pages/resources"));
@@ -63,8 +63,6 @@ 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) {
@@ -75,11 +73,6 @@ export const Router = () => {
);
}
if (isLoading && !user) return null;
if (!user || error) return <Login />;
if (!user.enabled) return <UserDisabled />;
return (
<Suspense
fallback={
@@ -91,34 +84,36 @@ export const Router = () => {
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<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 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>
</Route>
</Routes>
@@ -129,4 +124,29 @@ 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
@@ -0,0 +1,161 @@
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
@@ -0,0 +1,15 @@
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,6 +2197,11 @@ 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"
@@ -2288,6 +2293,49 @@ 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"
@@ -2380,6 +2428,11 @@ 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"
@@ -2486,6 +2539,11 @@ 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"
@@ -2964,6 +3022,34 @@ 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"
@@ -2994,6 +3080,11 @@ 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"
+3 -2
View File
@@ -55,8 +55,9 @@ 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)
}