From 61e4d7def07d33e232e0a9f028af7c176dbc44a4 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 11:56:50 -0400 Subject: [PATCH 001/203] dockerfile builds use latest rust 1.96.0 --- bin/binaries.Dockerfile | 2 +- bin/chef.binaries.Dockerfile | 2 +- bin/cli/aio.Dockerfile | 2 +- bin/core/aio.Dockerfile | 2 +- bin/periphery/aio.Dockerfile | 2 +- example/alerter/Dockerfile | 2 +- example/update_logger/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/binaries.Dockerfile b/bin/binaries.Dockerfile index 18b3cef3e9..a3f0190075 100644 --- a/bin/binaries.Dockerfile +++ b/bin/binaries.Dockerfile @@ -1,7 +1,7 @@ ## Builds the Komodo Core, Periphery, and Util binaries ## for a specific architecture. Requires OpenSSL 3 or later. -FROM rust:1.95.0-bookworm AS builder +FROM rust:1.96.0-bookworm AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/chef.binaries.Dockerfile b/bin/chef.binaries.Dockerfile index 9933981b12..77e699bff8 100644 --- a/bin/chef.binaries.Dockerfile +++ b/bin/chef.binaries.Dockerfile @@ -3,7 +3,7 @@ ## Uses chef for dependency caching to help speed up back-to-back builds. -FROM lukemathwalker/cargo-chef:latest-rust-1.95.0-bookworm AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.96.0-bookworm AS chef WORKDIR /builder # Plan just the RECIPE to see if things have changed diff --git a/bin/cli/aio.Dockerfile b/bin/cli/aio.Dockerfile index df0aee90d0..723cf58df7 100644 --- a/bin/cli/aio.Dockerfile +++ b/bin/cli/aio.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie AS builder +FROM rust:1.96.0-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index 0db1611e8c..1ba1869ed1 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -1,7 +1,7 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. # Build Core -FROM rust:1.95.0-trixie AS core-builder +FROM rust:1.96.0-trixie AS core-builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/periphery/aio.Dockerfile b/bin/periphery/aio.Dockerfile index affb4d89ee..d4edd37c65 100644 --- a/bin/periphery/aio.Dockerfile +++ b/bin/periphery/aio.Dockerfile @@ -1,6 +1,6 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. -FROM rust:1.95.0-trixie AS builder +FROM rust:1.96.0-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/example/alerter/Dockerfile b/example/alerter/Dockerfile index 9ffd8a07f3..1ef087454f 100644 --- a/example/alerter/Dockerfile +++ b/example/alerter/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie as builder +FROM rust:1.96.0-trixie as builder WORKDIR /builder COPY . . diff --git a/example/update_logger/Dockerfile b/example/update_logger/Dockerfile index a4bc44fa1e..1e00187f62 100644 --- a/example/update_logger/Dockerfile +++ b/example/update_logger/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie as builder +FROM rust:1.96.0-trixie as builder WORKDIR /builder COPY . . From bf4bc28b1f86204c424dbc42b1f5a0c535194328 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 12:10:38 -0400 Subject: [PATCH 002/203] bump rust and js dependencies --- Cargo.lock | 299 ++++++++------- Cargo.toml | 38 +- client/core/ts/package.json | 2 +- ui/package.json | 44 +-- ui/yarn.lock | 729 +++++++++++++++++++----------------- 5 files changed, 578 insertions(+), 534 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7472d58a48..2dee3e4724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,9 +281,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.16" +version = "1.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" dependencies = [ "aws-credential-types", "aws-runtime", @@ -295,6 +295,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -346,9 +347,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.3" +version = "1.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" +checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -371,10 +372,11 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.224.0" +version = "1.232.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12b9fba5a25f668638edc00be062da3b123e3bf9ea07f13b681a19de097f2d8" +checksum = "3ed1c72b10833ce8a789a21c1b95df11387932d7e9ee9a2931635bdb6770a0ad" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -396,10 +398,11 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.98.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" +checksum = "8c82b3ac19f1431854f7ace3a7531674633e286bfdde21976893bfee36fd493b" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -420,10 +423,11 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.100.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" +checksum = "321000d2b4c5519ee573f73167f612efd7329322d9b26969ad1979f0427f1913" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -444,10 +448,11 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.103.0" +version = "1.107.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +checksum = "3d0d328ba962af23ecfa3c9f23b98d3d35e325fa218d7f13d17a6bf522f8a560" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -469,9 +474,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.3" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" +checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -553,10 +558,12 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.5" +version = "0.62.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] @@ -581,15 +588,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", @@ -606,9 +614,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.0" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -633,11 +641,22 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + [[package]] name = "aws-smithy-types" -version = "1.4.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" dependencies = [ "base64-simd", "bytes", @@ -670,13 +689,14 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.15" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -1082,7 +1102,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 1.3.0", ] [[package]] @@ -1140,9 +1160,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1174,7 +1194,7 @@ dependencies = [ "mogh_error", "mogh_pki", "mogh_resolver", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -1311,10 +1331,10 @@ dependencies = [ [[package]] name = "command" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "komodo_client", - "shlex", + "shlex 2.0.1", "svi", "tokio", ] @@ -1684,7 +1704,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "async-compression", @@ -1855,6 +1875,16 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1976,7 +2006,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "bytes", @@ -2004,21 +2034,9 @@ dependencies = [ "regex", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "environment" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "formatting", @@ -2170,7 +2188,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "mogh_error", ] @@ -2420,7 +2438,7 @@ dependencies = [ [[package]] name = "git" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "command", @@ -2643,7 +2661,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "hickory-proto 0.26.1", + "hickory-proto", "idna", "ipnet", "jni", @@ -2655,31 +2673,6 @@ dependencies = [ "url", ] -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.4", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - [[package]] name = "hickory-proto" version = "0.26.1" @@ -2700,27 +2693,6 @@ dependencies = [ "url", ] -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto 0.25.2", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.4", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "hickory-resolver" version = "0.26.1" @@ -2730,7 +2702,7 @@ dependencies = [ "cfg-if", "futures-util", "hickory-net", - "hickory-proto 0.26.1", + "hickory-proto", "ipconfig", "ipnet", "jni", @@ -3240,7 +3212,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "komodo_client", @@ -3407,7 +3379,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "bcrypt", @@ -3437,7 +3409,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "async_timing_util", @@ -3459,7 +3431,7 @@ dependencies = [ "mongo_indexed", "partial_derive2", "rand 0.10.1", - "reqwest 0.13.3", + "reqwest 0.13.4", "schemars 1.2.1", "serde", "serde_json", @@ -3477,7 +3449,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "arc-swap", @@ -3523,7 +3495,7 @@ dependencies = [ "partial_derive2", "periphery_client", "regex", - "reqwest 0.13.3", + "reqwest 0.13.4", "rustls 0.23.40", "serde", "serde_json", @@ -3550,7 +3522,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "arc-swap", @@ -3569,7 +3541,7 @@ dependencies = [ "formatting", "futures-util", "git", - "hickory-resolver 0.26.1", + "hickory-resolver", "interpolate", "komodo_client", "mogh_cache", @@ -3587,7 +3559,7 @@ dependencies = [ "serde_json", "serde_yaml_ng", "shell-escape", - "shlex", + "shlex 2.0.1", "strum 0.28.0", "sysinfo", "tokio", @@ -3788,7 +3760,7 @@ dependencies = [ "anyhow", "mogh_error", "mogh_resolver", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -3818,7 +3790,7 @@ dependencies = [ "mogh_validations", "openidconnect", "rand 0.10.1", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -3875,15 +3847,16 @@ dependencies = [ [[package]] name = "mogh_logger" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0aac766e2f34066511e67bf330a88874a9ac9d58732c9c4eb0687546c7a206e" +checksum = "363551f4b80f4a3340d77d8a6a0b7d983d7cf64be58f85b2281266877c787c93" dependencies = [ "anyhow", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", + "serde", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -4013,9 +3986,9 @@ dependencies = [ [[package]] name = "mongo_indexed" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180b469d21b4cdb686c1e98050ec8e1168232f4b54022086668c93dc9839f9ab" +checksum = "d3c4b3c63eba1b673060ac84cd0557c9552cd9cf20a8cca1fff3b79ed84179d2" dependencies = [ "mongo_indexed_derive", "mongodb", @@ -4025,9 +3998,9 @@ dependencies = [ [[package]] name = "mongo_indexed_derive" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8405357bf431a2e73d83057f2960e9349ab946389dd78af655832921434949" +checksum = "5adbea6b2e44191849ebe42cbc6f185e45be7dd27a693978ed21676696167613" dependencies = [ "proc-macro2", "quote", @@ -4054,9 +4027,9 @@ checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" [[package]] name = "mongodb" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2c933617431ad0246fb5b43c425ebdae18c7f7259c87de0726d93b0e7e91b" +checksum = "276ba0cd571553d1f6936c6f180964776ece6ab7507dc8765f8a9c9c49d8cd00" dependencies = [ "base64 0.22.1", "bitflags 2.11.1", @@ -4067,8 +4040,9 @@ dependencies = [ "futures-io", "futures-util", "hex", - "hickory-proto 0.25.2", - "hickory-resolver 0.25.2", + "hickory-net", + "hickory-proto", + "hickory-resolver", "hmac 0.12.1", "macro_magic", "md-5", @@ -4079,7 +4053,6 @@ dependencies = [ "rand 0.9.4", "rustc_version_runtime", "rustls 0.23.40", - "rustversion", "serde", "serde_bytes", "serde_with", @@ -4100,9 +4073,9 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5758dc828eb2d02ec30563cba365609d56ddd833190b192beaee2b475a7bb3" +checksum = "99ceb1a9a1018e470077ec94cf3a8c2d0e6da542b2c05ea95a59a0a627147375" dependencies = [ "macro_magic", "proc-macro2", @@ -4122,9 +4095,9 @@ dependencies = [ [[package]] name = "mungos" -version = "3.2.2" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d8548efe9a10b49e1c92fa719d2e4e27cc6b40936f05fbb0f6a7ac31d3c0f" +checksum = "585b5da25f7186bfe37d661ce9c4c28b6f284a4ac911ec1ca092e365617d1441" dependencies = [ "anyhow", "envy", @@ -4263,6 +4236,15 @@ dependencies = [ "url", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4270,6 +4252,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2", ] [[package]] @@ -4282,6 +4282,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -4627,7 +4638,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "encoding", @@ -5127,9 +5138,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -5156,9 +5167,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -5221,9 +5232,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -5815,9 +5826,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap 2.14.0", "itoa", @@ -5849,9 +5860,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2316d01592c3382277c5062105510e35e0a6bfb2851e30028485f7af8cf1240" +checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332" dependencies = [ "itoa", "percent-encoding", @@ -6027,6 +6038,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "sif-itree" version = "0.4.1" @@ -6390,15 +6407,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.4" +version = "0.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", + "objc2-open-directory", "windows", ] @@ -6557,9 +6575,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -6641,6 +6659,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -6978,7 +6997,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "axum", @@ -7232,9 +7251,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7994,7 +8013,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.2.0" +version = "2.3.0-dev-0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5b0b7e3b43..c01c2c3994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.2.0" +version = "2.3.0-dev-0" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" @@ -40,25 +40,25 @@ mogh_secret_file = "1.0.1" mogh_validations = "1.0.1" mogh_rate_limit = "1.0.1" partial_derive2 = "0.5.0" -mongo_indexed = "2.0.2" +mongo_indexed = "2.1.0" mogh_resolver = "1.0.0" mogh_config = "1.1.0" -mogh_logger = "1.3.3" +mogh_logger = "1.3.4" mogh_server = "1.5.0" toml_pretty = "2.0.0" mogh_cache = "1.1.2" mogh_pki = "1.1.3" -mungos = "3.2.2" +mungos = "3.7.0" svi = "1.2.0" # ASYNC -reqwest = { version = "0.13.3", default-features = false, features = ["json", "stream", "form", "query", "rustls"] } -tokio = { version = "1.52.2", features = ["full"] } +reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "form", "query", "rustls"] } +tokio = { version = "1.52.3", features = ["full"] } tokio-util = { version = "0.7.18", features = ["io", "codec"] } tokio-stream = { version = "0.1.18", features = ["sync"] } pin-project-lite = "0.2.17" futures-util = "0.3.32" -arc-swap = "1.9.0" +arc-swap = "1.9.1" # SERVER tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } @@ -72,13 +72,13 @@ utoipa = "5.5.0" # SER/DE ipnetwork = { version = "0.21.1", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] } -serde = { version = "1.0.227", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] } bson = { version = "2.15.0" } # must keep in sync with mongodb version toml = "1.1.2" serde_yaml_ng = "0.10.0" -serde_json = "1.0.149" -serde_qs = "1.1.0" +serde_json = "1.0.150" +serde_qs = "1.1.2" url = "2.5.8" # ERROR @@ -94,11 +94,11 @@ dotenvy = "0.15.7" envy = "0.4.2" # CRYPTO / AUTH -uuid = { version = "1.23.1", features = ["v4", "fast-rng", "serde"] } +uuid = { version = "1.23.3", features = ["v4", "fast-rng", "serde"] } rustls = { version = "0.23.40", features = ["aws-lc-rs"] } -data-encoding = "2.10.0" +data-encoding = "2.11.0" urlencoding = "2.1.3" -bcrypt = "0.19.0" +bcrypt = "0.19.1" hmac = "0.13.0" sha1 = "0.11.0" sha2 = "0.11.0" @@ -111,18 +111,18 @@ portable-pty = "0.9.0" shell-escape = "0.1.5" crossterm = "0.29.0" bollard = "0.21.0" -sysinfo = "0.38.4" -shlex = "1.3.0" +sysinfo = "0.39.3" +shlex = "2.0.1" # CLOUD -aws-config = "1.8.16" -aws-sdk-ec2 = "1.224.0" +aws-config = "1.8.18" +aws-sdk-ec2 = "1.232.0" aws-credential-types = "1.2.14" ## CRON english-to-cron = "0.1.7" chrono-tz = "0.10.4" -chrono = "0.4.44" +chrono = "0.4.45" croner = "3.0.1" # MISC @@ -134,7 +134,7 @@ typeshare = "1.0.5" wildcard = "0.3.0" colored = "3.1.1" bytes = "1.11.1" -regex = "1.12.3" +regex = "1.12.4" [profile.release] strip = "debuginfo" \ No newline at end of file diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 96e668a679..7632d7cc6f 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -1,6 +1,6 @@ { "name": "komodo_client", - "version": "2.2.0", + "version": "2.3.0", "description": "Komodo client package", "homepage": "https://komo.do", "main": "dist/lib.js", diff --git a/ui/package.json b/ui/package.json index e898bf1bbc..03c1bf1605 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,48 +10,48 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@mantine/core": "^9.1.1", - "@mantine/form": "^9.1.1", - "@mantine/hooks": "^9.1.1", - "@mantine/notifications": "^9.1.1", - "@mantine/spotlight": "^9.1.1", + "@mantine/core": "^9.3.1", + "@mantine/form": "^9.3.1", + "@mantine/hooks": "^9.3.1", + "@mantine/notifications": "^9.3.1", + "@mantine/spotlight": "^9.3.1", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.100.9", + "@tanstack/react-query": "^5.101.0", "@tanstack/react-table": "^8.21.3", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ansi-to-html": "^0.7.2", - "jotai": "^2.20.0", - "jotai-family": "^1.0.1", + "jotai": "^2.20.1", + "jotai-family": "^1.0.2", "jotai-location": "^0.6.2", - "lucide-react": "^1.14.0", + "lucide-react": "^1.18.0", "mogh_auth_client": "^1.5.0", "mogh_ui": "^0.6.1", "monaco-editor": "^0.55.1", - "monaco-yaml": "^5.4.1", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "monaco-yaml": "^5.5.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-minimal-pie-chart": "^9.1.2", - "react-router-dom": "^7.15.0", - "react-xtermjs": "^1.0.11", + "react-router-dom": "^7.17.0", + "react-xtermjs": "^1.0.12", "recharts": "^3.8.1", - "sanitize-html": "^2.17.3", - "shell-quote": "^1.8.3" + "sanitize-html": "^2.17.5", + "shell-quote": "^1.8.4" }, "devDependencies": { - "@types/node": "^25.6.2", - "@types/react": "^19.2.14", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/sanitize-html": "^2.16.1", "@types/shell-quote": "^1.7.5", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^6.0.2", "dotenv": "^17.4.2", - "postcss": "^8.5.14", + "postcss": "^8.5.15", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "sass-embedded": "^1.99.0", + "sass-embedded": "^1.100.0", "typescript": "^6.0.3", - "vite": "^8.0.11" + "vite": "^8.0.16" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/ui/yarn.lock b/ui/yarn.lock index 5eb3104f42..3ab41bf7c3 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -70,10 +70,10 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== -"@mantine/core@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.1.1.tgz#7d2125a334c3828950707822dd02d39c72336e98" - integrity sha512-vClOZdCeZ4oLYuA/3jAOgKGQ6dXbF6ZkzpYz09Gied9nZpB7HcQeb3dcMh8UPBE4f+EM7KlYWk6dch7GoASeaA== +"@mantine/core@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.3.1.tgz#6b56312e9660fe3594b842d6e6b782d83a77b5e2" + integrity sha512-4rBoHpggSohayE+Os7lKdbsHTw7m/uZRKYjk9DN6cKBhQIjdiULdcG+b4b5CMVZSqZDHPgT70uWGI7yqkn8Ufw== dependencies: "@floating-ui/react" "^0.27.19" clsx "^2.1.1" @@ -81,39 +81,39 @@ react-remove-scroll "^2.7.2" type-fest "^5.6.0" -"@mantine/form@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.1.1.tgz#50f8b2e54297e6db7779d199c7cad90001fead3e" - integrity sha512-xmebZ3s8GGMrCOPOaOwA+gQkdgNVfT2F9kBtkjAbRoZrMoY+vYFbiPWbIvWFl8pU1jBslYZrj+M0PIawJmFOdQ== +"@mantine/form@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.3.1.tgz#45576b331c512ac85fb56d7f28000dce181e943d" + integrity sha512-dGJPcYGkfDnAPKHcnzgf2mSlnjwymaOyROiTEJNbbjE3OVoWbPaw8HwEKav1wATdxNYR9Bw1GHtbfbGTsm1Jww== dependencies: "@standard-schema/spec" "^1.1.0" fast-deep-equal "^3.1.3" klona "^2.0.6" -"@mantine/hooks@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.1.1.tgz#2349a910014353210f0ffc0e26621f05aa297869" - integrity sha512-tTJK73nGFyy1v214TLdvBq0be7QCoc6osfbXVuJgOH3YG85lWk9Mvvor6k+w6hC6HXSqKMqLKePyiGm83xGcMg== +"@mantine/hooks@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.3.1.tgz#b94c663b2ab2ba02a327cc06ebb0554b97c15b83" + integrity sha512-zAOlxV59j5CDgAnExN+ypaR6dVW1vwMKDvKUxIlUVd/e52qxYnPyYRD+shJmyOLaZRuQmVF0R/7mJAjt2jw9cA== -"@mantine/notifications@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.1.1.tgz#63cdf4f5afcdd8991dd7ab82d99c8a02e2d3435a" - integrity sha512-ZfcEMMDp0BQ+yKmVp8ifPXLKej8pv9TcaRnmy2CZ07USD61E9LH5ClRAP/hxQuCyf/qLb5BPHsI7+f3K8uhj4Q== +"@mantine/notifications@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.3.1.tgz#f9675dbc763fa42ad23602a99f59d965c85a1b2d" + integrity sha512-irkD9uszgVftthbCLd+4lS7M1jkhL4sVrZASv8+43aIkncW+oQkCFpBuaOAf6uRcMjmQ7XnKs2O8bXYzl/DqgQ== dependencies: - "@mantine/store" "9.1.1" + "@mantine/store" "9.3.1" react-transition-group "4.4.5" -"@mantine/spotlight@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.1.1.tgz#355890f6bfe8be68614626cbbd71b6872a8b4090" - integrity sha512-UWeL3ADxfMKltxSygdzoUlVH5PO9CbbEu41kXs5sBItU87SQxppyG35wCsYfZ0plIJ7/LBAjnl5lQcKWl8AOxA== +"@mantine/spotlight@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.3.1.tgz#e0f64c8f360a31a6ada92b8c91941c87cbae67c8" + integrity sha512-NsNXywYhKuHnESoMCX1+e2za0GQJYQ43zMUIkr8XwHiyIzdU/tmFJh+k+jfUeFKPfnS5uRy+vJvC6z320aNbTw== dependencies: - "@mantine/store" "9.1.1" + "@mantine/store" "9.3.1" -"@mantine/store@9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.1.1.tgz#09e63b31db5b3c0202b29ba052a751d4ad433396" - integrity sha512-kbxEU8wVGbobHlmQmk0lu9M+xCILKjuAPcMAshgzPznGLfXeE9zrB0gNT2cbk11Ik8dlV9J6Vsn9cuACyOSpfQ== +"@mantine/store@9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.3.1.tgz#e6e657167feabeb7a51ac5cc3de294d3757c74b5" + integrity sha512-UZ8wGKfF/NFMHCDl1Rd0M8XtHdt4W1w3V4ohNpFSaP3nSjJ+Cu1FT+iIWtXwC0Ogha8XnuVQ7vm/dw2BUfpw+g== "@monaco-editor/loader@^1.5.0": version "1.7.0" @@ -136,10 +136,10 @@ dependencies: "@tybys/wasm-util" "^0.10.1" -"@oxc-project/types@=0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149" - integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ== +"@oxc-project/types@=0.133.0": + version "0.133.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8" + integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA== "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" @@ -242,94 +242,89 @@ redux-thunk "^3.1.0" reselect "^5.1.0" -"@rolldown/binding-android-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz#3af8b2242086125934a85c1915b76e0a6a2054c1" - integrity sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ== - -"@rolldown/binding-darwin-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz#ae0b4467d24ecd6c6589f03d4d4699616ee9649c" - integrity sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ== - -"@rolldown/binding-darwin-x64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz#23cf24b0a7b96c8990bbdd8a91e7fd3ba82b00e7" - integrity sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g== - -"@rolldown/binding-freebsd-x64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz#a047a770f94dc451c062b729e5d1cf82e5c6f9c4" - integrity sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw== - -"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz#c0b7f346cbf50301cea669a4632bc63aabe6a72c" - integrity sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg== - -"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz#af56373c7996ebe6379207cd699c9f7f705e235d" - integrity sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ== - -"@rolldown/binding-linux-arm64-musl@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz#a8f5acd21fcffc8991aa84710e3ae603c4240ea4" - integrity sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug== - -"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz#1d4a89e040ff82141fc46e717cfab80b05f7c13f" - integrity sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg== - -"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz#97c21feeb2ed87d07820f0b2dcc5dd663e7a7f3b" - integrity sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA== - -"@rolldown/binding-linux-x64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz#06310d40fe139ccc3c433b361120d337c66ebec2" - integrity sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw== - -"@rolldown/binding-linux-x64-musl@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz#6a711258841f42609b238050cfcd5db13ac136d0" - integrity sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA== - -"@rolldown/binding-openharmony-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz#15cb644beeafdbec930d79ed45c2a7c2573eac70" - integrity sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A== - -"@rolldown/binding-wasm32-wasi@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz#ca3a56d11dfd533d743711141b3bb4c1ec10110e" - integrity sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg== +"@rolldown/binding-android-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592" + integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw== + +"@rolldown/binding-darwin-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc" + integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA== + +"@rolldown/binding-darwin-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad" + integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg== + +"@rolldown/binding-freebsd-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1" + integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g== + +"@rolldown/binding-linux-arm-gnueabihf@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f" + integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw== + +"@rolldown/binding-linux-arm64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1" + integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw== + +"@rolldown/binding-linux-arm64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069" + integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q== + +"@rolldown/binding-linux-ppc64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8" + integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg== + +"@rolldown/binding-linux-s390x-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763" + integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg== + +"@rolldown/binding-linux-x64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7" + integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg== + +"@rolldown/binding-linux-x64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3" + integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow== + +"@rolldown/binding-openharmony-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309" + integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg== + +"@rolldown/binding-wasm32-wasi@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b" + integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg== dependencies: "@emnapi/core" "1.10.0" "@emnapi/runtime" "1.10.0" "@napi-rs/wasm-runtime" "^1.1.4" -"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz#8c2117d68331d7de59d24631146d538fc203d27c" - integrity sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ== +"@rolldown/binding-win32-arm64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad" + integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g== -"@rolldown/binding-win32-x64-msvc@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz#bb5c28df3095046778cc1b020ef52fc5ee7b7e70" - integrity sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg== +"@rolldown/binding-win32-x64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb" + integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA== -"@rolldown/pluginutils@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz#51cf2589596a179ebe8cbf313f1358c7b51a2fdc" - integrity sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw== - -"@rolldown/pluginutils@1.0.0-rc.7": - version "1.0.0-rc.7" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022" - integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA== +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== "@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0": version "1.1.0" @@ -341,17 +336,17 @@ resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== -"@tanstack/query-core@5.100.9": - version "5.100.9" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.9.tgz#dcf44ef25cf42a4da229bcab1d8d33e80a740a99" - integrity sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ== +"@tanstack/query-core@5.101.0": + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.101.0.tgz#45391ce143c270b7e7a55bd1a6696918f20782d1" + integrity sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow== -"@tanstack/react-query@^5.100.9": - version "5.100.9" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.9.tgz#0c701bf56f38b484602255a92d4c9e452a04807d" - integrity sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A== +"@tanstack/react-query@^5.101.0": + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.101.0.tgz#2dd56c5b96ec816d6b6ec9cee5c80ed298974733" + integrity sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg== dependencies: - "@tanstack/query-core" "5.100.9" + "@tanstack/query-core" "5.101.0" "@tanstack/react-table@^8.21.3": version "8.21.3" @@ -423,22 +418,22 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== -"@types/node@^25.6.2": - version "25.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.2.tgz#8c491201373690e4ef2a2ffed0dfb510a5830b92" - integrity sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw== +"@types/node@^25.9.3": + version "25.9.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.3.tgz#11dfe7a33e68fa5c560f0aa76cc5595621ef26b9" + integrity sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg== dependencies: - undici-types "~7.19.0" + undici-types ">=7.24.0 <7.24.7" "@types/react-dom@^19.2.3": version "19.2.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== -"@types/react@^19.2.14": - version "19.2.14" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" - integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== +"@types/react@^19.2.17": + version "19.2.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.17.tgz#dccac365baa0f1734ec270ff4b51c89465e8dc7f" + integrity sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw== dependencies: csstype "^3.2.2" @@ -464,12 +459,17 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== -"@vitejs/plugin-react@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz#d9113b71a0a592714913eafd9e5e63bcafd0ff15" - integrity sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ== +"@vitejs/plugin-react@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz#f70cb8ed0ce225dbc3055d78070f820d8aa35eda" + integrity sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg== dependencies: - "@rolldown/pluginutils" "1.0.0-rc.7" + "@rolldown/pluginutils" "^1.0.0" + +"@vscode/l10n@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" + integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== "@xterm/addon-fit@^0.11.0": version "0.11.0" @@ -493,12 +493,12 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== dependencies: - readdirp "^4.0.1" + readdirp "^5.0.0" clsx@^2.1.1: version "2.1.1" @@ -596,6 +596,11 @@ d3-timer@^3.0.1: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== +dayjs@^1.11.7: + version "1.11.21" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + decimal.js-light@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -768,20 +773,20 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -jotai-family@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jotai-family/-/jotai-family-1.0.1.tgz#0782aaaf22042b63111df4c1f9acf7b2ceff03cb" - integrity sha512-Zb/79GNDhC/z82R+6qTTpeKW4l4H6ZCApfF5W8G4SH37E4mhbysU7r8DkP0KX94hWvjB/6lt/97nSr3wB+64Zg== +jotai-family@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jotai-family/-/jotai-family-1.0.2.tgz#11d9f88a39579aaf0d80dcd8826b8c32cf6b5269" + integrity sha512-U1aTMGxmsmz2Z8gaJD1/ljmMnsmG4/dqrcsfwGbjWV7p6boB9Vy3+75YwUpwPj7GbLNJk9O8rl1VNi8d7+6Rxw== jotai-location@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/jotai-location/-/jotai-location-0.6.2.tgz#7f90ec768d9aca62ce7d509baa771848225edb39" integrity sha512-D+fqGNNgPHpehOsGBsYsS7F3kNgml099MJrGYt8YlLumcnoTVrBdcO+Y11KyMlyzwPihEN0HPUgke9Iihz9OXw== -jotai@^2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.20.0.tgz#0e3422f6f7141758e8212d72338f9cf3bef5fd45" - integrity sha512-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg== +jotai@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.20.1.tgz#473274f1b78c60acce1b868be5655b3c706dec8e" + integrity sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg== "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" @@ -803,6 +808,13 @@ klona@^2.0.6: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + lightningcss-android-arm64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" @@ -884,10 +896,10 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lucide-react@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.14.0.tgz#3c3867749f5ff4eeb5a6f423557ec6a50521366f" - integrity sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA== +lucide-react@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.18.0.tgz#d1b8754d9c427061261c6a6a0b7fa6ecda36c84f" + integrity sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA== marked@14.0.0: version "14.0.0" @@ -940,11 +952,12 @@ monaco-worker-manager@^2.0.0: resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a" integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg== -monaco-yaml@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.4.1.tgz#c26106c6e497d514aa6254c78ab184610a9923b4" - integrity sha512-YQ6d/Ei98Uk073SJLFbwuSi95qhnl8F8NNmIUqN2XhDt9psZN2LqQ1T7pPQ866NJb2wFj44IrjnANgpa2jTfag== +monaco-yaml@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.5.1.tgz#9771c91d6b54456a1a398b028dd27435f473b58e" + integrity sha512-vLR1EoV7Au0tkuA9T6ZJr2q7CooRP69AvlqzWh9Gui6amuijdTsfCAQHtCFobRMwkhbuly6J1Q16NziTTiWtWQ== dependencies: + "@vscode/l10n" "^0.0.18" jsonc-parser "^3.0.0" monaco-languageserver-types "^0.4.0" monaco-marker-data-provider "^1.0.0" @@ -952,6 +965,7 @@ monaco-yaml@^5.4.1: monaco-worker-manager "^2.0.0" path-browserify "^1.0.0" prettier "^3.0.0" + proxy-disposable "^1.0.0" vscode-languageserver-textdocument "^1.0.0" vscode-languageserver-types "^3.0.0" vscode-uri "^3.0.0" @@ -962,6 +976,11 @@ nanoid@^3.3.11: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" + integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== + node-addon-api@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" @@ -1051,12 +1070,12 @@ postcss@^8.3.11: picocolors "^1.1.1" source-map-js "^1.2.1" -postcss@^8.5.14: - version "8.5.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c" - integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg== +postcss@^8.5.15: + version "8.5.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" + integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== dependencies: - nanoid "^3.3.11" + nanoid "^3.3.12" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -1074,10 +1093,15 @@ prop-types@^15.6.2: object-assign "^4.1.1" react-is "^16.13.1" -react-dom@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" - integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== +proxy-disposable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-disposable/-/proxy-disposable-1.0.0.tgz#c61f2ebcf1215600088de75badf5882873c64382" + integrity sha512-Y673BKpFqk8XkYuoK9DGZ1RIzldz5SkzxTKc/+DkpaMdrDEbEalWlO/uMRMdJsXvdbTybJI26XDwN0KJpZDN4w== + +react-dom@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" + integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== dependencies: scheduler "^0.27.0" @@ -1125,17 +1149,17 @@ react-remove-scroll@^2.7.2: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router-dom@^7.15.0: - version "7.15.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.15.0.tgz#a4b95c4402d896c2ad437014aff9076b94673063" - integrity sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ== +react-router-dom@^7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.17.0.tgz#e77527b4b7862f7b47ff26dd5b9315fb897b82a7" + integrity sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw== dependencies: - react-router "7.15.0" + react-router "7.17.0" -react-router@7.15.0: - version "7.15.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.15.0.tgz#cb438ff254ab5a1e356ef5a23d7821d8f6fbe652" - integrity sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ== +react-router@7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.17.0.tgz#88bbe817c6e37ab36faf140623b5d4678bf81e41" + integrity sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -1158,20 +1182,20 @@ react-transition-group@4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react-xtermjs@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.11.tgz#8ea013735a698a3c8173fc2d3d745f395ca72693" - integrity sha512-GyC4krigc+LREQKM25aFRCZf5kmUmx+xgEB2X0XEwZXYh/2QHeegHfdBP+oFztPgKsSROa1OIW4wqQzSCKSP+Q== +react-xtermjs@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.12.tgz#d678fee0f04315d87613ecbb10b0820c54c4ee38" + integrity sha512-8uXBzVF4vP6r2NABxt/9heH1Y5Vx2v2hFrAk1kre+HyY3CjM6Prbc8ViqBRia6EBzI57IFBWOicYV1/k05JBtw== -react@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.6.tgz#3dadb8e12b2a7934c1d5317973e5dce1301f9a4d" - integrity sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q== +react@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== recharts@^3.8.1: version "3.8.1" @@ -1205,29 +1229,29 @@ reselect@5.1.1, reselect@^5.1.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== -rolldown@1.0.0-rc.18: - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.18.tgz#c597f89a4ce12e6fc918fa91e4f892b340aa92f0" - integrity sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg== +rolldown@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" + integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g== dependencies: - "@oxc-project/types" "=0.128.0" - "@rolldown/pluginutils" "1.0.0-rc.18" + "@oxc-project/types" "=0.133.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.0-rc.18" - "@rolldown/binding-darwin-arm64" "1.0.0-rc.18" - "@rolldown/binding-darwin-x64" "1.0.0-rc.18" - "@rolldown/binding-freebsd-x64" "1.0.0-rc.18" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.18" - "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.18" - "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-x64-musl" "1.0.0-rc.18" - "@rolldown/binding-openharmony-arm64" "1.0.0-rc.18" - "@rolldown/binding-wasm32-wasi" "1.0.0-rc.18" - "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.18" - "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.18" + "@rolldown/binding-android-arm64" "1.0.3" + "@rolldown/binding-darwin-arm64" "1.0.3" + "@rolldown/binding-darwin-x64" "1.0.3" + "@rolldown/binding-freebsd-x64" "1.0.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.0.3" + "@rolldown/binding-linux-arm64-gnu" "1.0.3" + "@rolldown/binding-linux-arm64-musl" "1.0.3" + "@rolldown/binding-linux-ppc64-gnu" "1.0.3" + "@rolldown/binding-linux-s390x-gnu" "1.0.3" + "@rolldown/binding-linux-x64-gnu" "1.0.3" + "@rolldown/binding-linux-x64-musl" "1.0.3" + "@rolldown/binding-openharmony-arm64" "1.0.3" + "@rolldown/binding-wasm32-wasi" "1.0.3" + "@rolldown/binding-win32-arm64-msvc" "1.0.3" + "@rolldown/binding-win32-x64-msvc" "1.0.3" rxjs@^7.4.0: version "7.8.2" @@ -1236,116 +1260,117 @@ rxjs@^7.4.0: dependencies: tslib "^2.1.0" -sanitize-html@^2.17.3: - version "2.17.3" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.3.tgz#b2f79f75afeda3a90e87d480052dfc1601b7aeee" - integrity sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg== +sanitize-html@^2.17.5: + version "2.17.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.5.tgz#bef1d103773760e1c52a01b1f58178bee9c10556" + integrity sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" htmlparser2 "^10.1.0" is-plain-object "^5.0.0" + launder "^1.7.1" parse-srcset "^1.0.2" postcss "^8.3.11" -sass-embedded-all-unknown@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.99.0.tgz#a17211a0e6862bc36eea063fbd0c55dc2a41c3a1" - integrity sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw== - dependencies: - sass "1.99.0" - -sass-embedded-android-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.99.0.tgz#670907b907eef644d45c609b06d57c3659cf77a7" - integrity sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg== - -sass-embedded-android-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.99.0.tgz#2c0d5719d2022578e97d0296bfc7771c4588a2e9" - integrity sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ== - -sass-embedded-android-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.99.0.tgz#7b9006c5921205c252d9928dabc41d93713b0ae6" - integrity sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw== - -sass-embedded-android-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.99.0.tgz#cb0c4e0ec1991faab41a6d60c894ce12fcaaf339" - integrity sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ== - -sass-embedded-darwin-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.99.0.tgz#aa836ba0b2342f8d0fa03394781a9385fb8f222d" - integrity sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg== - -sass-embedded-darwin-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.99.0.tgz#47a8a95579f6b8b66f07d233d59d43a99f6000ac" - integrity sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A== - -sass-embedded-linux-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.99.0.tgz#201232294df6d119e89f12c1e35e0e9f316263bd" - integrity sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew== - -sass-embedded-linux-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.99.0.tgz#013ac6f04dedbcbdd3f2d460d078809c5244d1eb" - integrity sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw== - -sass-embedded-linux-musl-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.99.0.tgz#cc3cc0f4dcaf5072bc777719419a8fdf997910df" - integrity sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw== - -sass-embedded-linux-musl-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.99.0.tgz#4af04ac13b242253a276ccecc0905a4119501d16" - integrity sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ== - -sass-embedded-linux-musl-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.99.0.tgz#5e6fb8d41a6280d9dfda2b636a3b1c3a7c3c2e53" - integrity sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg== - -sass-embedded-linux-musl-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.99.0.tgz#e26a0d65cf5a488f24951ddce1842d6482c78a7d" - integrity sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew== - -sass-embedded-linux-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.99.0.tgz#3a114700b58e4d0d8fb2191f29f873068bb42afc" - integrity sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA== - -sass-embedded-linux-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.99.0.tgz#b23e34de696eeef621ada3671014208ae56b747b" - integrity sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA== - -sass-embedded-unknown-all@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.99.0.tgz#202d955a376c413706224dd6f83ce8df2a5f71fe" - integrity sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg== - dependencies: - sass "1.99.0" - -sass-embedded-win32-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.99.0.tgz#1461a6a14cd3596ee122fc1b7ead25b039a066fc" - integrity sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw== - -sass-embedded-win32-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.99.0.tgz#3d08d273b268918647093fd7d0bf0fb84ac2c321" - integrity sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg== - -sass-embedded@^1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.99.0.tgz#348fdb98b02694d8b193e7a0e1bfc47408f89d4f" - integrity sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg== +sass-embedded-all-unknown@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.100.0.tgz#efee7111ec5e2d6c34a2f57d090e6416c01890ff" + integrity sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q== + dependencies: + sass "1.100.0" + +sass-embedded-android-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.100.0.tgz#0a4456a751f76a378ac9f3516a034f56eca9b171" + integrity sha512-W+Ru9JwTnfU0UX3jSZcbqFdtKFMcYdfFwytc57h2DgnqCOIiAqI2E06mABZBZC+r3LwXCBuS5GbXAGeVgvVDkA== + +sass-embedded-android-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.100.0.tgz#7de61d65cfa997c58df426a8b20a096a60497313" + integrity sha512-70f3HgX2pFNmzpGQ86n5e6QfWn2fP4QUQGfFQK0P1XH73ZLIzLo2YqygrGKGKeeqtc5eU2Wl1/xQzhzuKnO4kw== + +sass-embedded-android-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.100.0.tgz#cc6721c1526ef5b10d9da02f1e44e7dbaffc11e0" + integrity sha512-icU3o0V/uCSytSpf+tX5Lf51BvyQEbLzDUJfUi9etSauYBGHpPKkdtdZH0si4v98phq11Kl8rSV1SggksxF1Hg== + +sass-embedded-android-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.100.0.tgz#dc38f6c633f8bf61b470ac00452ebb50e36f421f" + integrity sha512-mevF9VQk6gEYByy8+jusaHGmd7Usb2ytX/DsEOd0JtOGCtcf1kh575xJ6OUBDIcJ15uLnbau/0iy1eP6WVBvWA== + +sass-embedded-darwin-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.100.0.tgz#7773c0586e78caab599d53370626c28ca08110eb" + integrity sha512-1PVlYi61POo93IT/FfrG1mc1tAHxeSTyUALF2aOFmXGWjVXr3bQzEQiBGCOvQbj/ix+5hNyXFXcEMEyKvtUJJA== + +sass-embedded-darwin-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.100.0.tgz#91066f64875b87eba0cb699ba8254b4863d33901" + integrity sha512-x97o3JnGyImZNCIVs9wQHJUE5QCvmVIKaH1cwrz/5dK7OT1FpeNiW+u9TUomP9hG6Ekjd8EL8NBHpxTfIhdjmg== + +sass-embedded-linux-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.100.0.tgz#88d2f51a0882f399c327a1d6cd06757d13de4f82" + integrity sha512-Dwjmj8Z6VRy7rAi53JAdEwIyUjpfl7PhpSc2/LpQPQx+aO5Dp7Spaipkax0ufJl1SoDUdchCsM4y/88YaluorQ== + +sass-embedded-linux-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.100.0.tgz#9721dca177348c1448847ceee1d031a692df0771" + integrity sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw== + +sass-embedded-linux-musl-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.100.0.tgz#63a1e0618a2a3acf10bab915585c4dc495327581" + integrity sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg== + +sass-embedded-linux-musl-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.100.0.tgz#44f9472ae663411de82b7792a918e162c7bcbc02" + integrity sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw== + +sass-embedded-linux-musl-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.100.0.tgz#21954e480cfb55d4ba8167626cde1cc8560d7eee" + integrity sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ== + +sass-embedded-linux-musl-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.100.0.tgz#a6c93d51999a8b22795d383b67e61962d32c96cd" + integrity sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA== + +sass-embedded-linux-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.100.0.tgz#01f20471a1be16d18e150993e5ac771d618abd34" + integrity sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ== + +sass-embedded-linux-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.100.0.tgz#809649a0df17f7afb1dfd02e76dbf0676dd8471e" + integrity sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA== + +sass-embedded-unknown-all@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz#fdc210a25a06e9d79655a02c1c77d656fa6f2ca0" + integrity sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A== + dependencies: + sass "1.100.0" + +sass-embedded-win32-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.100.0.tgz#fd8cec45b41390c3db40d13c4d291be7f35b6b0b" + integrity sha512-iE+yxj+hUXwwbqpHkXxgAWTzeRfcWxJ7SSTQEPMk48lwq3oCrWLlz5sQuWHbuTK/i0GKQfROdP+hOmPi89yjUg== + +sass-embedded-win32-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.100.0.tgz#a758862b1455d6616be301b363e7e1c93e6361b7" + integrity sha512-qI4F8MI7/KYoy9NdjJfhSspG42WPkADSNDvwEV7qWvCSFC83koJssRsKO2/PfY+niZz6BG65Ic/D+A11h959hw== + +sass-embedded@^1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.100.0.tgz#fe4742f2f80c21b287e3b90399b9a3f7ca1dcf6f" + integrity sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw== dependencies: "@bufbuild/protobuf" "^2.5.0" colorjs.io "^0.5.0" @@ -1355,31 +1380,31 @@ sass-embedded@^1.99.0: sync-child-process "^1.0.2" varint "^6.0.0" optionalDependencies: - sass-embedded-all-unknown "1.99.0" - sass-embedded-android-arm "1.99.0" - sass-embedded-android-arm64 "1.99.0" - sass-embedded-android-riscv64 "1.99.0" - sass-embedded-android-x64 "1.99.0" - sass-embedded-darwin-arm64 "1.99.0" - sass-embedded-darwin-x64 "1.99.0" - sass-embedded-linux-arm "1.99.0" - sass-embedded-linux-arm64 "1.99.0" - sass-embedded-linux-musl-arm "1.99.0" - sass-embedded-linux-musl-arm64 "1.99.0" - sass-embedded-linux-musl-riscv64 "1.99.0" - sass-embedded-linux-musl-x64 "1.99.0" - sass-embedded-linux-riscv64 "1.99.0" - sass-embedded-linux-x64 "1.99.0" - sass-embedded-unknown-all "1.99.0" - sass-embedded-win32-arm64 "1.99.0" - sass-embedded-win32-x64 "1.99.0" - -sass@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" - integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== - dependencies: - chokidar "^4.0.0" + sass-embedded-all-unknown "1.100.0" + sass-embedded-android-arm "1.100.0" + sass-embedded-android-arm64 "1.100.0" + sass-embedded-android-riscv64 "1.100.0" + sass-embedded-android-x64 "1.100.0" + sass-embedded-darwin-arm64 "1.100.0" + sass-embedded-darwin-x64 "1.100.0" + sass-embedded-linux-arm "1.100.0" + sass-embedded-linux-arm64 "1.100.0" + sass-embedded-linux-musl-arm "1.100.0" + sass-embedded-linux-musl-arm64 "1.100.0" + sass-embedded-linux-musl-riscv64 "1.100.0" + sass-embedded-linux-musl-x64 "1.100.0" + sass-embedded-linux-riscv64 "1.100.0" + sass-embedded-linux-x64 "1.100.0" + sass-embedded-unknown-all "1.100.0" + sass-embedded-win32-arm64 "1.100.0" + sass-embedded-win32-x64 "1.100.0" + +sass@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.100.0.tgz#b4cab1bed286fe22ac6c879c514f71cd36aa06c8" + integrity sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ== + dependencies: + chokidar "^5.0.0" immutable "^5.1.5" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: @@ -1395,10 +1420,10 @@ set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== -shell-quote@^1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== +shell-quote@^1.8.4: + version "1.8.4" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190" + integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" @@ -1462,10 +1487,10 @@ tinyglobby@^0.2.14: fdir "^6.5.0" picomatch "^4.0.3" -tinyglobby@^0.2.16: - version "0.2.16" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" - integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" picomatch "^4.0.4" @@ -1487,10 +1512,10 @@ typescript@^6.0.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== -undici-types@~7.19.0: - version "7.19.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" - integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== +"undici-types@>=7.24.0 <7.24.7": + version "7.24.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" + integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== use-callback-ref@^1.3.3: version "1.3.3" @@ -1542,16 +1567,16 @@ victory-vendor@^37.0.2: d3-time "^3.0.0" d3-timer "^3.0.1" -vite@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6" - integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow== +vite@^8.0.16: + version "8.0.16" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6" + integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== dependencies: lightningcss "^1.32.0" picomatch "^4.0.4" - postcss "^8.5.14" - rolldown "1.0.0-rc.18" - tinyglobby "^0.2.16" + postcss "^8.5.15" + rolldown "1.0.3" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" From a2865cadc0005240b61b59bcce861bc7a87874a5 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 12:24:40 -0400 Subject: [PATCH 003/203] deploy 2.3.0-dev-1 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dee3e4724..b8faf0e5fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "komodo_client", "shlex 2.0.1", @@ -1704,7 +1704,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "async-compression", @@ -2006,7 +2006,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "bytes", @@ -2036,7 +2036,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "formatting", @@ -2188,7 +2188,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "mogh_error", ] @@ -2438,7 +2438,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "command", @@ -3212,7 +3212,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "komodo_client", @@ -3379,7 +3379,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "bcrypt", @@ -3409,7 +3409,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "async_timing_util", @@ -3449,7 +3449,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "arc-swap", @@ -3522,7 +3522,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "arc-swap", @@ -4638,7 +4638,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "encoding", @@ -6997,7 +6997,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "axum", @@ -8013,7 +8013,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index c01c2c3994..a6d29d91b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-0" +version = "2.3.0-dev-1" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From fbf33867dfefe3c32148605a031ae87dfa17e0aa Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 13:43:05 -0400 Subject: [PATCH 004/203] omnisearch clear on close option (default true), fix omnisearch border radius cutting off search input --- ui/src/app/topbar/omni-search/index.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index c5b8d9ee70..1fa594a774 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -1,13 +1,18 @@ -import { ActionIcon, Badge, Button, Group } from "@mantine/core"; +import { ActionIcon, Badge, Button, Group, Switch, Text } from "@mantine/core"; import { Spotlight, spotlight } from "@mantine/spotlight"; import { useOmniSearch } from "./hooks"; import { ICONS } from "@/lib/icons"; import { useShiftKeyListener } from "mogh_ui"; import classes from "./index.module.scss"; +import { useLocalStorage } from "@mantine/hooks"; export default function OmniSearch({}: {}) { const { search, setSearch, actions } = useOmniSearch(); useShiftKeyListener("S", () => spotlight.open()); + const [clearOnClose, setClearOnClose] = useLocalStorage({ + key: "omnisearch.clearOnClose", + defaultValue: true, + }); return ( <> } placeholder="search..." /> + + setClearOnClose((v) => !v)} + p="0.5rem 1rem" + style={{ cursor: "pointer" }} + > + Clear on close + setClearOnClose((v) => !v)} + /> + + {actions.map((group) => ( From cfd196277a3772c72483e87e8d2351584af97e41 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 13:53:07 -0400 Subject: [PATCH 005/203] fix resource description responsive overflow in some screen size --- ui/src/resources/description.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/resources/description.tsx b/ui/src/resources/description.tsx index 8d3f9aa953..25f58b97ac 100644 --- a/ui/src/resources/description.tsx +++ b/ui/src/resources/description.tsx @@ -59,9 +59,9 @@ export default function ResourceDescription({ > - {resource?.description || "Set Description"} + {resource?.description?.split("\n")[0] || "Set Description"} )} From a33938259bcc9c422fe15968e670f71d64c3be79 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 14:46:39 -0400 Subject: [PATCH 006/203] kill process group --- Cargo.lock | 1 + Cargo.toml | 3 + lib/command/Cargo.toml | 1 + lib/command/src/lib.rs | 124 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8faf0e5fb..bb37b199ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,6 +1334,7 @@ name = "command" version = "2.3.0-dev-1" dependencies = [ "komodo_client", + "libc", "shlex 2.0.1", "svi", "tokio", diff --git a/Cargo.toml b/Cargo.toml index a6d29d91b2..dfef89566e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,9 @@ url = "2.5.8" anyhow = "1.0.102" thiserror = "2.0.18" +# SYSTEM +libc = "0.2.186" + # LOGGING tracing = "0.1.44" diff --git a/lib/command/Cargo.toml b/lib/command/Cargo.toml index 00912dddcc..7bd70f2a2d 100644 --- a/lib/command/Cargo.toml +++ b/lib/command/Cargo.toml @@ -14,3 +14,4 @@ svi.workspace = true # tokio.workspace = true shlex.workspace = true +libc.workspace = true diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index 53c5baed02..6cfe508cfb 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -2,6 +2,7 @@ use std::{ path::{Path, PathBuf}, process::Stdio, sync::OnceLock, + time::Duration, }; use komodo_client::{ @@ -128,6 +129,18 @@ pub fn output_into_log( pub async fn run_standard_command( command: &str, path: impl Into>, +) -> CommandOutput { + run_standard_command_with_timeout(command, path, None).await +} + +/// Commands are run directly, and cannot include '&&'. +/// +/// If `timeout` is provided and elapses before the command finishes, +/// the child process is killed and a timeout error is returned. +pub async fn run_standard_command_with_timeout( + command: &str, + path: impl Into>, + timeout: impl Into>, ) -> CommandOutput { let lexed = if let Some(lexed) = shlex::split(command) && !lexed.is_empty() @@ -157,7 +170,7 @@ pub async fn run_standard_command( } } - CommandOutput::from(cmd.output().await) + output_with_timeout(cmd, timeout.into()).await } fn shell() -> &'static str { @@ -182,6 +195,18 @@ fn shell() -> &'static str { pub async fn run_shell_command( command: &str, path: impl Into>, +) -> CommandOutput { + run_shell_command_with_timeout(command, path, None).await +} + +/// Commands are wrapped in 'sh -c', and can include '&&'. +/// +/// If `timeout` is provided and elapses before the command finishes, +/// the child process is killed and a timeout error is returned. +pub async fn run_shell_command_with_timeout( + command: &str, + path: impl Into>, + timeout: impl Into>, ) -> CommandOutput { let mut cmd = Command::new(shell()); @@ -199,5 +224,100 @@ pub async fn run_shell_command( } } - CommandOutput::from(cmd.output().await) + output_with_timeout(cmd, timeout.into()).await +} + +/// Runs the command to completion, returning its output. +/// +/// With no `timeout`, this is just `cmd.output()`. +/// +/// With a `timeout`, the child is spawned in its own process group (via +/// `process_group(0)`, so the child's pid is also its process group id). +/// If the timeout elapses first, the entire process group is killed with +/// `SIGKILL` — not just the direct child — so any descendants the command +/// spawned (e.g. processes started by a `sh -c` wrapper) are torn down too. +/// `kill_on_drop(true)` remains set as a backstop to reap the direct child. +async fn output_with_timeout( + mut cmd: Command, + timeout: Option, +) -> CommandOutput { + let Some(timeout) = timeout else { + return CommandOutput::from(cmd.output().await); + }; + + // Place the child in a new process group so the whole group can be + // signalled together on timeout. `output()` configures stdout/stderr + // automatically, but since we spawn manually we set them here too. + cmd + .process_group(0) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let child = match cmd.spawn() { + Ok(child) => child, + Err(e) => return CommandOutput::from_err(e), + }; + + // Because of `process_group(0)`, the child's pid equals its pgid. + let pid = child.id(); + + match tokio::time::timeout(timeout, child.wait_with_output()).await { + Ok(output) => CommandOutput::from(output), + Err(_elapsed) => { + if let Some(pid) = pid { + // A negative pid targets the entire process group. The child is + // the group leader, so this kills it and all of its descendants. + // SAFETY: `kill` is a simple syscall with no memory safety + // concerns; we only signal our own child's process group. + unsafe { + libc::kill(-(pid as libc::pid_t), libc::SIGKILL); + } + } + CommandOutput::from_err(std::io::Error::other(format!( + "Command timed out after {:.1}s (process group killed)", + timeout.as_secs_f64() + ))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// On timeout, a backgrounded grandchild (here `sleep 31337`, started + /// with `&` so it is not the direct child of our spawned shell) must be + /// killed along with the rest of the process group. + #[tokio::test] + async fn timeout_kills_process_group() { + // Unique sleep duration so we can pgrep for exactly this process. + let marker = "sleep 31337"; + let out = run_shell_command_with_timeout( + &format!("{marker} & sleep 31336"), + None, + Duration::from_millis(300), + ) + .await; + + // The command should have reported a timeout failure. + assert!(!out.success(), "expected timeout failure, got: {out:?}"); + assert!( + out.stderr.contains("timed out"), + "expected timeout error, got: {out:?}" + ); + + // Give the kernel a moment to reap the killed group. + tokio::time::sleep(Duration::from_millis(200)).await; + + let still_running = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + let pids = String::from_utf8_lossy(&still_running.stdout); + let pids = pids.trim(); + assert!( + pids.is_empty(), + "backgrounded grandchild survived timeout: pids={pids:?}" + ); + } } From 88438c0352e313d282eaf622e69dc875c4481657 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 15:11:03 -0400 Subject: [PATCH 007/203] Start wiring through shell timeout / external cancel options --- Cargo.lock | 1 + bin/periphery/src/docker/mod.rs | 6 +- bin/periphery/src/docker/stats.rs | 5 +- bin/periphery/src/helpers.rs | 7 +- lib/command/Cargo.toml | 1 + lib/command/src/lib.rs | 206 ++++++++++++++++++++++-------- lib/git/src/commit.rs | 16 ++- lib/git/src/lib.rs | 27 ++-- 8 files changed, 194 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb37b199ec..057b0e5ffd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,7 @@ dependencies = [ "shlex 2.0.1", "svi", "tokio", + "tokio-util", ] [[package]] diff --git a/bin/periphery/src/docker/mod.rs b/bin/periphery/src/docker/mod.rs index 40599bc2e5..dea87e4e69 100644 --- a/bin/periphery/src/docker/mod.rs +++ b/bin/periphery/src/docker/mod.rs @@ -1,6 +1,8 @@ use anyhow::{Context, anyhow}; use bollard::Docker; -use command::{run_komodo_standard_command, run_shell_command}; +use command::{ + CommandConfig, run_komodo_standard_command, run_shell_command, +}; use komodo_client::entities::{ TerminationSignal, docker::{task::*, *}, @@ -53,7 +55,7 @@ pub async fn docker_login( let log = run_shell_command(&format!( "echo {registry_token} | docker login {domain} --username '{account}' --password-stdin", - ), None) + ), None, CommandConfig::default()) .await; if log.success() { diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index 1f9291bee7..2f7555c914 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::{Context, anyhow}; use async_timing_util::wait_until_timelength; use bollard::{models, query_parameters::StatsOptionsBuilder}; -use command::run_standard_command; +use command::{CommandConfig, run_standard_command}; use futures_util::StreamExt; use komodo_client::entities::docker::{ container::ContainerStats, @@ -65,7 +65,8 @@ pub async fn get_container_stats( }; let command = format!("docker stats{container_name} --no-stream {format}"); - let output = run_standard_command(&command, None).await; + let output = + run_standard_command(&command, None, CommandConfig::default()).await; if output.success() { output .stdout diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index a17c052334..d8d70fc454 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -5,8 +5,8 @@ use std::{ use anyhow::Context; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_standard_command, + CommandConfig, KomodoCommandMode, + run_komodo_command_with_sanitization, run_standard_command, }; use environment::write_env_file; use interpolate::Interpolator; @@ -367,7 +367,8 @@ async fn generate_self_signed_ssl_certs() { let command = format!( "openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\"" ); - let log = run_standard_command(&command, None).await; + let log = + run_standard_command(&command, None, CommandConfig::default()).await; if log.success() { info!("✅ SSL Certs generated"); diff --git a/lib/command/Cargo.toml b/lib/command/Cargo.toml index 7bd70f2a2d..981fbb4b00 100644 --- a/lib/command/Cargo.toml +++ b/lib/command/Cargo.toml @@ -13,5 +13,6 @@ komodo_client.workspace = true svi.workspace = true # tokio.workspace = true +tokio-util.workspace = true shlex.workspace = true libc.workspace = true diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index 6cfe508cfb..07e79908c7 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -14,6 +14,7 @@ mod output; pub use output::*; use tokio::process::Command; +use tokio_util::sync::CancellationToken; /// Commands are run directly, and cannot include '&&' pub async fn run_komodo_standard_command( @@ -23,7 +24,9 @@ pub async fn run_komodo_standard_command( ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = run_standard_command(&command, path).await; + let output = + run_standard_command(&command, path, CommandConfig::default()) + .await; output_into_log(stage, command, start_ts, output) } @@ -35,7 +38,8 @@ pub async fn run_komodo_shell_command( ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = run_shell_command(&command, path).await; + let output = + run_shell_command(&command, path, CommandConfig::default()).await; output_into_log(stage, command, start_ts, output) } @@ -125,22 +129,13 @@ pub fn output_into_log( } } -/// Commands are run directly, and cannot include '&&' -pub async fn run_standard_command( - command: &str, - path: impl Into>, -) -> CommandOutput { - run_standard_command_with_timeout(command, path, None).await -} - /// Commands are run directly, and cannot include '&&'. /// -/// If `timeout` is provided and elapses before the command finishes, -/// the child process is killed and a timeout error is returned. -pub async fn run_standard_command_with_timeout( +/// See [CommandConfig] for the available `timeout` / `cancel` controls. +pub async fn run_standard_command( command: &str, path: impl Into>, - timeout: impl Into>, + config: CommandConfig, ) -> CommandOutput { let lexed = if let Some(lexed) = shlex::split(command) && !lexed.is_empty() @@ -170,7 +165,7 @@ pub async fn run_standard_command_with_timeout( } } - output_with_timeout(cmd, timeout.into()).await + run_command(cmd, config).await } fn shell() -> &'static str { @@ -191,22 +186,13 @@ fn shell() -> &'static str { }) } -/// Commands are wrapped in 'sh -c', and can include '&&' -pub async fn run_shell_command( - command: &str, - path: impl Into>, -) -> CommandOutput { - run_shell_command_with_timeout(command, path, None).await -} - /// Commands are wrapped in 'sh -c', and can include '&&'. /// -/// If `timeout` is provided and elapses before the command finishes, -/// the child process is killed and a timeout error is returned. -pub async fn run_shell_command_with_timeout( +/// See [CommandConfig] for the available `timeout` / `cancel` controls. +pub async fn run_shell_command( command: &str, path: impl Into>, - timeout: impl Into>, + config: CommandConfig, ) -> CommandOutput { let mut cmd = Command::new(shell()); @@ -224,30 +210,68 @@ pub async fn run_shell_command_with_timeout( } } - output_with_timeout(cmd, timeout.into()).await + run_command(cmd, config).await +} + +/// Optional controls for how a command is executed. +/// +/// When neither field is set the command simply runs to completion. +/// When either is set, the child is spawned in its own process group so +/// that, on timeout or cancellation, the entire group (the command and any +/// descendants it spawned) is killed together — not just the direct child. +#[derive(Default, Clone)] +pub struct CommandConfig { + /// Kill the command (and its process group) if this duration elapses + /// before it finishes. + pub timeout: Option, + /// Kill the command (and its process group) when this token is + /// cancelled, allowing cancellation from elsewhere. + pub cancel: Option, +} + +impl CommandConfig { + pub fn timeout( + mut self, + timeout: impl Into>, + ) -> Self { + self.timeout = timeout.into(); + self + } + + pub fn cancel( + mut self, + cancel: impl Into>, + ) -> Self { + self.cancel = cancel.into(); + self + } } /// Runs the command to completion, returning its output. /// -/// With no `timeout`, this is just `cmd.output()`. +/// With an empty `config`, this is just `cmd.output()`. /// -/// With a `timeout`, the child is spawned in its own process group (via -/// `process_group(0)`, so the child's pid is also its process group id). -/// If the timeout elapses first, the entire process group is killed with +/// With a `timeout` and/or `cancel` token, the child is spawned in its own +/// process group (via `process_group(0)`, so the child's pid is also its +/// process group id). If the timeout elapses or the token is cancelled +/// before the command finishes, the entire process group is killed with /// `SIGKILL` — not just the direct child — so any descendants the command /// spawned (e.g. processes started by a `sh -c` wrapper) are torn down too. /// `kill_on_drop(true)` remains set as a backstop to reap the direct child. -async fn output_with_timeout( +async fn run_command( mut cmd: Command, - timeout: Option, + config: CommandConfig, ) -> CommandOutput { - let Some(timeout) = timeout else { + let CommandConfig { timeout, cancel } = config; + + // Fast path: nothing to wait on besides the command itself. + if timeout.is_none() && cancel.is_none() { return CommandOutput::from(cmd.output().await); - }; + } // Place the child in a new process group so the whole group can be - // signalled together on timeout. `output()` configures stdout/stderr - // automatically, but since we spawn manually we set them here too. + // signalled together. `output()` configures stdout/stderr automatically, + // but since we spawn manually we set them here too. cmd .process_group(0) .stdout(Stdio::piped()) @@ -261,23 +285,52 @@ async fn output_with_timeout( // Because of `process_group(0)`, the child's pid equals its pgid. let pid = child.id(); - match tokio::time::timeout(timeout, child.wait_with_output()).await { - Ok(output) => CommandOutput::from(output), - Err(_elapsed) => { - if let Some(pid) = pid { - // A negative pid targets the entire process group. The child is - // the group leader, so this kills it and all of its descendants. - // SAFETY: `kill` is a simple syscall with no memory safety - // concerns; we only signal our own child's process group. - unsafe { - libc::kill(-(pid as libc::pid_t), libc::SIGKILL); - } - } - CommandOutput::from_err(std::io::Error::other(format!( - "Command timed out after {:.1}s (process group killed)", - timeout.as_secs_f64() - ))) + // Resolve to `()` only when the relevant control fires; otherwise stay + // pending forever so it never wins the `select!`. + let on_timeout = async { + match timeout { + Some(timeout) => tokio::time::sleep(timeout).await, + None => std::future::pending().await, } + }; + let on_cancel = async { + match &cancel { + Some(cancel) => cancel.cancelled().await, + None => std::future::pending().await, + } + }; + + let killed_reason = tokio::select! { + output = child.wait_with_output() => { + return CommandOutput::from(output); + } + _ = on_timeout => format!( + "Command timed out after {:.1}s (process group killed)", + // `timeout` is `Some` here, since `on_timeout` only fires when set. + timeout.map(|t| t.as_secs_f64()).unwrap_or_default(), + ), + _ = on_cancel => { + "Command cancelled (process group killed)".to_string() + } + }; + + kill_process_group(pid); + CommandOutput::from_err(std::io::Error::other(killed_reason)) +} + +/// Sends `SIGKILL` to the entire process group led by `pid`. +/// +/// A negative pid targets the whole group, so a child spawned with +/// `process_group(0)` (group leader) is killed along with all of its +/// descendants. +fn kill_process_group(pid: Option) { + let Some(pid) = pid else { + return; + }; + // SAFETY: `kill` is a simple syscall with no memory safety concerns; + // we only signal our own child's process group. + unsafe { + libc::kill(-(pid as libc::pid_t), libc::SIGKILL); } } @@ -292,10 +345,10 @@ mod tests { async fn timeout_kills_process_group() { // Unique sleep duration so we can pgrep for exactly this process. let marker = "sleep 31337"; - let out = run_shell_command_with_timeout( + let out = run_shell_command( &format!("{marker} & sleep 31336"), None, - Duration::from_millis(300), + CommandConfig::default().timeout(Duration::from_millis(300)), ) .await; @@ -320,4 +373,45 @@ mod tests { "backgrounded grandchild survived timeout: pids={pids:?}" ); } + + /// Cancelling the token mid-run must kill the whole process group, just + /// like a timeout does, and return promptly with a cancellation error. + #[tokio::test] + async fn cancel_kills_process_group() { + let marker = "sleep 41337"; + let cancel = CancellationToken::new(); + + // Cancel shortly after the command starts. + let cancel_clone = cancel.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(300)).await; + cancel_clone.cancel(); + }); + + let out = run_shell_command( + &format!("{marker} & sleep 41336"), + None, + CommandConfig::default().cancel(cancel), + ) + .await; + + assert!(!out.success(), "expected cancel failure, got: {out:?}"); + assert!( + out.stderr.contains("cancelled"), + "expected cancellation error, got: {out:?}" + ); + + tokio::time::sleep(Duration::from_millis(200)).await; + + let still_running = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + let pids = String::from_utf8_lossy(&still_running.stdout); + let pids = pids.trim(); + assert!( + pids.is_empty(), + "backgrounded grandchild survived cancellation: pids={pids:?}" + ); + } } diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs index 42a6ca0743..7776de5830 100644 --- a/lib/git/src/commit.rs +++ b/lib/git/src/commit.rs @@ -1,7 +1,9 @@ use std::path::{Path, PathBuf}; use anyhow::Context; -use command::{run_komodo_standard_command, run_standard_command}; +use command::{ + CommandConfig, run_komodo_standard_command, run_standard_command, +}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionResponse, all_logs_success, update::Log, @@ -213,22 +215,28 @@ async fn ensure_global_git_config_set() { let res = run_standard_command( "git config --global --get user.email", None, + CommandConfig::default(), ) .await; if !res.success() { let _ = run_standard_command( "git config --global user.email komodo@komo.do", None, + CommandConfig::default(), ) .await; } - let res = - run_standard_command("git config --global --get user.name", None) - .await; + let res = run_standard_command( + "git config --global --get user.name", + None, + CommandConfig::default(), + ) + .await; if !res.success() { let _ = run_standard_command( "git config --global user.name komodo", None, + CommandConfig::default(), ) .await; } diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index 6a209ad0b6..ca75149684 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::anyhow; -use command::run_standard_command; +use command::{CommandConfig, run_standard_command}; use formatting::{bold, muted}; use komodo_client::entities::{ LatestCommit, komodo_timestamp, update::Log, @@ -24,9 +24,12 @@ pub use crate::{ pub async fn get_commit_hash_info( repo_dir: &Path, ) -> anyhow::Result { - let hash = - run_standard_command("git rev-parse --short HEAD", repo_dir) - .await; + let hash = run_standard_command( + "git rev-parse --short HEAD", + repo_dir, + CommandConfig::default(), + ) + .await; let hash = if hash.status.success() { hash.stdout.trim().to_string() } else { @@ -35,8 +38,12 @@ pub async fn get_commit_hash_info( hash.stderr )); }; - let message = - run_standard_command("git log -1 --pretty=%B", repo_dir).await; + let message = run_standard_command( + "git log -1 --pretty=%B", + repo_dir, + CommandConfig::default(), + ) + .await; let message = if message.status.success() { message.stdout.trim().to_string() } else { @@ -76,8 +83,12 @@ pub async fn get_commit_hash_log( /// Gets the remote url, with `.git` stripped from the end. pub async fn get_remote_url(path: &Path) -> anyhow::Result { - let output = - run_standard_command("git remote show origin", path).await; + let output = run_standard_command( + "git remote show origin", + path, + CommandConfig::default(), + ) + .await; if output.success() { Ok( output From 0581e7df5f336525fc80c5af61a518b3fee63578 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 15:11:27 -0400 Subject: [PATCH 008/203] deploy 2.3.0-dev-2 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 057b0e5ffd..08a7568fc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index dfef89566e..c184540502 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-1" +version = "2.3.0-dev-2" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 51c95ff9ce44210ad52aabd654fbc7bd0c9a4859 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 18:10:34 -0400 Subject: [PATCH 009/203] implement CommandOptions (timeout / cancel ability) throughout shell usage --- bin/core/src/api/execute/action.rs | 4 +- bin/core/src/api/execute/maintenance.rs | 4 +- bin/periphery/src/api/build/mod.rs | 23 ++-- bin/periphery/src/api/compose.rs | 43 ++++--- bin/periphery/src/api/container/mod.rs | 92 ++++++++++----- bin/periphery/src/api/container/run.rs | 5 +- bin/periphery/src/api/docker.rs | 60 +++++++--- bin/periphery/src/api/mod.rs | 10 +- bin/periphery/src/api/swarm/mod.rs | 12 +- bin/periphery/src/api/swarm/service.rs | 17 +-- bin/periphery/src/api/swarm/stack.rs | 16 +-- bin/periphery/src/docker/compose.rs | 4 +- bin/periphery/src/docker/config.rs | 25 ++-- bin/periphery/src/docker/image.rs | 6 +- bin/periphery/src/docker/mod.rs | 11 +- bin/periphery/src/docker/secret.rs | 21 +++- bin/periphery/src/docker/stack.rs | 8 +- bin/periphery/src/docker/stats.rs | 4 +- bin/periphery/src/helpers.rs | 8 +- lib/command/src/lib.rs | 151 ++++++++---------------- lib/command/src/options.rs | 45 +++++++ lib/git/src/clone.rs | 12 +- lib/git/src/commit.rs | 33 +++--- lib/git/src/init.rs | 15 ++- lib/git/src/lib.rs | 11 +- lib/git/src/pull.rs | 12 +- 26 files changed, 384 insertions(+), 268 deletions(-) create mode 100644 lib/command/src/options.rs diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 3764efadbe..536c726f35 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::Context; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use database::mungos::{ by_id::update_one_by_id, mongodb::bson::to_document, }; @@ -200,11 +200,11 @@ impl Resolve for RunAction { let mut res = run_komodo_standard_command( // Keep this stage name as is, the UI will find the latest update log by matching the stage name "Execute Action", - None, format!( "deno run --allow-all{https_cert_flag}{reload} {}", path.display() ), + CommandOptions::default(), ) .await; diff --git a/bin/core/src/api/execute/maintenance.rs b/bin/core/src/api/execute/maintenance.rs index 8db8ea95db..551219ba1f 100644 --- a/bin/core/src/api/execute/maintenance.rs +++ b/bin/core/src/api/execute/maintenance.rs @@ -1,7 +1,7 @@ use std::{fmt::Write as _, sync::OnceLock}; use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use database::{ bson::{Document, doc}, mungos::find::find_collect, @@ -167,8 +167,8 @@ impl Resolve for BackupCoreDatabase { let res = run_komodo_standard_command( "Backup Core Database", - None, "km database backup --yes", + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/api/build/mod.rs b/bin/periphery/src/api/build/mod.rs index b9fb37c22c..7abe951eeb 100644 --- a/bin/periphery/src/api/build/mod.rs +++ b/bin/periphery/src/api/build/mod.rs @@ -5,7 +5,8 @@ use std::{ use anyhow::{Context, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_standard_command, }; use formatting::format_serror; @@ -269,8 +270,8 @@ impl Resolve for build::Build { let span = info_span!("RunPreBuild"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Build", - pre_build_path.as_path(), &pre_build.command, + CommandOptions::default().path(pre_build_path.as_path()), if pre_build.shell_mode { KomodoCommandMode::Shell } else { @@ -329,8 +330,8 @@ impl Resolve for build::Build { let span = info_span!("RunDockerBuild"); if let Some(build_log) = run_komodo_command_with_sanitization( "Docker Build", - build_path.as_ref(), command, + CommandOptions::default().path(build_path.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -361,8 +362,12 @@ impl Resolve for PruneBuilders { ) -> anyhow::Result { let command = String::from("docker builder prune -a -f"); Ok( - run_komodo_standard_command("Prune Builders", None, command) - .await, + run_komodo_standard_command( + "Prune Builders", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -384,8 +389,12 @@ impl Resolve for PruneBuildx { ) -> anyhow::Result { let command = String::from("docker buildx prune -a -f"); Ok( - run_komodo_standard_command("Prune Buildx", None, command) - .await, + run_komodo_standard_command( + "Prune Buildx", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index d53e5016cc..b9a7d5b72a 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -2,8 +2,9 @@ use std::{borrow::Cow, fmt::Write, path::PathBuf}; use anyhow::{Context, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_shell_command, + run_komodo_standard_command, }; use formatting::format_serror; use git::write_commit_file; @@ -58,8 +59,12 @@ impl Resolve for GetComposeLog { services.join(" ") ); Ok( - run_komodo_standard_command("Get Stack Log", None, command) - .await, + run_komodo_standard_command( + "Get Stack Log", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -89,8 +94,12 @@ impl Resolve for GetComposeLogSearch { services.join(" ") ); Ok( - run_komodo_shell_command("Search Stack Log", None, command) - .await, + run_komodo_shell_command( + "Search Stack Log", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -390,8 +399,8 @@ impl Resolve for ComposePull { }; let Some(log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), pull_command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -488,8 +497,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecutePreDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Deploy", - pre_deploy_path.as_path(), &stack.config.pre_deploy.command, + CommandOptions::default().path(pre_deploy_path.as_path()), if stack.config.pre_deploy.shell_mode { KomodoCommandMode::Shell } else { @@ -567,8 +576,8 @@ impl Resolve for ComposeUp { let span = info_span!("GetComposeConfig", command); let Some(config_log) = run_komodo_command_with_sanitization( "Compose Config", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -651,8 +660,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecuteComposeBuild"); let Some(log) = run_komodo_command_with_sanitization( "Compose Build", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -694,8 +703,8 @@ impl Resolve for ComposeUp { let span = info_span!("RunComposePull"); let Some(log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -742,8 +751,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecuteComposeUp"); let Some(log) = run_komodo_command_with_sanitization( "Compose Up", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -762,8 +771,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecutePostDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Post Deploy", - post_deploy_path.as_path(), &stack.config.post_deploy.command, + CommandOptions::default().path(post_deploy_path.as_path()), if stack.config.post_deploy.shell_mode { KomodoCommandMode::Shell } else { @@ -803,8 +812,8 @@ impl Resolve for ComposeExecution { let docker_compose = docker_compose(); let log = run_komodo_standard_command( "Compose Command", - None, format!("{docker_compose} -p {project} {command}"), + CommandOptions::default(), ) .await; Ok(log) @@ -931,8 +940,8 @@ impl Resolve for ComposeRun { }; let Some(pull_log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), pull_command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -1002,8 +1011,8 @@ impl Resolve for ComposeRun { let span = info_span!("RunComposeRun", run_command); let Some(log) = run_komodo_command_with_sanitization( "Compose Run", - run_directory.as_path(), run_command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -1085,8 +1094,8 @@ async fn compose_down( }; let log = run_komodo_standard_command( "Compose Down", - None, format!("{docker_compose} -p {project} down{service_args}"), + CommandOptions::default(), ) .await; let success = log.success; diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index 2ecac8aad9..48eab5dfd4 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -1,6 +1,7 @@ use anyhow::Context; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use futures_util::future::join_all; use komodo_client::entities::{ @@ -61,8 +62,12 @@ impl Resolve for GetContainerLog { let command = format!("docker logs {name} --tail {tail}{timestamps}"); Ok( - run_komodo_standard_command("Get container log", None, command) - .await, + run_komodo_standard_command( + "Get container log", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -93,8 +98,8 @@ impl Resolve for GetContainerLogSearch { Ok( run_komodo_shell_command( "Get container log grep", - None, command, + CommandOptions::default(), ) .await, ) @@ -163,8 +168,8 @@ impl Resolve for StartContainer { Ok( run_komodo_standard_command( "Docker Start", - None, format!("docker start {}", self.name), + CommandOptions::default(), ) .await, ) @@ -190,8 +195,8 @@ impl Resolve for RestartContainer { Ok( run_komodo_standard_command( "Docker Restart", - None, format!("docker restart {}", self.name), + CommandOptions::default(), ) .await, ) @@ -217,8 +222,8 @@ impl Resolve for PauseContainer { Ok( run_komodo_standard_command( "Docker Pause", - None, format!("docker pause {}", self.name), + CommandOptions::default(), ) .await, ) @@ -242,8 +247,8 @@ impl Resolve for UnpauseContainer { Ok( run_komodo_standard_command( "Docker Unpause", - None, format!("docker unpause {}", self.name), + CommandOptions::default(), ) .await, ) @@ -268,13 +273,20 @@ impl Resolve for StopContainer { ) -> anyhow::Result { let StopContainer { name, signal, time } = self; let command = stop_container_command(&name, signal, time); - let log = - run_komodo_standard_command("Docker Stop", None, command).await; + let log = run_komodo_standard_command( + "Docker Stop", + command, + CommandOptions::default(), + ) + .await; if log.stderr.contains("unknown flag: --signal") { let command = stop_container_command(&name, None, time); - let mut log = - run_komodo_standard_command("Docker Stop", None, command) - .await; + let mut log = run_komodo_standard_command( + "Docker Stop", + command, + CommandOptions::default(), + ) + .await; log.stderr = format!( "old docker version: unable to use --signal flag{}", if !log.stderr.is_empty() { @@ -312,8 +324,8 @@ impl Resolve for RemoveContainer { format!("{stop_command} && docker container rm {name}"); let log = run_komodo_shell_command( "Docker Stop and Remove", - None, command, + CommandOptions::default(), ) .await; if log.stderr.contains("unknown flag: --signal") { @@ -322,8 +334,8 @@ impl Resolve for RemoveContainer { format!("{stop_command} && docker container rm {name}"); let mut log = run_komodo_shell_command( "Docker Stop and Remove", - None, command, + CommandOptions::default(), ) .await; log.stderr = format!( @@ -364,8 +376,12 @@ impl Resolve for RenameContainer { } = self; let command = format!("docker rename {curr_name} {new_name}"); Ok( - run_komodo_standard_command("Docker Rename", None, command) - .await, + run_komodo_standard_command( + "Docker Rename", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -387,8 +403,12 @@ impl Resolve for PruneContainers { ) -> anyhow::Result { let command = String::from("docker container prune -f"); Ok( - run_komodo_standard_command("Prune Containers", None, command) - .await, + run_komodo_standard_command( + "Prune Containers", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -426,8 +446,12 @@ impl Resolve for StartAllContainers { } let command = format!("docker start {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -468,8 +492,12 @@ impl Resolve for RestartAllContainers { } let command = format!("docker restart {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -510,8 +538,12 @@ impl Resolve for PauseAllContainers { } let command = format!("docker pause {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -552,8 +584,12 @@ impl Resolve for UnpauseAllContainers { } let command = format!("docker unpause {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -595,8 +631,8 @@ impl Resolve for StopAllContainers { Some(async move { run_komodo_standard_command( &format!("Docker stop {name}"), - None, stop_container_command(name, None, None), + CommandOptions::default(), ) .await }) diff --git a/bin/periphery/src/api/container/run.rs b/bin/periphery/src/api/container/run.rs index b409562c62..3a00201083 100644 --- a/bin/periphery/src/api/container/run.rs +++ b/bin/periphery/src/api/container/run.rs @@ -2,7 +2,8 @@ use std::fmt::Write; use anyhow::Context; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, }; use formatting::format_serror; use interpolate::Interpolator; @@ -107,8 +108,8 @@ impl Resolve for RunContainer { let span = info_span!("ExecuteDockerRun"); let Some(log) = run_komodo_command_with_sanitization( "Docker Run", - None, command, + CommandOptions::default(), KomodoCommandMode::Shell, &replacers, ) diff --git a/bin/periphery/src/api/docker.rs b/bin/periphery/src/api/docker.rs index 4f3eeae943..ce58a0eef8 100644 --- a/bin/periphery/src/api/docker.rs +++ b/bin/periphery/src/api/docker.rs @@ -1,7 +1,7 @@ use std::sync::OnceLock; use anyhow::Context; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::{ deployment::extract_registry_domain, docker::{ @@ -132,8 +132,8 @@ impl Resolve for PullImage { anyhow::Ok( run_komodo_standard_command( "Docker Pull", - None, format!("docker pull {name}"), + CommandOptions::default(), ) .await, ) @@ -166,8 +166,12 @@ impl Resolve for DeleteImage { ) -> anyhow::Result { let command = format!("docker image rm {}", self.name); Ok( - run_komodo_standard_command("Delete Image", None, command) - .await, + run_komodo_standard_command( + "Delete Image", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -189,8 +193,12 @@ impl Resolve for PruneImages { ) -> anyhow::Result { let command = String::from("docker image prune -a -f"); Ok( - run_komodo_standard_command("Prune Images", None, command) - .await, + run_komodo_standard_command( + "Prune Images", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -237,8 +245,12 @@ impl Resolve for CreateNetwork { }; let command = format!("docker network create{driver} {name}"); Ok( - run_komodo_standard_command("Create Network", None, command) - .await, + run_komodo_standard_command( + "Create Network", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -261,8 +273,12 @@ impl Resolve for DeleteNetwork { ) -> anyhow::Result { let command = format!("docker network rm {}", self.name); Ok( - run_komodo_standard_command("Delete Network", None, command) - .await, + run_komodo_standard_command( + "Delete Network", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -284,8 +300,12 @@ impl Resolve for PruneNetworks { ) -> anyhow::Result { let command = String::from("docker network prune -f"); Ok( - run_komodo_standard_command("Prune Networks", None, command) - .await, + run_komodo_standard_command( + "Prune Networks", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -326,8 +346,12 @@ impl Resolve for DeleteVolume { ) -> anyhow::Result { let command = format!("docker volume rm {}", self.name); Ok( - run_komodo_standard_command("Delete Volume", None, command) - .await, + run_komodo_standard_command( + "Delete Volume", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -349,8 +373,12 @@ impl Resolve for PruneVolumes { ) -> anyhow::Result { let command = String::from("docker volume prune -a -f"); Ok( - run_komodo_standard_command("Prune Volumes", None, command) - .await, + run_komodo_standard_command( + "Prune Volumes", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index ab9c19382f..8aa54c4d49 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -1,4 +1,4 @@ -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use encoding::{EncodedJsonMessage, EncodedResponse}; use komodo_client::entities::{ config::{DockerRegistry, GitProvider}, @@ -260,8 +260,12 @@ impl Resolve for PruneSystem { async fn resolve(self, args: &Args) -> anyhow::Result { let command = String::from("docker system prune -a -f --volumes"); Ok( - run_komodo_standard_command("Prune System", None, command) - .await, + run_komodo_standard_command( + "Prune System", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/swarm/mod.rs b/bin/periphery/src/api/swarm/mod.rs index 4e30a219a7..6817c8fe0f 100644 --- a/bin/periphery/src/api/swarm/mod.rs +++ b/bin/periphery/src/api/swarm/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context as _; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::{ docker::{SwarmLists, node::SwarmNode, task::SwarmTask}, update::Log, @@ -116,8 +116,12 @@ impl Resolve for UpdateSwarmNode { command += &self.node; Ok( - run_komodo_standard_command("Update Swarm Node", None, command) - .await, + run_komodo_standard_command( + "Update Swarm Node", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -148,8 +152,8 @@ impl Resolve for RemoveSwarmNodes { Ok( run_komodo_standard_command( "Remove Swarm Nodes", - None, command, + CommandOptions::default(), ) .await, ) diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 679e46bdb9..7e80256008 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -2,8 +2,9 @@ use std::fmt::Write; use anyhow::Context as _; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_shell_command, + run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; @@ -87,8 +88,8 @@ impl Resolve for GetSwarmServiceLog { Ok( run_komodo_standard_command( "Get Swarm Service Log", - None, command, + CommandOptions::default(), ) .await, ) @@ -137,8 +138,8 @@ impl Resolve for GetSwarmServiceLogSearch { Ok( run_komodo_shell_command( "Search Swarm Service Log", - None, command, + CommandOptions::default(), ) .await, ) @@ -167,8 +168,8 @@ impl Resolve for RemoveSwarmServices { Ok( run_komodo_standard_command( "Remove Swarm Services", - None, command, + CommandOptions::default(), ) .await, ) @@ -192,8 +193,8 @@ impl Resolve for RollbackSwarmService { Ok( run_komodo_standard_command( "Rollback Swarm Service", - None, format!("docker service rollback {}", self.service), + CommandOptions::default(), ) .await, ) @@ -293,8 +294,8 @@ impl Resolve for CreateSwarmService { let span = info_span!("ExecuteDockerServiceCreate"); if let Some(log) = run_komodo_command_with_sanitization( "Docker Service Create", - None, command, + CommandOptions::default(), KomodoCommandMode::Shell, &replacers, ) @@ -435,8 +436,8 @@ impl Resolve for UpdateSwarmService { let span = info_span!("ExecuteDockerServiceCreate"); let log = run_komodo_standard_command( "Docker Service Create", - None, command, + CommandOptions::default(), ) .instrument(span) .await; diff --git a/bin/periphery/src/api/swarm/stack.rs b/bin/periphery/src/api/swarm/stack.rs index b1fbc17628..93837e6b81 100644 --- a/bin/periphery/src/api/swarm/stack.rs +++ b/bin/periphery/src/api/swarm/stack.rs @@ -2,8 +2,8 @@ use std::fmt::Write; use anyhow::{Context as _, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; @@ -99,8 +99,8 @@ impl Resolve for RemoveSwarmStacks { Ok( run_komodo_standard_command( "Remove Swarm Stacks", - None, command, + CommandOptions::default(), ) .await, ) @@ -185,8 +185,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecutePreDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Deploy", - pre_deploy_path.as_path(), &stack.config.pre_deploy.command, + CommandOptions::default().path(pre_deploy_path.as_path()), KomodoCommandMode::Multiline, &replacers, ) @@ -251,8 +251,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("GetStackConfig", command); let Some(config_log) = run_komodo_command_with_sanitization( "Stack Config", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -323,8 +323,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecuteStackDeploy"); let Some(log) = run_komodo_command_with_sanitization( "Deploy Swarm Stack", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -343,8 +343,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecutePostDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Post Deploy", - post_deploy_path.as_path(), &stack.config.post_deploy.command, + CommandOptions::default().path(post_deploy_path.as_path()), KomodoCommandMode::Multiline, &replacers, ) @@ -403,8 +403,8 @@ async fn remove_stack( ) -> anyhow::Result<()> { let log = run_komodo_standard_command( "Remove Stack", - None, format!("docker stack rm --detach=false {stack}"), + CommandOptions::default(), ) .await; let success = log.success; diff --git a/bin/periphery/src/docker/compose.rs b/bin/periphery/src/docker/compose.rs index 996a249a4d..d79ed0f8be 100644 --- a/bin/periphery/src/docker/compose.rs +++ b/bin/periphery/src/docker/compose.rs @@ -1,5 +1,5 @@ use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::stack::ComposeProject; use serde::{Deserialize, Serialize}; @@ -18,8 +18,8 @@ pub async fn list_compose_projects() let docker_compose = docker_compose(); let res = run_komodo_standard_command( "List Projects", - None, format!("{docker_compose} ls --all --format json"), + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 056f580de8..7c5d3ed94e 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -2,7 +2,8 @@ use std::fmt::Write; use anyhow::{Context, anyhow}; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use data_encoding::BASE64URL; use futures_util::{TryStreamExt as _, stream::FuturesUnordered}; @@ -23,8 +24,8 @@ pub async fn list_swarm_configs( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Configs", - None, "docker config ls --format json", + CommandOptions::default(), ) .await; @@ -68,8 +69,8 @@ pub async fn inspect_swarm_config( ) -> anyhow::Result { let res = run_komodo_standard_command( "Inspect Swarm Config", - None, format!(r#"docker config inspect "{config}""#), + CommandOptions::default(), ) .await; @@ -133,8 +134,12 @@ EOF"#, data.trim() )?; - let log = - run_komodo_shell_command("Create Config", None, command).await; + let log = run_komodo_shell_command( + "Create Config", + command, + CommandOptions::default(), + ) + .await; Ok(log) } @@ -147,8 +152,12 @@ pub async fn remove_swarm_configs( command += " "; command += config; } - run_komodo_standard_command("Remove Swarm Configs", None, command) - .await + run_komodo_standard_command( + "Remove Swarm Configs", + command, + CommandOptions::default(), + ) + .await } pub async fn recreate_swarm_config( @@ -323,8 +332,8 @@ async fn switch_service_config( let log = run_komodo_standard_command( "Switch Service Config", - None, command, + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/image.rs b/bin/periphery/src/docker/image.rs index 2fa542661a..03c327ca5b 100644 --- a/bin/periphery/src/docker/image.rs +++ b/bin/periphery/src/docker/image.rs @@ -1,6 +1,6 @@ use anyhow::Context; use bollard::query_parameters::ListImagesOptions; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::docker::{ GraphDriverData, HealthConfig, container::ContainerListItem, image::*, @@ -193,7 +193,9 @@ pub async fn get_image_digest_from_registry( let command = String::from( r#"docker buildx imagetools inspect --format "{{json .Manifest}}" "#, ) + image; - let log = run_komodo_standard_command("", None, command).await; + let log = + run_komodo_standard_command("", command, CommandOptions::default()) + .await; if !log.success { return Err(anyhow::Error::msg(log.combined())); } diff --git a/bin/periphery/src/docker/mod.rs b/bin/periphery/src/docker/mod.rs index dea87e4e69..34ba6f5a58 100644 --- a/bin/periphery/src/docker/mod.rs +++ b/bin/periphery/src/docker/mod.rs @@ -1,7 +1,7 @@ use anyhow::{Context, anyhow}; use bollard::Docker; use command::{ - CommandConfig, run_komodo_standard_command, run_shell_command, + CommandOptions, run_komodo_standard_command, run_shell_command, }; use komodo_client::entities::{ TerminationSignal, @@ -55,7 +55,7 @@ pub async fn docker_login( let log = run_shell_command(&format!( "echo {registry_token} | docker login {domain} --username '{account}' --password-stdin", - ), None, CommandConfig::default()) + ), CommandOptions::default()) .await; if log.success() { @@ -79,7 +79,12 @@ pub async fn docker_login( #[instrument("PullImage")] pub async fn pull_image(image: &str) -> Log { let command = format!("docker pull {image}"); - run_komodo_standard_command("Docker Pull", None, command).await + run_komodo_standard_command( + "Docker Pull", + command, + CommandOptions::default(), + ) + .await } pub fn stop_container_command( diff --git a/bin/periphery/src/docker/secret.rs b/bin/periphery/src/docker/secret.rs index 058310ab35..9c53ed1c34 100644 --- a/bin/periphery/src/docker/secret.rs +++ b/bin/periphery/src/docker/secret.rs @@ -3,7 +3,8 @@ use std::fmt::Write; use anyhow::Context; use bollard::query_parameters::ListSecretsOptions; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use futures_util::{TryStreamExt as _, stream::FuturesUnordered}; use komodo_client::entities::{ @@ -149,8 +150,12 @@ EOF"#, EOF"# )?; - let mut log = - run_komodo_shell_command("Create Secret", None, command).await; + let mut log = run_komodo_shell_command( + "Create Secret", + command, + CommandOptions::default(), + ) + .await; log.command = sanitized_command; @@ -165,8 +170,12 @@ pub async fn remove_swarm_secrets( command += " "; command += secret; } - run_komodo_standard_command("Remove Swarm Secrets", None, command) - .await + run_komodo_standard_command( + "Remove Swarm Secrets", + command, + CommandOptions::default(), + ) + .await } pub async fn recreate_swarm_secret( @@ -344,8 +353,8 @@ async fn switch_service_secret( let log = run_komodo_standard_command( "Switch Service Secret", - None, command, + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/stack.rs b/bin/periphery/src/docker/stack.rs index 0069e34963..ac4bdf5345 100644 --- a/bin/periphery/src/docker/stack.rs +++ b/bin/periphery/src/docker/stack.rs @@ -1,5 +1,5 @@ use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ docker::{ @@ -71,8 +71,8 @@ pub async fn list_swarm_stacks( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stacks", - None, "docker stack ls --format json", + CommandOptions::default(), ) .await; @@ -130,8 +130,8 @@ pub async fn list_swarm_stack_service_ids( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Services", - None, format!("docker stack services --format json {stack}"), + CommandOptions::default(), ) .await; @@ -160,8 +160,8 @@ pub async fn list_swarm_stack_tasks( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Tasks", - None, format!("docker stack ps --format json --no-trunc {stack}"), + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index 2f7555c914..9633bc66c9 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::{Context, anyhow}; use async_timing_util::wait_until_timelength; use bollard::{models, query_parameters::StatsOptionsBuilder}; -use command::{CommandConfig, run_standard_command}; +use command::{CommandOptions, run_standard_command}; use futures_util::StreamExt; use komodo_client::entities::docker::{ container::ContainerStats, @@ -66,7 +66,7 @@ pub async fn get_container_stats( let command = format!("docker stats{container_name} --no-stream {format}"); let output = - run_standard_command(&command, None, CommandConfig::default()).await; + run_standard_command(&command, CommandOptions::default()).await; if output.success() { output .stdout diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index d8d70fc454..c2ecd3bb47 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::Context; use command::{ - CommandConfig, KomodoCommandMode, + CommandOptions, KomodoCommandMode, run_komodo_command_with_sanitization, run_standard_command, }; use environment::write_env_file; @@ -187,8 +187,8 @@ pub async fn handle_post_repo_execution( .collect::(); if let Some(log) = run_komodo_command_with_sanitization( "On Clone", - path.as_path(), on_clone.command, + CommandOptions::default().path(path.as_path()), if on_clone.shell_mode { KomodoCommandMode::Shell } else { @@ -216,8 +216,8 @@ pub async fn handle_post_repo_execution( .collect::(); if let Some(log) = run_komodo_command_with_sanitization( "On Pull", - path.as_path(), on_pull.command, + CommandOptions::default().path(path.as_path()), if on_pull.shell_mode { KomodoCommandMode::Shell } else { @@ -368,7 +368,7 @@ async fn generate_self_signed_ssl_certs() { "openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\"" ); let log = - run_standard_command(&command, None, CommandConfig::default()).await; + run_standard_command(&command, CommandOptions::default()).await; if log.success() { info!("✅ SSL Certs generated"); diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index 07e79908c7..c33b6632db 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -1,45 +1,38 @@ -use std::{ - path::{Path, PathBuf}, - process::Stdio, - sync::OnceLock, - time::Duration, -}; +use std::{path::PathBuf, process::Stdio, sync::OnceLock}; use komodo_client::{ entities::{komodo_timestamp, update::Log}, parsers::parse_multiline_command, }; +use tokio::process::Command; +mod options; mod output; +pub use options::*; pub use output::*; -use tokio::process::Command; -use tokio_util::sync::CancellationToken; /// Commands are run directly, and cannot include '&&' pub async fn run_komodo_standard_command( stage: &str, - path: impl Into>, command: impl Into, + options: CommandOptions<'_>, ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = - run_standard_command(&command, path, CommandConfig::default()) - .await; + let output = run_standard_command(&command, options).await; output_into_log(stage, command, start_ts, output) } /// Commands are wrapped in 'sh -c', and can include '&&' pub async fn run_komodo_shell_command( stage: &str, - path: impl Into>, command: impl Into, + options: CommandOptions<'_>, ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = - run_shell_command(&command, path, CommandConfig::default()).await; + let output = run_shell_command(&command, options).await; output_into_log(stage, command, start_ts, output) } @@ -52,14 +45,14 @@ pub async fn run_komodo_shell_command( /// ie if all the lines are commented out. pub async fn run_komodo_multiline_command( stage: &str, - path: impl Into>, command: impl AsRef, + options: CommandOptions<'_>, ) -> Option { let command = parse_multiline_command(command); if command.is_empty() { return None; } - Some(run_komodo_shell_command(stage, path, command).await) + Some(run_komodo_shell_command(stage, command, options).await) } pub enum KomodoCommandMode { @@ -78,28 +71,24 @@ pub enum KomodoCommandMode { /// See [parse_multiline_command]. pub async fn run_komodo_command_with_sanitization( stage: &str, - path: impl Into>, command: impl AsRef, + options: CommandOptions<'_>, mode: KomodoCommandMode, replacers: &[(String, String)], ) -> Option { let mut log = match mode { - KomodoCommandMode::Standard => run_komodo_standard_command( - stage, - path, - command.as_ref().to_string(), - ) - .await - .into(), - KomodoCommandMode::Shell => run_komodo_shell_command( - stage, - path, - command.as_ref().to_string(), - ) - .await - .into(), + KomodoCommandMode::Standard => { + run_komodo_standard_command(stage, command.as_ref(), options) + .await + .into() + } + KomodoCommandMode::Shell => { + run_komodo_shell_command(stage, command.as_ref(), options) + .await + .into() + } KomodoCommandMode::Multiline => { - run_komodo_multiline_command(stage, path, command).await + run_komodo_multiline_command(stage, command, options).await } }?; @@ -131,11 +120,10 @@ pub fn output_into_log( /// Commands are run directly, and cannot include '&&'. /// -/// See [CommandConfig] for the available `timeout` / `cancel` controls. +/// See [CommandOptions] for the available `timeout` / `cancel` controls. pub async fn run_standard_command( command: &str, - path: impl Into>, - config: CommandConfig, + options: CommandOptions<'_>, ) -> CommandOutput { let lexed = if let Some(lexed) = shlex::split(command) && !lexed.is_empty() @@ -156,16 +144,7 @@ pub async fn run_standard_command( .stdout(Stdio::piped()) .stderr(Stdio::piped()); - if let Some(path) = path.into() { - match path.canonicalize() { - Ok(path) => { - cmd.current_dir(path); - } - Err(e) => return CommandOutput::from_err(e), - } - } - - run_command(cmd, config).await + run_command(cmd, options).await } fn shell() -> &'static str { @@ -188,11 +167,10 @@ fn shell() -> &'static str { /// Commands are wrapped in 'sh -c', and can include '&&'. /// -/// See [CommandConfig] for the available `timeout` / `cancel` controls. +/// See [CommandOptions] for the available `timeout` / `cancel` controls. pub async fn run_shell_command( command: &str, - path: impl Into>, - config: CommandConfig, + options: CommandOptions<'_>, ) -> CommandOutput { let mut cmd = Command::new(shell()); @@ -201,55 +179,12 @@ pub async fn run_shell_command( .kill_on_drop(true) .stdin(Stdio::null()); - if let Some(path) = path.into() { - match path.canonicalize() { - Ok(path) => { - cmd.current_dir(path); - } - Err(e) => return CommandOutput::from_err(e), - } - } - - run_command(cmd, config).await -} - -/// Optional controls for how a command is executed. -/// -/// When neither field is set the command simply runs to completion. -/// When either is set, the child is spawned in its own process group so -/// that, on timeout or cancellation, the entire group (the command and any -/// descendants it spawned) is killed together — not just the direct child. -#[derive(Default, Clone)] -pub struct CommandConfig { - /// Kill the command (and its process group) if this duration elapses - /// before it finishes. - pub timeout: Option, - /// Kill the command (and its process group) when this token is - /// cancelled, allowing cancellation from elsewhere. - pub cancel: Option, -} - -impl CommandConfig { - pub fn timeout( - mut self, - timeout: impl Into>, - ) -> Self { - self.timeout = timeout.into(); - self - } - - pub fn cancel( - mut self, - cancel: impl Into>, - ) -> Self { - self.cancel = cancel.into(); - self - } + run_command(cmd, options).await } /// Runs the command to completion, returning its output. /// -/// With an empty `config`, this is just `cmd.output()`. +/// With an empty `options`, this is just `cmd.output()`. /// /// With a `timeout` and/or `cancel` token, the child is spawned in its own /// process group (via `process_group(0)`, so the child's pid is also its @@ -260,9 +195,23 @@ impl CommandConfig { /// `kill_on_drop(true)` remains set as a backstop to reap the direct child. async fn run_command( mut cmd: Command, - config: CommandConfig, + options: CommandOptions<'_>, ) -> CommandOutput { - let CommandConfig { timeout, cancel } = config; + let CommandOptions { + path, + timeout, + cancel, + } = options; + + // Attach the path to cmd as current dir + if let Some(path) = path { + match path.canonicalize() { + Ok(path) => { + cmd.current_dir(path); + } + Err(e) => return CommandOutput::from_err(e), + } + } // Fast path: nothing to wait on besides the command itself. if timeout.is_none() && cancel.is_none() { @@ -336,6 +285,10 @@ fn kill_process_group(pid: Option) { #[cfg(test)] mod tests { + use std::time::Duration; + + use tokio_util::sync::CancellationToken; + use super::*; /// On timeout, a backgrounded grandchild (here `sleep 31337`, started @@ -347,8 +300,7 @@ mod tests { let marker = "sleep 31337"; let out = run_shell_command( &format!("{marker} & sleep 31336"), - None, - CommandConfig::default().timeout(Duration::from_millis(300)), + CommandOptions::default().timeout(Duration::from_millis(300)), ) .await; @@ -390,8 +342,7 @@ mod tests { let out = run_shell_command( &format!("{marker} & sleep 41336"), - None, - CommandConfig::default().cancel(cancel), + CommandOptions::default().cancel(cancel), ) .await; diff --git a/lib/command/src/options.rs b/lib/command/src/options.rs new file mode 100644 index 0000000000..5c78cf7be8 --- /dev/null +++ b/lib/command/src/options.rs @@ -0,0 +1,45 @@ +use std::{path::Path, time::Duration}; + +use tokio_util::sync::CancellationToken; + +/// Controls for how a command is executed. +/// +/// When either timeout or cancel is set, the child is spawned in its own process group so +/// that, on timeout or cancellation, the entire group (the command and any +/// descendants it spawned) is killed together — not just the direct child. +#[derive(Default, Clone)] +pub struct CommandOptions<'a> { + /// Run the command at a particular path + pub path: Option<&'a Path>, + /// Kill the command (and its process group) if this duration elapses + /// before it finishes. + pub timeout: Option, + /// Kill the command (and its process group) when this token is + /// cancelled, allowing cancellation from elsewhere. + pub cancel: Option, +} + +impl<'a> CommandOptions<'a> { + pub fn path(mut self, path: impl Into>) -> Self { + self.path = path.into(); + self + } +} + +impl CommandOptions<'_> { + pub fn timeout( + mut self, + timeout: impl Into>, + ) -> Self { + self.timeout = timeout.into(); + self + } + + pub fn cancel( + mut self, + cancel: impl Into>, + ) -> Self { + self.cancel = cancel.into(); + self + } +} diff --git a/lib/git/src/clone.rs b/lib/git/src/clone.rs index 0f3371f34c..c9b2d6dca7 100644 --- a/lib/git/src/clone.rs +++ b/lib/git/src/clone.rs @@ -1,7 +1,7 @@ use std::{io::ErrorKind, path::Path}; use anyhow::Context; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, RepoExecutionResponse, all_logs_success, @@ -70,8 +70,12 @@ where args.branch ); - let mut log = - run_komodo_standard_command("Clone Repo", None, command).await; + let mut log = run_komodo_standard_command( + "Clone Repo", + command, + CommandOptions::default(), + ) + .await; if let Some(token) = access_token { log.command = log.command.replace(&token, ""); @@ -88,8 +92,8 @@ where if let Some(commit) = args.commit { let reset_log = run_komodo_standard_command( "set commit", - res.path.as_path(), format!("git reset --hard {commit}",), + CommandOptions::default().path(res.path.as_path()), ) .await; res.logs.push(reset_log); diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs index 7776de5830..87f45b9863 100644 --- a/lib/git/src/commit.rs +++ b/lib/git/src/commit.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use anyhow::Context; use command::{ - CommandConfig, run_komodo_standard_command, run_standard_command, + CommandOptions, run_komodo_standard_command, run_standard_command, }; use formatting::format_serror; use komodo_client::entities::{ @@ -97,8 +97,8 @@ pub async fn commit_file_inner( let add_log = run_komodo_standard_command( "Add Files", - repo_dir, format!("git add {}", file.display()), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(add_log); @@ -108,10 +108,10 @@ pub async fn commit_file_inner( let commit_log = run_komodo_standard_command( "Commit", - repo_dir, format!( r#"git commit -m "[Komodo] {commit_msg}: update {file:?}""#, ), + CommandOptions::default().path(repo_dir), ) .await; @@ -142,8 +142,8 @@ pub async fn commit_file_inner( let push_log = run_komodo_standard_command( "Push", - repo_dir, format!("git push --set-upstream origin {branch}"), + CommandOptions::default().path(repo_dir), ) .await; @@ -166,9 +166,12 @@ pub async fn commit_all( commit_message: None, }; - let add_log = - run_komodo_standard_command("Add Files", repo_dir, "git add -A") - .await; + let add_log = run_komodo_standard_command( + "Add Files", + "git add -A", + CommandOptions::default().path(repo_dir), + ) + .await; res.logs.push(add_log); if !all_logs_success(&res.logs) { return res; @@ -176,8 +179,8 @@ pub async fn commit_all( let commit_log = run_komodo_standard_command( "Commit", - repo_dir, format!(r#"git commit -m "[Komodo] {message}""#), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(commit_log); @@ -202,8 +205,8 @@ pub async fn commit_all( let push_log = run_komodo_standard_command( "Push", - repo_dir, format!("git push --set-upstream origin {branch}"), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(push_log); @@ -214,29 +217,25 @@ pub async fn commit_all( async fn ensure_global_git_config_set() { let res = run_standard_command( "git config --global --get user.email", - None, - CommandConfig::default(), + CommandOptions::default(), ) .await; if !res.success() { let _ = run_standard_command( "git config --global user.email komodo@komo.do", - None, - CommandConfig::default(), + CommandOptions::default(), ) .await; } let res = run_standard_command( "git config --global --get user.name", - None, - CommandConfig::default(), + CommandOptions::default(), ) .await; if !res.success() { let _ = run_standard_command( "git config --global user.name komodo", - None, - CommandConfig::default(), + CommandOptions::default(), ) .await; } diff --git a/lib/git/src/init.rs b/lib/git/src/init.rs index 33b159fa9c..8ecd4daeb7 100644 --- a/lib/git/src/init.rs +++ b/lib/git/src/init.rs @@ -1,6 +1,6 @@ use std::path::Path; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, all_logs_success, update::Log, @@ -13,9 +13,12 @@ pub async fn init_folder_as_repo( logs: &mut Vec, ) { // Initialize the folder as a git repo - let init_repo = - run_komodo_standard_command("Git Init", folder_path, "git init") - .await; + let init_repo = run_komodo_standard_command( + "Git Init", + "git init", + CommandOptions::default().path(folder_path), + ) + .await; logs.push(init_repo); if !all_logs_success(logs) { return; @@ -33,8 +36,8 @@ pub async fn init_folder_as_repo( // Set remote url let mut set_remote = run_komodo_standard_command( "Add git remote", - folder_path, format!("git remote add origin {repo_url}"), + CommandOptions::default().path(folder_path), ) .await; // Sanitize the output @@ -51,8 +54,8 @@ pub async fn init_folder_as_repo( // Set branch. let init_repo = run_komodo_standard_command( "Set Branch", - folder_path, format!("git switch -c {}", args.branch), + CommandOptions::default().path(folder_path), ) .await; if !init_repo.success { diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index ca75149684..6a126d2195 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::anyhow; -use command::{CommandConfig, run_standard_command}; +use command::{CommandOptions, run_standard_command}; use formatting::{bold, muted}; use komodo_client::entities::{ LatestCommit, komodo_timestamp, update::Log, @@ -26,8 +26,7 @@ pub async fn get_commit_hash_info( ) -> anyhow::Result { let hash = run_standard_command( "git rev-parse --short HEAD", - repo_dir, - CommandConfig::default(), + CommandOptions::default().path(repo_dir), ) .await; let hash = if hash.status.success() { @@ -40,8 +39,7 @@ pub async fn get_commit_hash_info( }; let message = run_standard_command( "git log -1 --pretty=%B", - repo_dir, - CommandConfig::default(), + CommandOptions::default().path(repo_dir), ) .await; let message = if message.status.success() { @@ -85,8 +83,7 @@ pub async fn get_commit_hash_log( pub async fn get_remote_url(path: &Path) -> anyhow::Result { let output = run_standard_command( "git remote show origin", - path, - CommandConfig::default(), + CommandOptions::default().path(path), ) .await; if output.success() { diff --git a/lib/git/src/pull.rs b/lib/git/src/pull.rs index c9e5e76955..ad33e780e5 100644 --- a/lib/git/src/pull.rs +++ b/lib/git/src/pull.rs @@ -3,7 +3,7 @@ use std::{ sync::OnceLock, }; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, RepoExecutionResponse, all_logs_success, @@ -78,8 +78,8 @@ where // Set remote url let mut set_remote = run_komodo_standard_command( "Set Git Remote", - res.path.as_ref(), format!("git remote set-url origin {repo_url}"), + CommandOptions::default().path(res.path.as_ref()), ) .await; // Sanitize the output @@ -99,8 +99,8 @@ where // First fetch remote branches before checkout let fetch = run_komodo_standard_command( "Git Fetch", - res.path.as_ref(), "git fetch --all --prune", + CommandOptions::default().path(res.path.as_ref()), ) .await; if !fetch.success { @@ -110,8 +110,8 @@ where let checkout = run_komodo_standard_command( "Checkout branch", - res.path.as_ref(), format!("git checkout -f {}", args.branch), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(checkout); @@ -121,8 +121,8 @@ where let pull_log = run_komodo_standard_command( "Git pull", - res.path.as_ref(), format!("git pull --rebase --force origin {}", args.branch), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(pull_log); @@ -133,8 +133,8 @@ where if let Some(commit) = args.commit { let reset_log = run_komodo_standard_command( "Set commit", - res.path.as_ref(), format!("git reset --hard {commit}"), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(reset_log); From 6e51b6d0308e815ebf24e8cc261decf445367762 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 19:10:06 -0400 Subject: [PATCH 010/203] Add timeouts to read child commands --- bin/periphery/src/api/compose.rs | 6 +++--- bin/periphery/src/api/container/mod.rs | 6 +++--- bin/periphery/src/api/swarm/service.rs | 6 +++--- bin/periphery/src/docker/compose.rs | 6 ++++-- bin/periphery/src/docker/config.rs | 6 +++--- bin/periphery/src/docker/image.rs | 13 +++++++++---- bin/periphery/src/docker/stack.rs | 10 ++++++---- bin/periphery/src/docker/stats.rs | 11 ++++++++--- lib/command/src/options.rs | 6 ++++++ lib/git/src/lib.rs | 16 ++++++++++++---- 10 files changed, 57 insertions(+), 29 deletions(-) diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index b9a7d5b72a..9769e0d673 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt::Write, path::PathBuf}; use anyhow::{Context, anyhow}; use command::{ - CommandOptions, KomodoCommandMode, + CommandOptions, KomodoCommandMode, QUICK_COMMAND_TIMEOUT, run_komodo_command_with_sanitization, run_komodo_shell_command, run_komodo_standard_command, }; @@ -62,7 +62,7 @@ impl Resolve for GetComposeLog { run_komodo_standard_command( "Get Stack Log", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) @@ -97,7 +97,7 @@ impl Resolve for GetComposeLogSearch { run_komodo_shell_command( "Search Stack Log", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index 48eab5dfd4..77857ed2ce 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -1,6 +1,6 @@ use anyhow::Context; use command::{ - CommandOptions, run_komodo_shell_command, + CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_shell_command, run_komodo_standard_command, }; use futures_util::future::join_all; @@ -65,7 +65,7 @@ impl Resolve for GetContainerLog { run_komodo_standard_command( "Get container log", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) @@ -99,7 +99,7 @@ impl Resolve for GetContainerLogSearch { run_komodo_shell_command( "Get container log grep", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 7e80256008..8b534ce27b 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use anyhow::Context as _; use command::{ - CommandOptions, KomodoCommandMode, + CommandOptions, KomodoCommandMode, QUICK_COMMAND_TIMEOUT, run_komodo_command_with_sanitization, run_komodo_shell_command, run_komodo_standard_command, }; @@ -89,7 +89,7 @@ impl Resolve for GetSwarmServiceLog { run_komodo_standard_command( "Get Swarm Service Log", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) @@ -139,7 +139,7 @@ impl Resolve for GetSwarmServiceLogSearch { run_komodo_shell_command( "Search Swarm Service Log", command, - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await, ) diff --git a/bin/periphery/src/docker/compose.rs b/bin/periphery/src/docker/compose.rs index d79ed0f8be..bec4842c23 100644 --- a/bin/periphery/src/docker/compose.rs +++ b/bin/periphery/src/docker/compose.rs @@ -1,5 +1,7 @@ use anyhow::{Context, anyhow}; -use command::{CommandOptions, run_komodo_standard_command}; +use command::{ + CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, +}; use komodo_client::entities::stack::ComposeProject; use serde::{Deserialize, Serialize}; @@ -19,7 +21,7 @@ pub async fn list_compose_projects() let res = run_komodo_standard_command( "List Projects", format!("{docker_compose} ls --all --format json"), - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 7c5d3ed94e..8c498a2e27 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use anyhow::{Context, anyhow}; use command::{ - CommandOptions, run_komodo_shell_command, + CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_shell_command, run_komodo_standard_command, }; use data_encoding::BASE64URL; @@ -25,7 +25,7 @@ pub async fn list_swarm_configs( let res = run_komodo_standard_command( "List Swarm Configs", "docker config ls --format json", - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; @@ -70,7 +70,7 @@ pub async fn inspect_swarm_config( let res = run_komodo_standard_command( "Inspect Swarm Config", format!(r#"docker config inspect "{config}""#), - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; diff --git a/bin/periphery/src/docker/image.rs b/bin/periphery/src/docker/image.rs index 03c327ca5b..9f024fce17 100644 --- a/bin/periphery/src/docker/image.rs +++ b/bin/periphery/src/docker/image.rs @@ -1,6 +1,8 @@ use anyhow::Context; use bollard::query_parameters::ListImagesOptions; -use command::{CommandOptions, run_komodo_standard_command}; +use command::{ + CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, +}; use komodo_client::entities::docker::{ GraphDriverData, HealthConfig, container::ContainerListItem, image::*, @@ -193,9 +195,12 @@ pub async fn get_image_digest_from_registry( let command = String::from( r#"docker buildx imagetools inspect --format "{{json .Manifest}}" "#, ) + image; - let log = - run_komodo_standard_command("", command, CommandOptions::default()) - .await; + let log = run_komodo_standard_command( + "", + command, + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + ) + .await; if !log.success { return Err(anyhow::Error::msg(log.combined())); } diff --git a/bin/periphery/src/docker/stack.rs b/bin/periphery/src/docker/stack.rs index ac4bdf5345..9f2a2ca61f 100644 --- a/bin/periphery/src/docker/stack.rs +++ b/bin/periphery/src/docker/stack.rs @@ -1,5 +1,7 @@ use anyhow::{Context, anyhow}; -use command::{CommandOptions, run_komodo_standard_command}; +use command::{ + CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, +}; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ docker::{ @@ -72,7 +74,7 @@ pub async fn list_swarm_stacks( let res = run_komodo_standard_command( "List Swarm Stacks", "docker stack ls --format json", - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; @@ -131,7 +133,7 @@ pub async fn list_swarm_stack_service_ids( let res = run_komodo_standard_command( "List Swarm Stack Services", format!("docker stack services --format json {stack}"), - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; @@ -161,7 +163,7 @@ pub async fn list_swarm_stack_tasks( let res = run_komodo_standard_command( "List Swarm Stack Tasks", format!("docker stack ps --format json --no-trunc {stack}"), - CommandOptions::default(), + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), ) .await; diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index 9633bc66c9..f6178201c5 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -3,7 +3,9 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::{Context, anyhow}; use async_timing_util::wait_until_timelength; use bollard::{models, query_parameters::StatsOptionsBuilder}; -use command::{CommandOptions, run_standard_command}; +use command::{ + CommandOptions, QUICK_COMMAND_TIMEOUT, run_standard_command, +}; use futures_util::StreamExt; use komodo_client::entities::docker::{ container::ContainerStats, @@ -65,8 +67,11 @@ pub async fn get_container_stats( }; let command = format!("docker stats{container_name} --no-stream {format}"); - let output = - run_standard_command(&command, CommandOptions::default()).await; + let output = run_standard_command( + &command, + CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + ) + .await; if output.success() { output .stdout diff --git a/lib/command/src/options.rs b/lib/command/src/options.rs index 5c78cf7be8..c8df472c18 100644 --- a/lib/command/src/options.rs +++ b/lib/command/src/options.rs @@ -2,6 +2,12 @@ use std::{path::Path, time::Duration}; use tokio_util::sync::CancellationToken; +/// A sensible timeout for quick read-only / inspection commands +/// (listing, logs, stats, inspect, status, etc). These should resolve in +/// at most a few seconds; the timeout exists purely to avoid hanging +/// indefinitely on a stuck docker daemon, network mount, or git process. +pub const QUICK_COMMAND_TIMEOUT: Duration = Duration::from_secs(60); + /// Controls for how a command is executed. /// /// When either timeout or cancel is set, the child is spawned in its own process group so diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index 6a126d2195..1a6f57b074 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -1,7 +1,9 @@ use std::path::Path; use anyhow::anyhow; -use command::{CommandOptions, run_standard_command}; +use command::{ + CommandOptions, QUICK_COMMAND_TIMEOUT, run_standard_command, +}; use formatting::{bold, muted}; use komodo_client::entities::{ LatestCommit, komodo_timestamp, update::Log, @@ -26,7 +28,9 @@ pub async fn get_commit_hash_info( ) -> anyhow::Result { let hash = run_standard_command( "git rev-parse --short HEAD", - CommandOptions::default().path(repo_dir), + CommandOptions::default() + .path(repo_dir) + .timeout(QUICK_COMMAND_TIMEOUT), ) .await; let hash = if hash.status.success() { @@ -39,7 +43,9 @@ pub async fn get_commit_hash_info( }; let message = run_standard_command( "git log -1 --pretty=%B", - CommandOptions::default().path(repo_dir), + CommandOptions::default() + .path(repo_dir) + .timeout(QUICK_COMMAND_TIMEOUT), ) .await; let message = if message.status.success() { @@ -83,7 +89,9 @@ pub async fn get_commit_hash_log( pub async fn get_remote_url(path: &Path) -> anyhow::Result { let output = run_standard_command( "git remote show origin", - CommandOptions::default().path(path), + CommandOptions::default() + .path(path) + .timeout(QUICK_COMMAND_TIMEOUT), ) .await; if output.success() { From 6c98b2ccbba14d5e5a202fa2b5c6b326f3334eeb Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 13 Jun 2026 19:10:31 -0400 Subject: [PATCH 011/203] deploy 2.3.0-dev-3 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08a7568fc8..fb6a40b068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index c184540502..d62149b7a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-2" +version = "2.3.0-dev-3" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 47c35c617a41b80cbd59dfa698021bb3025551f4 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 15:53:04 -0400 Subject: [PATCH 012/203] remove periphery custom_timeout option, it doesn't function as timeout as expected --- bin/core/src/helpers/swarm.rs | 24 +----------------------- bin/core/src/monitor/swarm.rs | 15 ++++----------- bin/core/src/periphery/mod.rs | 26 +++++++++----------------- bin/periphery/src/connection/mod.rs | 2 +- 4 files changed, 15 insertions(+), 52 deletions(-) diff --git a/bin/core/src/helpers/swarm.rs b/bin/core/src/helpers/swarm.rs index dccc998ce4..d3317ace97 100644 --- a/bin/core/src/helpers/swarm.rs +++ b/bin/core/src/helpers/swarm.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use anyhow::anyhow; use komodo_client::entities::server::{Server, ServerState}; use mogh_resolver::HasResponse; @@ -13,23 +11,6 @@ pub async fn swarm_request( server_ids: &[String], request: T, ) -> anyhow::Result -where - T: std::fmt::Debug + Clone + Serialize + HasResponse, - T::Response: DeserializeOwned, -{ - swarm_request_custom_timeout( - server_ids, - request, - Duration::from_secs(10), - ) - .await -} - -pub async fn swarm_request_custom_timeout( - server_ids: &[String], - request: T, - timeout: Duration, -) -> anyhow::Result where T: std::fmt::Debug + Clone + Serialize + HasResponse, T::Response: DeserializeOwned, @@ -54,10 +35,7 @@ where else { continue; }; - match periphery - .request_custom_timeout(request.clone(), timeout) - .await - { + match periphery.request(request.clone()).await { Ok(res) => return Ok(res), Err(e) => err = Some(e), } diff --git a/bin/core/src/monitor/swarm.rs b/bin/core/src/monitor/swarm.rs index b76e492d02..8c0673ac69 100644 --- a/bin/core/src/monitor/swarm.rs +++ b/bin/core/src/monitor/swarm.rs @@ -1,7 +1,4 @@ -use std::{ - sync::{Arc, OnceLock}, - time::Duration, -}; +use std::sync::{Arc, OnceLock}; use anyhow::anyhow; use async_timing_util::wait_until_timelength; @@ -20,7 +17,7 @@ use tokio::sync::Mutex; use crate::{ config::monitoring_interval, - helpers::swarm::swarm_request_custom_timeout, + helpers::swarm::swarm_request, monitor::{ RefreshCacheResources, resources::{ @@ -118,12 +115,8 @@ pub async fn refresh_swarm_cache(swarm: &Swarm, force: bool) { } let PollSwarmStatusResponse { inspect, lists } = - match swarm_request_custom_timeout( - &swarm.config.server_ids, - PollSwarmStatus {}, - Duration::from_secs(1), - ) - .await + match swarm_request(&swarm.config.server_ids, PollSwarmStatus {}) + .await { Ok(info) => info, Err(e) => { diff --git a/bin/core/src/periphery/mod.rs b/bin/core/src/periphery/mod.rs index 9a05053047..0dcd5a661d 100644 --- a/bin/core/src/periphery/mod.rs +++ b/bin/core/src/periphery/mod.rs @@ -85,20 +85,6 @@ impl PeripheryClient { &self, request: T, ) -> anyhow::Result - where - T: std::fmt::Debug + Serialize + HasResponse, - T::Response: DeserializeOwned, - { - self - .request_custom_timeout(request, Duration::from_secs(10)) - .await - } - - pub async fn request_custom_timeout( - &self, - request: T, - timeout: Duration, - ) -> anyhow::Result where T: std::fmt::Debug + Serialize + HasResponse, T::Response: DeserializeOwned, @@ -134,11 +120,17 @@ impl PeripheryClient { let res = async { // Poll for the associated response loop { - let message = - response_receiever.recv().with_timeout(timeout).await?; + let message = response_receiever + .recv() + // Periphery request handler sends pings every 4s + // *on this channel specifically* so Core knows + // request is being processed. Hardcoded 11s + // allows for missed 5s ping due to network reconnect. + .with_timeout(Duration::from_secs(10)) + .await?; - // Still in progress, sent to avoid timeout. let Some(message) = message.decode()? else { + // Just a ping from periphery request handler continue; }; diff --git a/bin/periphery/src/connection/mod.rs b/bin/periphery/src/connection/mod.rs index 27960e101c..4c3190c3c8 100644 --- a/bin/periphery/src/connection/mod.rs +++ b/bin/periphery/src/connection/mod.rs @@ -172,7 +172,7 @@ fn handle_request( let ping_in_progress = async { loop { - tokio::time::sleep(Duration::from_secs(5)).await; + tokio::time::sleep(Duration::from_secs(4)).await; if let Err(e) = sender.send_in_progress(channel).await { error!("Failed to ping in progress over channel | {e:?}"); } From ec57c5a0a8ea8fd5b55f692d0f7b8a7ea7a58bc2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 15:58:59 -0400 Subject: [PATCH 013/203] customize command timeouts --- bin/periphery/src/api/compose.rs | 8 ++++---- bin/periphery/src/api/container/mod.rs | 8 +++++--- bin/periphery/src/api/swarm/service.rs | 8 ++++---- bin/periphery/src/docker/compose.rs | 8 ++++---- bin/periphery/src/docker/config.rs | 8 ++++---- bin/periphery/src/docker/image.rs | 8 ++++---- bin/periphery/src/docker/stack.rs | 12 ++++++------ bin/periphery/src/docker/stats.rs | 8 +++----- lib/command/src/options.rs | 6 ------ lib/git/src/lib.rs | 12 +++++------- 10 files changed, 39 insertions(+), 47 deletions(-) diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index 9769e0d673..221d787974 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, fmt::Write, path::PathBuf}; +use std::{borrow::Cow, fmt::Write, path::PathBuf, time::Duration}; use anyhow::{Context, anyhow}; use command::{ - CommandOptions, KomodoCommandMode, QUICK_COMMAND_TIMEOUT, + CommandOptions, KomodoCommandMode, run_komodo_command_with_sanitization, run_komodo_shell_command, run_komodo_standard_command, }; @@ -62,7 +62,7 @@ impl Resolve for GetComposeLog { run_komodo_standard_command( "Get Stack Log", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -97,7 +97,7 @@ impl Resolve for GetComposeLogSearch { run_komodo_shell_command( "Search Stack Log", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index 77857ed2ce..8e0afb416d 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -1,6 +1,8 @@ +use std::time::Duration; + use anyhow::Context; use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_shell_command, + CommandOptions, run_komodo_shell_command, run_komodo_standard_command, }; use futures_util::future::join_all; @@ -65,7 +67,7 @@ impl Resolve for GetContainerLog { run_komodo_standard_command( "Get container log", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -99,7 +101,7 @@ impl Resolve for GetContainerLogSearch { run_komodo_shell_command( "Get container log grep", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 8b534ce27b..d7e20dcd76 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -1,8 +1,8 @@ -use std::fmt::Write; +use std::{fmt::Write, time::Duration}; use anyhow::Context as _; use command::{ - CommandOptions, KomodoCommandMode, QUICK_COMMAND_TIMEOUT, + CommandOptions, KomodoCommandMode, run_komodo_command_with_sanitization, run_komodo_shell_command, run_komodo_standard_command, }; @@ -89,7 +89,7 @@ impl Resolve for GetSwarmServiceLog { run_komodo_standard_command( "Get Swarm Service Log", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -139,7 +139,7 @@ impl Resolve for GetSwarmServiceLogSearch { run_komodo_shell_command( "Search Swarm Service Log", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) diff --git a/bin/periphery/src/docker/compose.rs b/bin/periphery/src/docker/compose.rs index bec4842c23..2860e3cc00 100644 --- a/bin/periphery/src/docker/compose.rs +++ b/bin/periphery/src/docker/compose.rs @@ -1,7 +1,7 @@ +use std::time::Duration; + use anyhow::{Context, anyhow}; -use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, -}; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::stack::ComposeProject; use serde::{Deserialize, Serialize}; @@ -21,7 +21,7 @@ pub async fn list_compose_projects() let res = run_komodo_standard_command( "List Projects", format!("{docker_compose} ls --all --format json"), - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(1)), ) .await; diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 8c498a2e27..042145b3d6 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -1,8 +1,8 @@ -use std::fmt::Write; +use std::{fmt::Write, time::Duration}; use anyhow::{Context, anyhow}; use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_shell_command, + CommandOptions, run_komodo_shell_command, run_komodo_standard_command, }; use data_encoding::BASE64URL; @@ -25,7 +25,7 @@ pub async fn list_swarm_configs( let res = run_komodo_standard_command( "List Swarm Configs", "docker config ls --format json", - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(1)), ) .await; @@ -70,7 +70,7 @@ pub async fn inspect_swarm_config( let res = run_komodo_standard_command( "Inspect Swarm Config", format!(r#"docker config inspect "{config}""#), - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; diff --git a/bin/periphery/src/docker/image.rs b/bin/periphery/src/docker/image.rs index 9f024fce17..fd910b550e 100644 --- a/bin/periphery/src/docker/image.rs +++ b/bin/periphery/src/docker/image.rs @@ -1,8 +1,8 @@ +use std::time::Duration; + use anyhow::Context; use bollard::query_parameters::ListImagesOptions; -use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, -}; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::docker::{ GraphDriverData, HealthConfig, container::ContainerListItem, image::*, @@ -198,7 +198,7 @@ pub async fn get_image_digest_from_registry( let log = run_komodo_standard_command( "", command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; if !log.success { diff --git a/bin/periphery/src/docker/stack.rs b/bin/periphery/src/docker/stack.rs index 9f2a2ca61f..749e53658b 100644 --- a/bin/periphery/src/docker/stack.rs +++ b/bin/periphery/src/docker/stack.rs @@ -1,7 +1,7 @@ +use std::time::Duration; + use anyhow::{Context, anyhow}; -use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_komodo_standard_command, -}; +use command::{CommandOptions, run_komodo_standard_command}; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ docker::{ @@ -74,7 +74,7 @@ pub async fn list_swarm_stacks( let res = run_komodo_standard_command( "List Swarm Stacks", "docker stack ls --format json", - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -133,7 +133,7 @@ pub async fn list_swarm_stack_service_ids( let res = run_komodo_standard_command( "List Swarm Stack Services", format!("docker stack services --format json {stack}"), - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -163,7 +163,7 @@ pub async fn list_swarm_stack_tasks( let res = run_komodo_standard_command( "List Swarm Stack Tasks", format!("docker stack ps --format json --no-trunc {stack}"), - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index f6178201c5..f2f6a5435f 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -1,11 +1,9 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use anyhow::{Context, anyhow}; use async_timing_util::wait_until_timelength; use bollard::{models, query_parameters::StatsOptionsBuilder}; -use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_standard_command, -}; +use command::{CommandOptions, run_standard_command}; use futures_util::StreamExt; use komodo_client::entities::docker::{ container::ContainerStats, @@ -69,7 +67,7 @@ pub async fn get_container_stats( format!("docker stats{container_name} --no-stream {format}"); let output = run_standard_command( &command, - CommandOptions::default().timeout(QUICK_COMMAND_TIMEOUT), + CommandOptions::default().timeout(Duration::from_secs(10)), ) .await; if output.success() { diff --git a/lib/command/src/options.rs b/lib/command/src/options.rs index c8df472c18..5c78cf7be8 100644 --- a/lib/command/src/options.rs +++ b/lib/command/src/options.rs @@ -2,12 +2,6 @@ use std::{path::Path, time::Duration}; use tokio_util::sync::CancellationToken; -/// A sensible timeout for quick read-only / inspection commands -/// (listing, logs, stats, inspect, status, etc). These should resolve in -/// at most a few seconds; the timeout exists purely to avoid hanging -/// indefinitely on a stuck docker daemon, network mount, or git process. -pub const QUICK_COMMAND_TIMEOUT: Duration = Duration::from_secs(60); - /// Controls for how a command is executed. /// /// When either timeout or cancel is set, the child is spawned in its own process group so diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index 1a6f57b074..2a7b49c638 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -1,9 +1,7 @@ -use std::path::Path; +use std::{path::Path, time::Duration}; use anyhow::anyhow; -use command::{ - CommandOptions, QUICK_COMMAND_TIMEOUT, run_standard_command, -}; +use command::{CommandOptions, run_standard_command}; use formatting::{bold, muted}; use komodo_client::entities::{ LatestCommit, komodo_timestamp, update::Log, @@ -30,7 +28,7 @@ pub async fn get_commit_hash_info( "git rev-parse --short HEAD", CommandOptions::default() .path(repo_dir) - .timeout(QUICK_COMMAND_TIMEOUT), + .timeout(Duration::from_secs(1)), ) .await; let hash = if hash.status.success() { @@ -45,7 +43,7 @@ pub async fn get_commit_hash_info( "git log -1 --pretty=%B", CommandOptions::default() .path(repo_dir) - .timeout(QUICK_COMMAND_TIMEOUT), + .timeout(Duration::from_secs(1)), ) .await; let message = if message.status.success() { @@ -91,7 +89,7 @@ pub async fn get_remote_url(path: &Path) -> anyhow::Result { "git remote show origin", CommandOptions::default() .path(path) - .timeout(QUICK_COMMAND_TIMEOUT), + .timeout(Duration::from_secs(1)), ) .await; if output.success() { From 9aef44681c5c6f837c129e559fad09544e856cc2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 16:35:05 -0400 Subject: [PATCH 014/203] Ensure git is installed for clear error message --- lib/git/src/clone.rs | 4 +++- lib/git/src/commit.rs | 20 +++++++++++++++++--- lib/git/src/init.rs | 7 +++++++ lib/git/src/installed.rs | 19 +++++++++++++++++++ lib/git/src/lib.rs | 4 ++++ lib/git/src/pull.rs | 5 +++-- 6 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 lib/git/src/installed.rs diff --git a/lib/git/src/clone.rs b/lib/git/src/clone.rs index c9b2d6dca7..1c3bb08057 100644 --- a/lib/git/src/clone.rs +++ b/lib/git/src/clone.rs @@ -8,7 +8,7 @@ use komodo_client::entities::{ update::Log, }; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Will delete the existing repo folder, /// clone the repo, get the latest hash / message, @@ -24,6 +24,8 @@ pub async fn clone( where T: Into + std::fmt::Debug, { + check_installed().await?; + let args: RepoExecutionArgs = clone_args.into(); let repo_url = args.remote_url(access_token.as_deref())?; diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs index 87f45b9863..060e56b365 100644 --- a/lib/git/src/commit.rs +++ b/lib/git/src/commit.rs @@ -9,7 +9,7 @@ use komodo_client::entities::{ RepoExecutionResponse, all_logs_success, update::Log, }; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Write file, add, commit, force push. /// Repo must be cloned. @@ -93,6 +93,13 @@ pub async fn commit_file_inner( file: &Path, branch: &str, ) { + if let Err(e) = check_installed().await { + res + .logs + .push(Log::error("Commit", format_serror(&e.into()))); + return; + }; + ensure_global_git_config_set().await; let add_log = run_komodo_standard_command( @@ -157,8 +164,6 @@ pub async fn commit_all( message: &str, branch: &str, ) -> RepoExecutionResponse { - ensure_global_git_config_set().await; - let mut res = RepoExecutionResponse { path: repo_dir.to_path_buf(), logs: Vec::new(), @@ -166,6 +171,15 @@ pub async fn commit_all( commit_message: None, }; + if let Err(e) = check_installed().await { + res + .logs + .push(Log::error("Commit", format_serror(&e.into()))); + return res; + }; + + ensure_global_git_config_set().await; + let add_log = run_komodo_standard_command( "Add Files", "git add -A", diff --git a/lib/git/src/init.rs b/lib/git/src/init.rs index 8ecd4daeb7..f8a4d6273c 100644 --- a/lib/git/src/init.rs +++ b/lib/git/src/init.rs @@ -6,12 +6,19 @@ use komodo_client::entities::{ RepoExecutionArgs, all_logs_success, update::Log, }; +use crate::check_installed; + pub async fn init_folder_as_repo( folder_path: &Path, args: &RepoExecutionArgs, access_token: Option<&str>, logs: &mut Vec, ) { + if let Err(e) = check_installed().await { + logs.push(Log::error("Git Init", format_serror(&e.into()))); + return; + }; + // Initialize the folder as a git repo let init_repo = run_komodo_standard_command( "Git Init", diff --git a/lib/git/src/installed.rs b/lib/git/src/installed.rs new file mode 100644 index 0000000000..0c7d78422c --- /dev/null +++ b/lib/git/src/installed.rs @@ -0,0 +1,19 @@ +use std::time::Duration; + +use anyhow::anyhow; +use command::{CommandOptions, run_standard_command}; + +/// Returns error if git not installed +pub async fn check_installed() -> anyhow::Result<()> { + if run_standard_command( + "which git", + CommandOptions::default().timeout(Duration::from_secs(1)), + ) + .await + .success() + { + Ok(()) + } else { + Err(anyhow!("Failed: 'git' is not installed or available on $PATH")) + } +} diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index 2a7b49c638..fbf19398cb 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -10,6 +10,7 @@ use komodo_client::entities::{ mod clone; mod commit; mod init; +mod installed; mod pull; mod pull_or_clone; @@ -17,6 +18,7 @@ pub use crate::{ clone::clone, commit::{commit_all, commit_file, write_commit_file}, init::init_folder_as_repo, + installed::check_installed, pull::pull, pull_or_clone::pull_or_clone, }; @@ -24,6 +26,7 @@ pub use crate::{ pub async fn get_commit_hash_info( repo_dir: &Path, ) -> anyhow::Result { + check_installed().await?; let hash = run_standard_command( "git rev-parse --short HEAD", CommandOptions::default() @@ -85,6 +88,7 @@ pub async fn get_commit_hash_log( /// Gets the remote url, with `.git` stripped from the end. pub async fn get_remote_url(path: &Path) -> anyhow::Result { + check_installed().await?; let output = run_standard_command( "git remote show origin", CommandOptions::default() diff --git a/lib/git/src/pull.rs b/lib/git/src/pull.rs index ad33e780e5..18ac693393 100644 --- a/lib/git/src/pull.rs +++ b/lib/git/src/pull.rs @@ -11,7 +11,7 @@ use komodo_client::entities::{ }; use mogh_cache::TimeoutCache; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Wait this long after a pull to allow another pull through const PULL_TIMEOUT: i64 = 5_000; @@ -27,7 +27,6 @@ fn pull_cache() /// This will pull in a way that handles edge cases /// from possible state of the repo. For example, the user /// can change branch after clone, or even the remote. -#[allow(clippy::too_many_arguments)] pub async fn pull( clone_args: T, root_repo_dir: &Path, @@ -36,6 +35,8 @@ pub async fn pull( where T: Into + std::fmt::Debug, { + check_installed().await?; + let args: RepoExecutionArgs = clone_args.into(); let repo_url = args.remote_url(access_token.as_deref())?; From 4f866ceac31219444e2dd246f2720b675c6d6b65 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 16:38:36 -0400 Subject: [PATCH 015/203] deploy 2.3.0-dev-4 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb6a40b068..d85077abe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index d62149b7a2..cb448f1fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-3" +version = "2.3.0-dev-4" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 052a4fd74be0f3e10b6b60874bb0259e95441103 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 16:40:45 -0400 Subject: [PATCH 016/203] deploy 2.3.0-dev-5 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d85077abe5..868a033fb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index cb448f1fa5..e6c8dca993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-4" +version = "2.3.0-dev-5" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 913b6d543626e3f6554a6ef173bd0d07f0004625 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 14 Jun 2026 16:41:54 -0400 Subject: [PATCH 017/203] deploy 2.3.0-dev-6 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 868a033fb4..4e517820c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index e6c8dca993..c322992e4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-5" +version = "2.3.0-dev-6" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 8b1979b9d3bb7c8c70d108d8e2946aec0148f17d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 15 Jun 2026 15:12:43 -0400 Subject: [PATCH 018/203] swarm completed in roadmap --- roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roadmap.md b/roadmap.md index 2033190740..5ca08b458f 100644 --- a/roadmap.md +++ b/roadmap.md @@ -13,7 +13,7 @@ If you have an idea for Komodo, feel free to open an issue beginning with the `[ - **v1.16**: "Action" resource: Run requests on the Komodo API using snippets of typescript. ✅ - **v1.17**: Procedure Schedules: Run procedures / Actions at scheduled times, like CRON job. Connect to host terminals and exec into containers ✅ - **v1.18**: Upgrade granular role based access control system ✅ -- **v2.0**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". +- **v2.0**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". ✅ - **Undecided**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" **Note. The specific versions associated with these features are not final.** \ No newline at end of file From f99254e685b9ffb1994ab9da0ece05660e0719df Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 15 Jun 2026 15:12:59 -0400 Subject: [PATCH 019/203] unnecessary line --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index ad92ff14bd..3f962daba2 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ A tool to build and deploy software across many servers. The Komodo dragon is the largest living member of the [*Monitor* family of lizards](https://en.wikipedia.org/wiki/Monitor_lizard). -There is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. No "business edition" here. +There is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. ## Disclaimer From 9c73b316f85a7be67685b096480af45fa9001d1a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 15 Jun 2026 15:17:59 -0400 Subject: [PATCH 020/203] Komodo v2 march 2026 --- docsite/docs/releases/v2.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docsite/docs/releases/v2.0.0.md b/docsite/docs/releases/v2.0.0.md index 0cf433f874..765492dfbd 100644 --- a/docsite/docs/releases/v2.0.0.md +++ b/docsite/docs/releases/v2.0.0.md @@ -1,4 +1,4 @@ -# v2.0.0 +# v2.0.0 – March 2026 Komodo v2 is a major release with significant architectural changes and new features. From b311c13bee0d4d76280dd335a567b14efc6c34fc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 16 Jun 2026 15:33:54 -0700 Subject: [PATCH 021/203] remove debub container ports logs --- ui/src/components/docker/container-ports.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ui/src/components/docker/container-ports.tsx b/ui/src/components/docker/container-ports.tsx index fd4a7a8753..a36e6f2c09 100644 --- a/ui/src/components/docker/container-ports.tsx +++ b/ui/src/components/docker/container-ports.tsx @@ -31,19 +31,11 @@ export default function ContainerPorts({ }: ContainerPortsProps) { const portsMap = useContainerPortsMap(ports); - if (Object.keys(portsMap).length > 0) { - console.log("mapped", portsMap); - } - const sortedNumericPorts = Object.keys(portsMap) .map(Number) .filter((port) => !Number.isNaN(port)) .sort((a, b) => a - b); - if (Object.keys(portsMap).length > 0) { - console.log("sorted", sortedNumericPorts); - } - type Group = { start: number; end: number; ports: Types.Port[] }; const groupedPorts = sortedNumericPorts.reduce((acc, port) => { @@ -62,8 +54,6 @@ export default function ContainerPorts({ return null; } - console.log("grouped", groupedPorts); - return ( {groupedPorts.map((group) => ( From 36c76835f72f3d13b175349c2ba5d6c7cd27546f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 16 Jun 2026 15:45:19 -0700 Subject: [PATCH 022/203] remove log view console log --- ui/src/components/log-viewer.tsx | 2 -- ui/src/components/terminal/index.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/src/components/log-viewer.tsx b/ui/src/components/log-viewer.tsx index 9212c3e886..1a4d1a39fd 100644 --- a/ui/src/components/log-viewer.tsx +++ b/ui/src/components/log-viewer.tsx @@ -71,8 +71,6 @@ export default function LogViewer({ } }; - console.log(lines); - return ( { - // console.log("logged in terminal"); + // console.log("Logged into terminal"); }, on_open: resize, on_message: (e: MessageEvent) => { From 0b44d485143370ec25243ee54f80fd92e3dfcad3 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 16 Jun 2026 16:00:50 -0700 Subject: [PATCH 023/203] Fix deployment tabs disappear --- ui/src/resources/deployment/tabs.tsx | 4 ++-- ui/src/resources/deployment/tasks.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/src/resources/deployment/tabs.tsx b/ui/src/resources/deployment/tabs.tsx index ad136316a0..ec1d04ea3d 100644 --- a/ui/src/resources/deployment/tabs.tsx +++ b/ui/src/resources/deployment/tabs.tsx @@ -19,7 +19,7 @@ type DeploymentTabsView = "Config" | "Tasks" | "Log" | "Inspect" | "Terminals"; export default function DeploymentTabs({ id }: { id: string }) { const [_view, setView] = useLocalStorage({ - key: "deployment-tabs-v1", + key: `deployment-${id}-tab-v2`, defaultValue: "Config", }); const info = useDeployment(id)?.info; @@ -46,7 +46,7 @@ export default function DeploymentTabs({ id }: { id: string }) { const view = (logsDisabled && _view === "Log") || - (downOrUnknown && _view === "Tasks") || + ((downOrUnknown || !info?.swarm_id) && _view === "Tasks") || (inspectDisabled && _view === "Inspect") || (terminalDisabled && _view === "Terminals") ? "Config" diff --git a/ui/src/resources/deployment/tasks.tsx b/ui/src/resources/deployment/tasks.tsx index dc076b6faf..5dd7743c7c 100644 --- a/ui/src/resources/deployment/tasks.tsx +++ b/ui/src/resources/deployment/tasks.tsx @@ -4,6 +4,8 @@ import SwarmServiceTasksSection, { SwarmServiceTasksSectionProps, } from "@/pages/swarm/service/tasks"; import { useState } from "react"; +import { Section } from "mogh_ui"; +import { Text } from "@mantine/core"; export interface DeploymentTasksSectionProps extends Omit< Omit, @@ -25,7 +27,13 @@ export default function DeploymentTasksSection({ ).data?.find((service) => service.Name === deployment?.name); const _search = useState(""); - if (!swarmId || !service) return; + if (!swarmId || !service) { + return ( +
+ Did not find {!swarmId ? "Swarm" : "Swarm Service"} +
+ ); + } return ( Date: Tue, 16 Jun 2026 16:01:04 -0700 Subject: [PATCH 024/203] align other resource store tab per resource id --- ui/src/resources/repo/tabs.tsx | 7 ++----- ui/src/resources/sync/hooks.ts | 12 +++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/ui/src/resources/repo/tabs.tsx b/ui/src/resources/repo/tabs.tsx index ff31e3fe45..44c76e919c 100644 --- a/ui/src/resources/repo/tabs.tsx +++ b/ui/src/resources/repo/tabs.tsx @@ -1,10 +1,7 @@ import { useLocalStorage } from "@mantine/hooks"; import { useRepo } from "."; import { useMemo } from "react"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { ICONS } from "@/lib/icons"; import { repoStateIntention } from "@/lib/color"; import { Tabs } from "@mantine/core"; @@ -16,7 +13,7 @@ type RepoTabsView = "Config" | "Resources"; export default function RepoTabs({ id }: { id: string }) { const [view, setView] = useLocalStorage({ - key: "repo-tabs-v1", + key: `repo-${id}-tab-v2`, defaultValue: "Config", }); const info = useRepo(id)?.info; diff --git a/ui/src/resources/sync/hooks.ts b/ui/src/resources/sync/hooks.ts index e5d22acdec..34e7a29f08 100644 --- a/ui/src/resources/sync/hooks.ts +++ b/ui/src/resources/sync/hooks.ts @@ -1,16 +1,14 @@ -import { atomWithStorage } from "@/lib/hooks"; import { resourceSyncNoChanges } from "@/lib/utils"; -import { useAtom } from "jotai"; +import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit"; -const syncTabsViewAtom = atomWithStorage( - "sync-tabs-v5", - "Config", -); export function useResourceSyncTabsView(sync: Types.ResourceSync | undefined) { - const [_view, setView] = useAtom(syncTabsViewAtom); + const [_view, setView] = useLocalStorage({ + key: `sync-${sync?._id?.$oid}-tab-v1`, + defaultValue: "Config", + }); const hideInfo = sync?.config?.files_on_host ? false From 8e9d48fa756033a5d1f19e13766090f67aa86cb9 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 16 Jun 2026 16:05:27 -0700 Subject: [PATCH 025/203] disable terminal tab when attached to swarm --- ui/src/resources/deployment/tabs.tsx | 4 ++-- ui/src/resources/stack/tabs.tsx | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/src/resources/deployment/tabs.tsx b/ui/src/resources/deployment/tabs.tsx index ec1d04ea3d..37d5e0bdc8 100644 --- a/ui/src/resources/deployment/tabs.tsx +++ b/ui/src/resources/deployment/tabs.tsx @@ -42,7 +42,8 @@ export default function DeploymentTabs({ id }: { id: string }) { const terminalDisabled = !specificTerminal || containerTerminalsDisabled || - state !== Types.DeploymentState.Running; + state !== Types.DeploymentState.Running || + !!info?.swarm_id; const view = (logsDisabled && _view === "Log") || @@ -78,7 +79,6 @@ export default function DeploymentTabs({ id }: { id: string }) { value: "Terminals", icon: ICONS.Terminal, disabled: terminalDisabled, - hidden: !!info?.swarm_id, }, ], [logsDisabled, inspectDisabled, terminalDisabled, info?.swarm_id], diff --git a/ui/src/resources/stack/tabs.tsx b/ui/src/resources/stack/tabs.tsx index e5ce95f683..340420358b 100644 --- a/ui/src/resources/stack/tabs.tsx +++ b/ui/src/resources/stack/tabs.tsx @@ -3,10 +3,7 @@ import { useStack } from "."; import { usePermissions, useRead } from "@/lib/hooks"; import { Types } from "komodo_client"; import { useMemo } from "react"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { Tabs } from "@mantine/core"; import { ICONS } from "@/lib/icons"; import { stackStateIntention } from "@/lib/color"; @@ -45,6 +42,8 @@ export default function StackTabs({ id }: { id: string }) { const terminalDisabled = !specificTerminal || containerTerminalsDisabled || + // Not attached to swarm + !!info?.swarm_id || // All services are not running services?.every( (service) => From 07eb7c60d4194fd0f39dfc6b4339c89ce8caf079 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 08:56:07 -0700 Subject: [PATCH 026/203] List All Docker Containers supports wildcard container name matching --- bin/core/src/api/read/server.rs | 12 ++++++++++-- client/core/rs/src/api/read/docker.rs | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 24090d2d9b..8f8a2dfef9 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -46,6 +46,7 @@ use periphery_client::api::{ }; use reqwest::StatusCode; use tokio::sync::Mutex; +use wildcard::Wildcard; use crate::{ helpers::{periphery_client, query::get_all_tags}, @@ -404,6 +405,11 @@ impl Resolve for ListAllDockerContainers { ) .await?; + let wildcards = self + .containers + .into_iter() + .flat_map(|c| Wildcard::from_owned(c.into_bytes())) + .collect::>(); let mut containers = Vec::::new(); for server in servers { @@ -417,8 +423,10 @@ impl Resolve for ListAllDockerContainers { .containers .iter() .filter(|container| { - self.containers.is_empty() - || self.containers.contains(&container.name) + wildcards.is_empty() + || wildcards + .iter() + .any(|wc| wc.is_match(container.name.as_bytes())) }) .cloned(); containers.extend(more); diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 38cb76c979..5546311d02 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -85,6 +85,7 @@ pub struct ListAllDockerContainers { pub servers: Vec, /// Filter by container name. + /// Supports wildcard matching syntax. #[serde(default)] pub containers: Vec, } From 330d077cf98f29ab0efb239a1011ab30563638fc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 09:56:58 -0700 Subject: [PATCH 027/203] improve container pagination situation --- bin/cli/src/command/container.rs | 27 +++------ bin/cli/src/command/terminal.rs | 4 ++ bin/core/src/api/read/server.rs | 36 +++++++---- client/core/rs/src/api/read/docker.rs | 17 ++++++ .../src/entities/config/cli/args/container.rs | 3 + client/core/ts/src/types.ts | 18 +++++- ui/public/client/types.d.ts | 18 +++++- ui/src/app/topbar/omni-search/hooks.tsx | 13 +--- ui/src/pages/containers.tsx | 59 ++++++++++++------- 9 files changed, 131 insertions(+), 64 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index df571d7b06..a3826bb826 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -49,6 +49,7 @@ async fn list_containers( networks, servers, format, + page, command: _, }: &Container, ) -> anyhow::Result<()> { @@ -62,7 +63,9 @@ async fn list_containers( .collect::>())), client.read(ListAllDockerContainers { servers: Default::default(), - containers: Default::default(), + containers: names.clone(), + limit: 300, + page: *page, }), )?; @@ -78,7 +81,6 @@ async fn list_containers( (Some(server.name.as_str()), c) }); - let names = parse_wildcards(names); let servers = parse_wildcards(servers); let images = parse_wildcards(images); let networks = parse_wildcards(networks); @@ -105,7 +107,6 @@ async fn list_containers( ); state_check && network_check - && matches_wildcards(&names, &[c.name.as_str()]) && matches_wildcards( &servers, &server_name @@ -146,8 +147,10 @@ pub async fn inspect_container( .map(|s| (s.id.clone(), s)) .collect::>())), client.read(ListAllDockerContainers { - servers: Default::default(), - containers: Default::default() + servers: inspect.servers.clone(), + containers: vec![inspect.container.clone()], + limit: 300, + page: 0, }), )?; @@ -162,22 +165,8 @@ pub async fn inspect_container( c.server_id = Some(server.name.clone()); }); - let names = [inspect.container.to_string()]; - let names = parse_wildcards(&names); - let servers = parse_wildcards(&inspect.servers); - let mut containers = containers .into_iter() - .filter(|c| { - matches_wildcards(&names, &[c.name.as_str()]) - && matches_wildcards( - &servers, - &c.server_id - .as_deref() - .map(|i| vec![i]) - .unwrap_or_default(), - ) - }) .map(|c| async move { client .read(InspectDockerContainer { diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index f286e4ca39..b68316441b 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -118,6 +118,8 @@ async fn get_server( server: Option, container: &str, ) -> anyhow::Result { + // Don't need to find server for terminal, + // user provided it, early return. if let Some(server) = server { return Ok(server); } @@ -128,6 +130,8 @@ async fn get_server( .read(ListAllDockerContainers { servers: Default::default(), containers: vec![container.to_string()], + limit: 300, + page: 0, }) .await?; diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 8f8a2dfef9..a6a690821a 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -405,12 +405,17 @@ impl Resolve for ListAllDockerContainers { ) .await?; - let wildcards = self + let terms = self .containers - .into_iter() - .flat_map(|c| Wildcard::from_owned(c.into_bytes())) + .iter() + .flat_map(|term| { + anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) + }) .collect::>(); + let mut containers = Vec::::new(); + let mut skipped = 0; + let limit_usize = self.limit as usize; for server in servers { let cache = server_status_cache() @@ -423,13 +428,24 @@ impl Resolve for ListAllDockerContainers { .containers .iter() .filter(|container| { - wildcards.is_empty() - || wildcards - .iter() - .any(|wc| wc.is_match(container.name.as_bytes())) - }) - .cloned(); - containers.extend(more); + terms.is_empty() + // Match when all terms contained within a name. + || terms.iter().all(|(term, _)| container.name.contains(*term)) + // Match when any wildcard term directly matches. + || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes())) + }); + for container in more { + if skipped < self.limit * self.page { + // Eg. page 1 skips until after 300 containers, page 2 after 600. + skipped += 1; + } else { + // push and maybe early return + containers.push(container.clone()); + if containers.len() >= limit_usize { + return Ok(containers); + } + } + } } Ok(containers) diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 5546311d02..423b1e3861 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -88,6 +88,23 @@ pub struct ListAllDockerContainers { /// Supports wildcard matching syntax. #[serde(default)] pub containers: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of containers per-page. + /// `limit: 300` is default. + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_limit")] + pub limit: U64, +} + +fn default_limit() -> u64 { + 300 } #[typeshare] diff --git a/client/core/rs/src/entities/config/cli/args/container.rs b/client/core/rs/src/entities/config/cli/args/container.rs index 6b0ff02f62..e4b3dd1a11 100644 --- a/client/core/rs/src/entities/config/cli/args/container.rs +++ b/client/core/rs/src/entities/config/cli/args/container.rs @@ -37,6 +37,9 @@ pub struct Container { /// Specify the format of the output. #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)] pub format: super::CliFormat, + /// Specify page for container list + #[arg(long, short = 'p', default_value_t = 300)] + pub page: u64, } #[derive(Debug, Clone, clap::Subcommand)] diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index cb7f0c110f..6a3ced2690 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -8577,8 +8577,24 @@ export interface ListAlertsResponse { export interface ListAllDockerContainers { /** Filter by server id or name. */ servers?: string[]; - /** Filter by container name. */ + /** + * Filter by container name. + * Supports wildcard matching syntax. + */ containers?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of containers per-page. + * `limit: 300` is default. + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 244a4c6fc4..de42756eba 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -8167,8 +8167,24 @@ export interface ListAlertsResponse { export interface ListAllDockerContainers { /** Filter by server id or name. */ servers?: string[]; - /** Filter by container name. */ + /** + * Filter by container name. + * Supports wildcard matching syntax. + */ containers?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of containers per-page. + * `limit: 300` is default. + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * Gets list of api keys for the calling user. diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 4d28016736..357b7e4e9a 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -47,20 +47,11 @@ export function useOmniSearch(): { [search], ); - const _containers = useRead( + const containers = useRead( "ListAllDockerContainers", - {}, + { containers: searchTerms, limit: 300, page: 0 }, { refetchInterval: 15_000 }, ).data; - const containers = useMemo(() => { - return _containers?.filter((c) => { - if (searchTerms.length === 0) return true; - const lower = c.name.toLowerCase(); - return searchTerms.every( - (term) => lower.includes(term) || "containers".includes(term), - ); - }); - }, [_containers, searchTerms]); const _terminals = useRead( "ListTerminals", diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 3f61d0c3d2..e6044b161c 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -3,11 +3,10 @@ import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; import { useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; -import { filterBySplit } from "mogh_ui"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; -import { Group, MultiSelect, Stack } from "@mantine/core"; +import { Group, MultiSelect, Pagination, Stack } from "@mantine/core"; import { useCallback, useMemo, useState } from "react"; import { DividedChildren } from "mogh_ui"; import ResourceLink from "@/resources/link"; @@ -15,17 +14,18 @@ import { SearchInput } from "mogh_ui"; export default function Containers() { const [search, setSearch] = useState(""); + const [page, setPage] = useState(0); const [selectedServers, setSelectedServers] = useState([]); const debouncedSearch = useDebounce(search, 300); + const containersQuery = useMemo( + () => debouncedSearch.split(" ").filter((term) => term), + [debouncedSearch], + ); const servers = useRead("ListServers", {}).data; - const serverOptions = useMemo( - () => - servers?.map((server) => ({ - label: server.name, - value: server.id, - })) || [], + const serverNames = useMemo( + () => servers?.map((server) => server.name) || [], [servers], ); @@ -34,17 +34,17 @@ export default function Containers() { [servers], ); - const _containers = useRead("ListAllDockerContainers", {}).data?.filter( + const containers = useRead("ListAllDockerContainers", { + containers: containersQuery, + servers: serverNames, + page, + limit: 300, + }).data?.filter( (container) => !selectedServers.length || (container.server_id && selectedServers.includes(container.server_id)), ); - const containers = useMemo( - () => filterBySplit(_containers, debouncedSearch, (item) => item.name), - [_containers, debouncedSearch], - ); - return ( - + + + {/* PAGINATION */} + setPage(page - 1)} + > + + + + + + + + From c71a400804d27fcee961b925aed3a066d79c46c8 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 09:57:33 -0700 Subject: [PATCH 028/203] deploy 2.3.0-dev-7 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e517820c6..8ff0aecf24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index c322992e4d..5558481f89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-6" +version = "2.3.0-dev-7" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 0e931c36306298679052e605744ec836baf9723c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 10:21:21 -0700 Subject: [PATCH 029/203] fix container data table search slowness --- ui/src/pages/containers.tsx | 327 ++++++++++++++++++------------------ 1 file changed, 165 insertions(+), 162 deletions(-) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index e6044b161c..d4ecf3ef16 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -17,7 +17,7 @@ export default function Containers() { const [page, setPage] = useState(0); const [selectedServers, setSelectedServers] = useState([]); - const debouncedSearch = useDebounce(search, 300); + const debouncedSearch = useDebounce(search, 700); const containersQuery = useMemo( () => debouncedSearch.split(" ").filter((term) => term), [debouncedSearch], @@ -34,16 +34,168 @@ export default function Containers() { [servers], ); - const containers = useRead("ListAllDockerContainers", { - containers: containersQuery, - servers: serverNames, - page, - limit: 300, - }).data?.filter( - (container) => - !selectedServers.length || - (container.server_id && selectedServers.includes(container.server_id)), - ); + const containers = + useRead("ListAllDockerContainers", { + containers: containersQuery, + servers: serverNames, + page, + limit: 300, + }).data ?? []; + + const Table = useMemo(() => { + return ( + ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "server_id", + size: 200, + sortingFn: (a, b) => { + const sa = serverName(a.original.server_id!); + const sb = serverName(b.original.server_id!); + + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + if (sa > sb) return 1; + else if (sa < sb) return -1; + else return 0; + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "state", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + + ); + }, + }, + { + accessorKey: "image", + size: 300, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "networks.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => + (row.original.networks?.length ?? 0) > 0 ? ( + + {row.original.networks?.map((network) => ( + + ))} + + ) : ( + row.original.network_mode && ( + + ) + ), + }, + { + 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 }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "volumes.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.volumes?.map((volume) => ( + + ))} + + ), + }, + ]} + /> + ); + }, [serverName, containers]); return ( {/* PAGINATION */} = 300 ? page + 2 : page + 1} value={page + 1} onChange={(page) => setPage(page - 1)} > @@ -79,156 +231,7 @@ export default function Containers() { - ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "server_id", - size: 200, - sortingFn: (a, b) => { - const sa = serverName(a.original.server_id!); - const sb = serverName(b.original.server_id!); - - if (!sa && !sb) return 0; - if (!sa) return -1; - if (!sb) return 1; - - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "state", - size: 160, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const state = row.original?.state; - return ( - - ); - }, - }, - { - accessorKey: "image", - size: 300, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "networks.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => - (row.original.networks?.length ?? 0) > 0 ? ( - - {row.original.networks?.map((network) => ( - - ))} - - ) : ( - row.original.network_mode && ( - - ) - ), - }, - { - 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 }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "volumes.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - {row.original.volumes?.map((volume) => ( - - ))} - - ), - }, - ]} - /> + {Table} ); From f6d764c476c85f32977b626b38f066b1a1e6985c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 10:21:52 -0700 Subject: [PATCH 030/203] deploy 2.3.0-dev-8 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ff0aecf24..a9bdbb7950 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5558481f89..17afdad7cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-7" +version = "2.3.0-dev-8" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From bbaf6578042c106a065f144b07943abaea600c79 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 13:26:45 -0700 Subject: [PATCH 031/203] ListAllStackServices, add stack services to omni search, All containers filter by tags (based on server) --- bin/cli/src/command/container.rs | 2 + bin/cli/src/command/terminal.rs | 1 + bin/core/src/api/read/docker.rs | 532 ++++++++++++++++++++++ bin/core/src/api/read/mod.rs | 2 + bin/core/src/api/read/server.rs | 560 +----------------------- bin/core/src/api/read/stack.rs | 64 ++- bin/core/src/monitor/resources.rs | 2 + client/core/rs/src/api/read/docker.rs | 4 + client/core/rs/src/api/read/openapi.rs | 1 + client/core/rs/src/api/read/stack.rs | 57 +++ client/core/rs/src/entities/stack.rs | 2 + client/core/ts/src/responses.ts | 1 + client/core/ts/src/types.ts | 62 ++- ui/public/client/responses.d.ts | 1 + ui/public/client/types.d.ts | 60 ++- ui/src/app/topbar/omni-search/hooks.tsx | 56 ++- ui/src/lib/hooks.ts | 15 +- ui/src/pages/containers.tsx | 14 +- ui/src/pages/resources.tsx | 5 +- ui/src/resources/server/table/stats.tsx | 4 +- ui/src/resources/server/version.tsx | 4 +- 21 files changed, 846 insertions(+), 603 deletions(-) create mode 100644 bin/core/src/api/read/docker.rs diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index a3826bb826..97ac1e493b 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -63,6 +63,7 @@ async fn list_containers( .collect::>())), client.read(ListAllDockerContainers { servers: Default::default(), + tags: Default::default(), containers: names.clone(), limit: 300, page: *page, @@ -148,6 +149,7 @@ pub async fn inspect_container( .collect::>())), client.read(ListAllDockerContainers { servers: inspect.servers.clone(), + tags: Default::default(), containers: vec![inspect.container.clone()], limit: 300, page: 0, diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index b68316441b..68c5e0d127 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -130,6 +130,7 @@ async fn get_server( .read(ListAllDockerContainers { servers: Default::default(), containers: vec![container.to_string()], + tags: Default::default(), limit: 300, page: 0, }) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs new file mode 100644 index 0000000000..c849cdef25 --- /dev/null +++ b/bin/core/src/api/read/docker.rs @@ -0,0 +1,532 @@ +use std::cmp; + +use anyhow::{Context as _, anyhow}; +use database::bson::doc; +use komodo_client::{ + api::read::*, + entities::{ + ResourceTarget, + deployment::Deployment, + docker::{ + container::{ + Container, ContainerListItem, ContainerStateStatusEnum, + }, + image::{Image, ImageHistoryResponseItem}, + network::Network, + volume::Volume, + }, + permission::PermissionLevel, + server::{Server, ServerQuery, ServerState}, + stack::{Stack, StackServiceNames}, + update::Log, + }, +}; +use mogh_resolver::Resolve; +use periphery_client::api::{ + self as periphery, + container::InspectContainer, + docker::{ + ImageHistory, InspectImage, InspectNetwork, InspectVolume, + }, +}; +use wildcard::Wildcard; + +use crate::{ + api::read::ReadArgs, + helpers::periphery_client, + permission::{get_check_permissions, list_resources_for_user}, + resource, + stack::compose_container_match_regex, + state::server_status_cache, +}; + +impl Resolve for GetDockerContainersSummary { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let servers = resource::list_full_for_user::( + Default::default(), + user, + PermissionLevel::Read.into(), + &[], + ) + .await + .context("failed to get servers from db")?; + + let mut res = GetDockerContainersSummaryResponse::default(); + + for server in servers { + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + + if let Some(docker) = &cache.docker { + for container in &docker.containers { + res.total += 1; + match container.state { + ContainerStateStatusEnum::Created + | ContainerStateStatusEnum::Paused + | ContainerStateStatusEnum::Exited => res.stopped += 1, + ContainerStateStatusEnum::Running => res.running += 1, + ContainerStateStatusEnum::Empty => res.unknown += 1, + _ => res.unhealthy += 1, + } + } + } + } + + Ok(res) + } +} + +impl Resolve for ListAllDockerContainers { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let servers = resource::list_for_user::( + ServerQuery::builder() + .names(self.servers.clone()) + .tags(self.tags) + .build(), + user, + PermissionLevel::Read.into(), + &[], + ) + .await?; + + let terms = self + .containers + .iter() + .flat_map(|term| { + anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) + }) + .collect::>(); + + let mut containers = Vec::::new(); + let mut skipped = 0; + let limit_usize = self.limit as usize; + + for server in servers { + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + let Some(docker) = &cache.docker else { + continue; + }; + let more = docker + .containers + .iter() + .filter(|container| { + terms.is_empty() + // Match when all terms contained within a name. + || terms.iter().all(|(term, _)| container.name.contains(*term)) + // Match when any wildcard term directly matches. + || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes())) + }); + for container in more { + if skipped < self.limit * self.page { + // Eg. page 1 skips until after 300 containers, page 2 after 600. + skipped += 1; + } else { + // push and maybe early return + containers.push(container.clone()); + if containers.len() >= limit_usize { + return Ok(containers); + } + } + } + } + + Ok(containers) + } +} + +impl Resolve for ListDockerContainers { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.containers.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectDockerContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.inspect(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect container: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(InspectContainer { + name: self.container, + }) + .await?; + Ok(res) + } +} + +impl Resolve for GetResourceMatchingContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + // first check deployments + if let Ok(deployment) = + resource::get::(&self.container).await + { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Deployment(deployment.id).into(), + }); + } + + // then check stacks + let stacks = list_resources_for_user::( + doc! { "config.server_id": &server.id }, + user, + PermissionLevel::Read.into(), + ) + .await?; + + // check matching stack + for stack in stacks { + for StackServiceNames { + service_name, + container_name, + .. + } in stack + .info + .deployed_services + .unwrap_or(stack.info.latest_services) + { + let is_match = match compose_container_match_regex(&container_name) + .with_context(|| format!("failed to construct container name matching regex for service {service_name}")) + { + Ok(regex) => regex, + Err(e) => { + warn!("{e:#}"); + continue; + } + }.is_match(&self.container); + + if is_match { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Stack(stack.id).into(), + }); + } + } + } + + Ok(GetResourceMatchingContainerResponse { resource: None }) + } +} + +const MAX_LOG_LENGTH: u64 = 5000; + +impl Resolve for GetContainerLog { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let GetContainerLog { + server, + container, + tail, + timestamps, + } = self; + let server = get_check_permissions::( + &server, + user, + PermissionLevel::Read.logs(), + ) + .await?; + let res = periphery_client(&server) + .await? + .request(periphery::container::GetContainerLog { + name: container, + tail: cmp::min(tail, MAX_LOG_LENGTH), + timestamps, + }) + .await + .context("failed at call to periphery")?; + Ok(res) + } +} + +impl Resolve for SearchContainerLog { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let SearchContainerLog { + server, + container, + terms, + combinator, + invert, + timestamps, + } = self; + let server = get_check_permissions::( + &server, + user, + PermissionLevel::Read.logs(), + ) + .await?; + let res = periphery_client(&server) + .await? + .request(periphery::container::GetContainerLogSearch { + name: container, + terms, + combinator, + invert, + timestamps, + }) + .await + .context("failed at call to periphery")?; + Ok(res) + } +} + +impl Resolve for ListComposeProjects { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.projects.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for ListDockerNetworks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.networks.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectDockerNetwork { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect network: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(InspectNetwork { name: self.network }) + .await?; + Ok(res) + } +} + +impl Resolve for ListDockerImages { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.images.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectDockerImage { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!("Cannot inspect image: server is {:?}", cache.state) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(InspectImage { name: self.image }) + .await?; + Ok(res) + } +} + +impl Resolve for ListDockerImageHistory { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result> { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot get image history: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(ImageHistory { name: self.image }) + .await?; + Ok(res) + } +} + +impl Resolve for ListDockerVolumes { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.volumes.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectDockerVolume { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!("Cannot inspect volume: server is {:?}", cache.state) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(InspectVolume { name: self.volume }) + .await?; + Ok(res) + } +} diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 9ac98a06ab..bad446baec 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -44,6 +44,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod docker; mod onboarding_key; mod permission; mod procedure; @@ -151,6 +152,7 @@ enum ReadRequest { ListStacks(ListStacks), ListFullStacks(ListFullStacks), ListStackServices(ListStackServices), + ListAllStackServices(ListAllStackServices), ListCommonStackExtraArgs(ListCommonStackExtraArgs), ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs), diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index a6a690821a..a207fa5ac6 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -1,5 +1,4 @@ use std::{ - cmp, collections::HashMap, sync::{Arc, OnceLock}, }; @@ -15,44 +14,23 @@ use database::mungos::{ use komodo_client::{ api::read::*, entities::{ - ResourceTarget, - deployment::Deployment, - docker::{ - container::{ - Container, ContainerListItem, ContainerStateStatusEnum, - }, - image::{Image, ImageHistoryResponseItem}, - network::Network, - volume::Volume, - }, permission::PermissionLevel, server::{ - Server, ServerActionState, ServerListItem, ServerQuery, - ServerState, + Server, ServerActionState, ServerListItem, ServerState, }, - stack::{Stack, StackServiceNames}, stats::{SystemInformation, SystemProcess}, - update::Log, }, }; use mogh_error::AddStatusCode; use mogh_resolver::Resolve; -use periphery_client::api::{ - self as periphery, - container::InspectContainer, - docker::{ - ImageHistory, InspectImage, InspectNetwork, InspectVolume, - }, -}; +use periphery_client::api as periphery; use reqwest::StatusCode; use tokio::sync::Mutex; -use wildcard::Wildcard; use crate::{ helpers::{periphery_client, query::get_all_tags}, - permission::{get_check_permissions, list_resources_for_user}, + permission::get_check_permissions, resource, - stack::compose_container_match_regex, state::{action_states, db_client, server_status_cache}, }; @@ -369,535 +347,3 @@ impl Resolve for GetHistoricalServerStats { Ok(res) } } - -impl Resolve for ListDockerContainers { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.containers.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for ListAllDockerContainers { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let servers = resource::list_for_user::( - ServerQuery::builder().names(self.servers.clone()).build(), - user, - PermissionLevel::Read.into(), - &[], - ) - .await?; - - let terms = self - .containers - .iter() - .flat_map(|term| { - anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) - }) - .collect::>(); - - let mut containers = Vec::::new(); - let mut skipped = 0; - let limit_usize = self.limit as usize; - - for server in servers { - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - let Some(docker) = &cache.docker else { - continue; - }; - let more = docker - .containers - .iter() - .filter(|container| { - terms.is_empty() - // Match when all terms contained within a name. - || terms.iter().all(|(term, _)| container.name.contains(*term)) - // Match when any wildcard term directly matches. - || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes())) - }); - for container in more { - if skipped < self.limit * self.page { - // Eg. page 1 skips until after 300 containers, page 2 after 600. - skipped += 1; - } else { - // push and maybe early return - containers.push(container.clone()); - if containers.len() >= limit_usize { - return Ok(containers); - } - } - } - } - - Ok(containers) - } -} - -impl Resolve for GetDockerContainersSummary { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let servers = resource::list_full_for_user::( - Default::default(), - user, - PermissionLevel::Read.into(), - &[], - ) - .await - .context("failed to get servers from db")?; - - let mut res = GetDockerContainersSummaryResponse::default(); - - for server in servers { - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - - if let Some(docker) = &cache.docker { - for container in &docker.containers { - res.total += 1; - match container.state { - ContainerStateStatusEnum::Created - | ContainerStateStatusEnum::Paused - | ContainerStateStatusEnum::Exited => res.stopped += 1, - ContainerStateStatusEnum::Running => res.running += 1, - ContainerStateStatusEnum::Empty => res.unknown += 1, - _ => res.unhealthy += 1, - } - } - } - } - - Ok(res) - } -} - -impl Resolve for InspectDockerContainer { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.inspect(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot inspect container: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectContainer { - name: self.container, - }) - .await?; - Ok(res) - } -} - -const MAX_LOG_LENGTH: u64 = 5000; - -impl Resolve for GetContainerLog { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let GetContainerLog { - server, - container, - tail, - timestamps, - } = self; - let server = get_check_permissions::( - &server, - user, - PermissionLevel::Read.logs(), - ) - .await?; - let res = periphery_client(&server) - .await? - .request(periphery::container::GetContainerLog { - name: container, - tail: cmp::min(tail, MAX_LOG_LENGTH), - timestamps, - }) - .await - .context("failed at call to periphery")?; - Ok(res) - } -} - -impl Resolve for SearchContainerLog { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let SearchContainerLog { - server, - container, - terms, - combinator, - invert, - timestamps, - } = self; - let server = get_check_permissions::( - &server, - user, - PermissionLevel::Read.logs(), - ) - .await?; - let res = periphery_client(&server) - .await? - .request(periphery::container::GetContainerLogSearch { - name: container, - terms, - combinator, - invert, - timestamps, - }) - .await - .context("failed at call to periphery")?; - Ok(res) - } -} - -impl Resolve for GetResourceMatchingContainer { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - // first check deployments - if let Ok(deployment) = - resource::get::(&self.container).await - { - return Ok(GetResourceMatchingContainerResponse { - resource: ResourceTarget::Deployment(deployment.id).into(), - }); - } - - // then check stacks - let stacks = list_resources_for_user::( - doc! { "config.server_id": &server.id }, - user, - PermissionLevel::Read.into(), - ) - .await?; - - // check matching stack - for stack in stacks { - for StackServiceNames { - service_name, - container_name, - .. - } in stack - .info - .deployed_services - .unwrap_or(stack.info.latest_services) - { - let is_match = match compose_container_match_regex(&container_name) - .with_context(|| format!("failed to construct container name matching regex for service {service_name}")) - { - Ok(regex) => regex, - Err(e) => { - warn!("{e:#}"); - continue; - } - }.is_match(&self.container); - - if is_match { - return Ok(GetResourceMatchingContainerResponse { - resource: ResourceTarget::Stack(stack.id).into(), - }); - } - } - } - - Ok(GetResourceMatchingContainerResponse { resource: None }) - } -} - -impl Resolve for ListDockerNetworks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.networks.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerNetwork { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot inspect network: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectNetwork { name: self.network }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerImages { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.images.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerImage { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!("Cannot inspect image: server is {:?}", cache.state) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectImage { name: self.image }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerImageHistory { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result> { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot get image history: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(ImageHistory { name: self.image }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerVolumes { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.volumes.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerVolume { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!("Cannot inspect volume: server is {:?}", cache.state) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectVolume { name: self.volume }) - .await?; - Ok(res) - } -} - -impl Resolve for ListComposeProjects { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.projects.clone()) - } else { - Ok(Vec::new()) - } - } -} - -// impl Resolve for ListAllTerminals { -// async fn resolve( -// self, -// args: &ReadArgs, -// ) -> Result { -// // match self.tar -// let mut terminals = resource::list_full_for_user::( -// self.query, &args.user, &all_tags, -// ) -// .await? -// .into_iter() -// .map(|server| async move { -// ( -// list_terminals_inner(&server, self.fresh).await, -// (server.id, server.name), -// ) -// }) -// .collect::>() -// .collect::>() -// .await -// .into_iter() -// .flat_map(|(terminals, server)| { -// let terminals = terminals.ok()?; -// Some((terminals, server)) -// }) -// .flat_map(|(terminals, (server_id, server_name))| { -// terminals.into_iter().map(move |info| { -// TerminalInfoWithServer::from_terminal_info( -// &server_id, -// &server_name, -// info, -// ) -// }) -// }) -// .collect::>(); - -// terminals.sort_by(|a, b| { -// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name)) -// }); - -// Ok(terminals) -// } -// } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 984cc04678..bf880aa1d2 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -9,7 +9,10 @@ use komodo_client::{ container::Container, service::SwarmService, stack::SwarmStack, }, permission::PermissionLevel, - stack::{Stack, StackActionState, StackListItem, StackState}, + stack::{ + Stack, StackActionState, StackListItem, StackQuery, + StackService, StackState, + }, }, }; use mogh_error::AddStatusCodeError as _; @@ -19,6 +22,7 @@ use periphery_client::api::{ container::InspectContainer, }; use reqwest::StatusCode; +use wildcard::Wildcard; use crate::{ helpers::{ @@ -72,6 +76,64 @@ impl Resolve for ListStackServices { } } +impl Resolve for ListAllStackServices { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let stacks = resource::list_for_user::( + StackQuery::builder() + .names(self.stacks.clone()) + .tags(self.tags) + .build(), + user, + PermissionLevel::Read.into(), + &[], + ) + .await?; + + let terms = self + .services + .iter() + .flat_map(|term| { + anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) + }) + .collect::>(); + + let mut services = Vec::::new(); + let mut skipped = 0; + let limit_usize = self.limit as usize; + + for stack in stacks { + let cache = + stack_status_cache().get_or_insert_default(&stack.id).await; + let more = cache.curr.services + .iter() + .filter(|service| { + terms.is_empty() + // Match when all terms contained within a name. + || terms.iter().all(|(term, _)| service.service.contains(*term)) + // Match when any wildcard term directly matches. + || terms.iter().any(|(_, wc)| wc.is_match(service.service.as_bytes())) + }); + for service in more { + if skipped < self.limit * self.page { + // Eg. page 1 skips until after 300 services, page 2 after 600. + skipped += 1; + } else { + // push and maybe early return + services.push(service.clone()); + if services.len() >= limit_usize { + return Ok(services); + } + } + } + } + + Ok(services) + } +} + impl Resolve for GetStackLog { async fn resolve( self, diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 49fe252090..39c3c787be 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -96,6 +96,7 @@ pub async fn update_swarm_stack_cache( )); StackService { + stack_id: stack.id.clone(), service: service_name.clone(), container: None, swarm_service, @@ -177,6 +178,7 @@ pub async fn update_server_stack_cache( )); StackService { + stack_id: stack.id.clone(), service: service_name.clone(), image: image.clone(), container, diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 423b1e3861..89be211507 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -84,6 +84,10 @@ pub struct ListAllDockerContainers { #[serde(default)] pub servers: Vec, + /// Filter servers by tag. + #[serde(default)] + pub tags: Vec, + /// Filter by container name. /// Supports wildcard matching syntax. #[serde(default)] diff --git a/client/core/rs/src/api/read/openapi.rs b/client/core/rs/src/api/read/openapi.rs index 4601da1961..b83ece3da5 100644 --- a/client/core/rs/src/api/read/openapi.rs +++ b/client/core/rs/src/api/read/openapi.rs @@ -59,6 +59,7 @@ use crate::api::read; read::get_stack_action_state, read::get_stacks_summary, read::list_stack_services, + read::list_all_stack_services, read::inspect_stack_container, read::inspect_stack_swarm_service, read::inspect_stack_swarm_info, diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 15e70f2e3d..87268bd03a 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -77,6 +77,63 @@ pub type ListStackServicesResponse = Vec; // +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListAllStackServices", + description = "List all stack services on the target servers / stacks.", + request_body(content = ListAllStackServices), + responses( + (status = 200, description = "The list of stack services", body = ListAllStackServicesResponse), + ), +)] +pub fn list_all_stack_services() {} + +/// List all stack services part of the target stacks. +/// Response: [ListStackServicesResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListAllStackServicesResponse)] +#[error(mogh_error::Error)] +pub struct ListAllStackServices { + /// Filter by stack name. + #[serde(default)] + pub stacks: Vec, + + /// Filter stacks by tag. + #[serde(default)] + pub tags: Vec, + + /// Filter by service name. + /// Supports wildcard matching syntax. + #[serde(default)] + pub services: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of services per-page. + /// `limit: 300` is default. + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_limit")] + pub limit: U64, +} + +fn default_limit() -> u64 { + 300 +} + +#[typeshare] +pub type ListAllStackServicesResponse = Vec; + +// + #[cfg(feature = "utoipa")] #[utoipa::path( post, diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 1b62304bd2..2716397f70 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -807,6 +807,8 @@ pub struct StackServiceNames { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct StackService { + /// The stack which the service is a part of. + pub stack_id: String, /// The service name pub service: String, /// The service image diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 46bbe9aacb..4434f3084b 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -76,6 +76,7 @@ export type ReadResponses = { ListStacks: Types.ListStacksResponse; ListFullStacks: Types.ListFullStacksResponse; ListStackServices: Types.ListStackServicesResponse; + ListAllStackServices: Types.ListAllStackServicesResponse; ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 6a3ced2690..1d561f3111 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -4963,6 +4963,23 @@ export interface ContainerListItem { export type ListAllDockerContainersResponse = ContainerListItem[]; +export interface StackService { + /** The stack which the service is a part of. */ + stack_id: string; + /** The service name */ + service: string; + /** The service image */ + image: string; + /** The container (Server mode) */ + container?: ContainerListItem; + /** The service (Swarm mode) */ + swarm_service?: SwarmServiceListItem; + /** The service image digests */ + image_digests?: ImageDigest[]; +} + +export type ListAllStackServicesResponse = StackService[]; + /** An api key used to authenticate requests via request headers. */ export interface ApiKey { /** Unique key associated with secret */ @@ -5400,19 +5417,6 @@ export type ServerListItem = ResourceListItem; export type ListServersResponse = ServerListItem[]; -export interface StackService { - /** The service name */ - service: string; - /** The service image */ - image: string; - /** The container (Server mode) */ - container?: ContainerListItem; - /** The service (Swarm mode) */ - swarm_service?: SwarmServiceListItem; - /** The service image digests */ - image_digests?: ImageDigest[]; -} - export type ListStackServicesResponse = StackService[]; export enum StackState { @@ -8577,6 +8581,8 @@ export interface ListAlertsResponse { export interface ListAllDockerContainers { /** Filter by server id or name. */ servers?: string[]; + /** Filter servers by tag. */ + tags?: string[]; /** * Filter by container name. * Supports wildcard matching syntax. @@ -8597,6 +8603,35 @@ export interface ListAllDockerContainers { limit: U64; } +/** + * List all stack services part of the target stacks. + * Response: [ListStackServicesResponse]. + */ +export interface ListAllStackServices { + /** Filter by stack name. */ + stacks?: string[]; + /** Filter stacks by tag. */ + tags?: string[]; + /** + * Filter by service name. + * Supports wildcard matching syntax. + */ + services?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of services per-page. + * `limit: 300` is default. + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; +} + /** * Gets list of api keys for the calling user. * Response: [ListApiKeysResponse] @@ -10903,6 +10938,7 @@ export type ReadRequest = | { type: "ListStacks", params: ListStacks } | { type: "ListFullStacks", params: ListFullStacks } | { type: "ListStackServices", params: ListStackServices } + | { type: "ListAllStackServices", params: ListAllStackServices } | { type: "ListCommonStackExtraArgs", params: ListCommonStackExtraArgs } | { type: "ListCommonStackBuildExtraArgs", params: ListCommonStackBuildExtraArgs } | { type: "GetDeploymentsSummary", params: GetDeploymentsSummary } diff --git a/ui/public/client/responses.d.ts b/ui/public/client/responses.d.ts index 22254a253e..a10ea90866 100644 --- a/ui/public/client/responses.d.ts +++ b/ui/public/client/responses.d.ts @@ -63,6 +63,7 @@ export type ReadResponses = { ListStacks: Types.ListStacksResponse; ListFullStacks: Types.ListFullStacksResponse; ListStackServices: Types.ListStackServicesResponse; + ListAllStackServices: Types.ListAllStackServicesResponse; ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse; diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index de42756eba..e1bc4cfb5d 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -4901,6 +4901,21 @@ export interface ContainerListItem { labels?: Record; } export type ListAllDockerContainersResponse = ContainerListItem[]; +export interface StackService { + /** The stack which the service is a part of. */ + stack_id: string; + /** The service name */ + service: string; + /** The service image */ + image: string; + /** The container (Server mode) */ + container?: ContainerListItem; + /** The service (Swarm mode) */ + swarm_service?: SwarmServiceListItem; + /** The service image digests */ + image_digests?: ImageDigest[]; +} +export type ListAllStackServicesResponse = StackService[]; /** An api key used to authenticate requests via request headers. */ export interface ApiKey { /** Unique key associated with secret */ @@ -5278,18 +5293,6 @@ export interface ServerListItemInfo { } export type ServerListItem = ResourceListItem; export type ListServersResponse = ServerListItem[]; -export interface StackService { - /** The service name */ - service: string; - /** The service image */ - image: string; - /** The container (Server mode) */ - container?: ContainerListItem; - /** The service (Swarm mode) */ - swarm_service?: SwarmServiceListItem; - /** The service image digests */ - image_digests?: ImageDigest[]; -} export type ListStackServicesResponse = StackService[]; export declare enum StackState { /** The stack is currently re/deploying */ @@ -8167,6 +8170,8 @@ export interface ListAlertsResponse { export interface ListAllDockerContainers { /** Filter by server id or name. */ servers?: string[]; + /** Filter servers by tag. */ + tags?: string[]; /** * Filter by container name. * Supports wildcard matching syntax. @@ -8186,6 +8191,34 @@ export interface ListAllDockerContainers { */ limit: U64; } +/** + * List all stack services part of the target stacks. + * Response: [ListStackServicesResponse]. + */ +export interface ListAllStackServices { + /** Filter by stack name. */ + stacks?: string[]; + /** Filter stacks by tag. */ + tags?: string[]; + /** + * Filter by service name. + * Supports wildcard matching syntax. + */ + services?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of services per-page. + * `limit: 300` is default. + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; +} /** * Gets list of api keys for the calling user. * Response: [ListApiKeysResponse] @@ -10587,6 +10620,9 @@ export type ReadRequest = { } | { type: "ListStackServices"; params: ListStackServices; +} | { + type: "ListAllStackServices"; + params: ListAllStackServices; } | { type: "ListCommonStackExtraArgs"; params: ListCommonStackExtraArgs; diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 357b7e4e9a..8827d7adf0 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -19,6 +19,8 @@ import { useNavigate } from "react-router-dom"; import { TemplateMarker } from "@/components/template-marker"; import { DOCKER_LINK_ICONS } from "@/components/docker/link"; import { Types } from "komodo_client"; +import { hexColorByIntention, useDebounce } from "mogh_ui"; +import { containerStateIntention, swarmStateIntention } from "@/lib/color"; const ITEM_LIMIT = 7; let count = 0; @@ -47,11 +49,33 @@ export function useOmniSearch(): { [search], ); - const containers = useRead( - "ListAllDockerContainers", - { containers: searchTerms, limit: 300, page: 0 }, - { refetchInterval: 15_000 }, - ).data; + const debouncedTerms = useDebounce(searchTerms, 700); + + const containersQuery = useMemo( + () => ({ + containers: debouncedTerms, + limit: 300, + page: 0, + }), + [debouncedTerms], + ); + + const containers = useRead("ListAllDockerContainers", containersQuery, { + refetchInterval: 15_000, + }).data; + + const servicesQuery = useMemo( + () => ({ + services: debouncedTerms, + limit: 300, + page: 0, + }), + [debouncedTerms], + ); + + const services = useRead("ListAllStackServices", servicesQuery, { + refetchInterval: 15_000, + }).data; const _terminals = useRead( "ListTerminals", @@ -223,6 +247,28 @@ export function useOmniSearch(): { })) ?? [], }, + { + group: "Services", + actions: + services?.map((service) => { + const intention = service?.swarm_service?.State + ? swarmStateIntention(service?.swarm_service?.State) + : containerStateIntention(service?.container?.state); + const color = hexColorByIntention(intention); + return { + id: service.stack_id + " " + service.service, + label: service.service, + description: + "Stack: " + + resources.Stack?.find((stack) => service.stack_id === stack.id) + ?.name, + onClick: () => + nav(`/stacks/${service.stack_id}/service/${service.service}`), + leftSection: , + }; + }) ?? [], + }, + { group: "Terminals", actions: diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index dd130022c3..621bf241f5 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -242,17 +242,14 @@ export function useFilterResources( resources?: Types.ResourceListItem[], search?: string, ) { - const tags = useTagsFilter(); const searchSplit = search?.toLowerCase()?.split(" ") || []; return ( - resources?.filter( - (resource) => - tags.every((tag: string) => resource.tags.includes(tag)) && - (searchSplit.length > 0 - ? searchSplit.every((search) => - resource.name.toLowerCase().includes(search), - ) - : true), + resources?.filter((resource) => + searchSplit.length > 0 + ? searchSplit.every((search) => + resource.name.toLowerCase().includes(search), + ) + : true, ) ?? [] ); } diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index d4ecf3ef16..83dce803a1 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -1,7 +1,7 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useRead, useTagsFilter } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; @@ -11,6 +11,7 @@ import { useCallback, useMemo, useState } from "react"; import { DividedChildren } from "mogh_ui"; import ResourceLink from "@/resources/link"; import { SearchInput } from "mogh_ui"; +import TagsFilter from "@/components/tags/filter"; export default function Containers() { const [search, setSearch] = useState(""); @@ -34,10 +35,13 @@ export default function Containers() { [servers], ); + const tags = useTagsFilter(); + const containers = useRead("ListAllDockerContainers", { containers: containersQuery, servers: serverNames, + tags, page, limit: 300, }).data ?? []; @@ -205,7 +209,7 @@ export default function Containers() { > - + - + + + + + {Table} diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index e3652fcc9b..7868be7b55 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -5,6 +5,7 @@ import { useRead, useResourceParamType, useSetTitle, + useTagsFilter, useTemplatesQueryBehavior, useUser, } from "@/lib/hooks"; @@ -32,14 +33,16 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const [search, setSearch] = useState(""); const [filterUpdateAvailable, toggleFilterUpdateAvailable] = useFilterByUpdateAvailable(); + const tags = useTagsFilter(); const query = type === "Stack" || type === "Deployment" ? { + tags, query: { specific: { update_available: filterUpdateAvailable }, }, } - : {}; + : { tags }; const [templatesQueryBehavior] = useTemplatesQueryBehavior(); const resources = useRead(`List${type}s`, query).data; const templatesFilterFn = diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 221cd091d1..64ce3c08fc 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -154,5 +154,7 @@ function NetCell({ id }: { id: string }) { if (!stats) { return N/A; } - return {fmtRateBytes(ingress + egress)}; + return ( + {fmtRateBytes(ingress + egress)} + ); } diff --git a/ui/src/resources/server/version.tsx b/ui/src/resources/server/version.tsx index 519e2fa262..aa4e627a64 100644 --- a/ui/src/resources/server/version.tsx +++ b/ui/src/resources/server/version.tsx @@ -23,7 +23,9 @@ export default function ServerVersion({ id }: { id: string }) { ) : ( )} - {serverVersion ?? "Unknown"} + + {serverVersion ?? "Unknown"} + From bb440ce8677cd1478671b15d23835aaf53e86342 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 13:27:14 -0700 Subject: [PATCH 032/203] deploy 2.3.0-dev-9 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9bdbb7950..e6daf296ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 17afdad7cd..5dd53e2142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-8" +version = "2.3.0-dev-9" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 705275c7688b0681b65c77bd86b38b81fcc1ef0d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 17 Jun 2026 13:33:06 -0700 Subject: [PATCH 033/203] service icon correct size --- ui/src/app/topbar/omni-search/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 8827d7adf0..dfe86fda92 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -264,7 +264,7 @@ export function useOmniSearch(): { ?.name, onClick: () => nav(`/stacks/${service.stack_id}/service/${service.service}`), - leftSection: , + leftSection: , }; }) ?? [], }, From 1f8351377bcdedc28ce6b729a62f9d61a3895591 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 18 Jun 2026 13:02:00 -0700 Subject: [PATCH 034/203] list all containers / services support filtering by state --- bin/cli/src/command/container.rs | 2 + bin/cli/src/command/terminal.rs | 3 +- bin/core/src/api/read/docker.rs | 7 ++- bin/core/src/api/read/stack.rs | 3 + bin/core/src/monitor/resources.rs | 17 +++++- bin/periphery/src/api/build/mod.rs | 3 +- client/core/rs/src/api/read/docker.rs | 8 ++- client/core/rs/src/api/read/stack.rs | 5 ++ client/core/rs/src/entities/stack.rs | 88 ++++++++++++++++++++++++++- client/core/ts/src/types.ts | 38 ++++++++++++ lib/git/src/installed.rs | 4 +- ui/public/client/types.d.ts | 37 +++++++++++ ui/public/client/types.js | 31 ++++++++++ 13 files changed, 237 insertions(+), 9 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index 97ac1e493b..ade3b150b3 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -65,6 +65,7 @@ async fn list_containers( servers: Default::default(), tags: Default::default(), containers: names.clone(), + state: Default::default(), limit: 300, page: *page, }), @@ -151,6 +152,7 @@ pub async fn inspect_container( servers: inspect.servers.clone(), tags: Default::default(), containers: vec![inspect.container.clone()], + state: Default::default(), limit: 300, page: 0, }), diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index 68c5e0d127..721ddc7c8e 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -129,8 +129,9 @@ async fn get_server( let mut containers = client .read(ListAllDockerContainers { servers: Default::default(), - containers: vec![container.to_string()], tags: Default::default(), + containers: vec![container.to_string()], + state: Default::default(), limit: 300, page: 0, }) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index c849cdef25..20a91fddca 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -119,11 +119,14 @@ impl Resolve for ListAllDockerContainers { .containers .iter() .filter(|container| { - terms.is_empty() + // Apply state filter if defined. + (self.state.is_empty() || self.state.contains(&container.state)) && + // Apply terms filter if defined + (terms.is_empty() // Match when all terms contained within a name. || terms.iter().all(|(term, _)| container.name.contains(*term)) // Match when any wildcard term directly matches. - || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes())) + || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes()))) }); for container in more { if skipped < self.limit * self.page { diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index bf880aa1d2..e474d9d9da 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -110,6 +110,9 @@ impl Resolve for ListAllStackServices { let more = cache.curr.services .iter() .filter(|service| { + // Apply state filter if defined. + (self.state.is_empty() || self.state.contains(&service.state)) && + // Apply terms filter if defined terms.is_empty() // Match when all terms contained within a name. || terms.iter().all(|(term, _)| service.service.contains(*term)) diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 39c3c787be..6897c70072 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -6,7 +6,10 @@ use komodo_client::entities::{ container::ContainerListItem, image::ImageListItem, service::SwarmServiceListItem, stack::SwarmStackListItem, }, - stack::{Stack, StackService, StackServiceNames, StackState}, + stack::{ + Stack, StackService, StackServiceNames, StackServiceState, + StackState, + }, swarm::SwarmState, }; @@ -95,11 +98,17 @@ pub async fn update_swarm_stack_cache( None, )); + let state = swarm_service + .as_ref() + .map(|c| c.state.into()) + .unwrap_or(StackServiceState::Unknown); + StackService { stack_id: stack.id.clone(), service: service_name.clone(), container: None, swarm_service, + state, image, image_digests, } @@ -177,12 +186,18 @@ pub async fn update_server_stack_cache( None )); + let state = container + .as_ref() + .map(|c| c.state.into()) + .unwrap_or(StackServiceState::Unknown); + StackService { stack_id: stack.id.clone(), service: service_name.clone(), image: image.clone(), container, swarm_service: None, + state, image_digests, } }).collect::>(); diff --git a/bin/periphery/src/api/build/mod.rs b/bin/periphery/src/api/build/mod.rs index 7abe951eeb..21368818d9 100644 --- a/bin/periphery/src/api/build/mod.rs +++ b/bin/periphery/src/api/build/mod.rs @@ -6,8 +6,7 @@ use std::{ use anyhow::{Context, anyhow}; use command::{ CommandOptions, KomodoCommandMode, - run_komodo_command_with_sanitization, - run_komodo_standard_command, + run_komodo_command_with_sanitization, run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 89be211507..121a905585 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -5,7 +5,9 @@ use typeshare::typeshare; use crate::entities::{ ResourceTarget, SearchCombinator, U64, docker::{ - container::{Container, ContainerListItem}, + container::{ + Container, ContainerListItem, ContainerStateStatusEnum, + }, image::{Image, ImageHistoryResponseItem, ImageListItem}, network::{Network, NetworkListItem}, volume::{Volume, VolumeListItem}, @@ -93,6 +95,10 @@ pub struct ListAllDockerContainers { #[serde(default)] pub containers: Vec, + /// Filter by container state. + #[serde(default)] + pub state: Vec, + /// Retrieve more results by incrementing the page. /// `page: 0` is default. #[serde(default)] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 87268bd03a..646c9d293b 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -9,6 +9,7 @@ use crate::entities::{ }, stack::{ Stack, StackActionState, StackListItem, StackQuery, StackService, + StackServiceState, }, update::Log, }; @@ -111,6 +112,10 @@ pub struct ListAllStackServices { #[serde(default)] pub services: Vec, + /// Filter by service state. + #[serde(default)] + pub state: Vec, + /// Retrieve more results by incrementing the page. /// `page: 0` is default. #[serde(default)] diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 2716397f70..b0160cd030 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -22,7 +22,12 @@ use crate::{ }, entities::{ EnvironmentVar, ImageDigest, - docker::service::SwarmServiceListItem, environment_vars_from_str, + docker::{ + container::ContainerStateStatusEnum, + service::SwarmServiceListItem, + }, + environment_vars_from_str, + swarm::SwarmState, }, }; @@ -803,6 +808,7 @@ pub struct StackServiceNames { pub image_digest: Option, } +/// A stack service, whether server or swarm based. #[typeshare] #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -817,10 +823,90 @@ pub struct StackService { pub container: Option, /// The service (Swarm mode) pub swarm_service: Option, + /// The service state + pub state: StackServiceState, /// The service image digests pub image_digests: Option>, } +/// Combined state options for +/// both Server and Swarm based Stacks. +#[typeshare] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Serialize, + Deserialize, + Display, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum StackServiceState { + /// (Swarm) All tasks OK + Healthy, + /// (Swarm) Some tasks don't match desired state + Unhealthy, + /// (Swarm) All tasks down. + Down, + /// (Container) Container is running + Running, + /// (Container) Container is created + Created, + /// (Container) Container is paused + Paused, + /// (Container) Container is restarting + Restarting, + /// (Container) Container is exited + Exited, + /// (Container) Container is stopping + Stopping, + /// (Container) Container is removing + Removing, + /// (Container) Container is dead + Dead, + /// Unknown case + #[default] + Unknown, +} + +impl From for StackServiceState { + fn from(value: SwarmState) -> Self { + match value { + SwarmState::Healthy => StackServiceState::Healthy, + SwarmState::Unhealthy => StackServiceState::Unhealthy, + SwarmState::Down => StackServiceState::Down, + SwarmState::Unknown => StackServiceState::Unknown, + } + } +} + +impl From for StackServiceState { + fn from(value: ContainerStateStatusEnum) -> Self { + match value { + ContainerStateStatusEnum::Running => StackServiceState::Running, + ContainerStateStatusEnum::Created => StackServiceState::Created, + ContainerStateStatusEnum::Paused => StackServiceState::Paused, + ContainerStateStatusEnum::Restarting => { + StackServiceState::Restarting + } + ContainerStateStatusEnum::Exited => StackServiceState::Exited, + ContainerStateStatusEnum::Stopping => { + StackServiceState::Stopping + } + ContainerStateStatusEnum::Removing => { + StackServiceState::Removing + } + ContainerStateStatusEnum::Dead => StackServiceState::Dead, + ContainerStateStatusEnum::Empty => StackServiceState::Unknown, + } + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 1d561f3111..6bbafe8f32 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -4963,6 +4963,38 @@ export interface ContainerListItem { export type ListAllDockerContainersResponse = ContainerListItem[]; +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export enum StackServiceState { + /** (Swarm) All tasks OK */ + Healthy = "Healthy", + /** (Swarm) Some tasks don't match desired state */ + Unhealthy = "Unhealthy", + /** (Swarm) All tasks down. */ + Down = "Down", + /** (Container) Container is running */ + Running = "Running", + /** (Container) Container is created */ + Created = "Created", + /** (Container) Container is paused */ + Paused = "Paused", + /** (Container) Container is restarting */ + Restarting = "Restarting", + /** (Container) Container is exited */ + Exited = "Exited", + /** (Container) Container is stopping */ + Stopping = "Stopping", + /** (Container) Container is removing */ + Removing = "Removing", + /** (Container) Container is dead */ + Dead = "Dead", + /** Unknown case */ + Unknown = "Unknown", +} + +/** A stack service, whether server or swarm based. */ export interface StackService { /** The stack which the service is a part of. */ stack_id: string; @@ -4974,6 +5006,8 @@ export interface StackService { container?: ContainerListItem; /** The service (Swarm mode) */ swarm_service?: SwarmServiceListItem; + /** The service state */ + state: StackServiceState; /** The service image digests */ image_digests?: ImageDigest[]; } @@ -8588,6 +8622,8 @@ export interface ListAllDockerContainers { * Supports wildcard matching syntax. */ containers?: string[]; + /** Filter by container state. */ + state?: ContainerStateStatusEnum[]; /** * Retrieve more results by incrementing the page. * `page: 0` is default. @@ -8617,6 +8653,8 @@ export interface ListAllStackServices { * Supports wildcard matching syntax. */ services?: string[]; + /** Filter by service state. */ + state?: StackServiceState[]; /** * Retrieve more results by incrementing the page. * `page: 0` is default. diff --git a/lib/git/src/installed.rs b/lib/git/src/installed.rs index 0c7d78422c..6b3af67003 100644 --- a/lib/git/src/installed.rs +++ b/lib/git/src/installed.rs @@ -14,6 +14,8 @@ pub async fn check_installed() -> anyhow::Result<()> { { Ok(()) } else { - Err(anyhow!("Failed: 'git' is not installed or available on $PATH")) + Err(anyhow!( + "Failed: 'git' is not installed or available on $PATH" + )) } } diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index e1bc4cfb5d..fc73d82341 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -4901,6 +4901,37 @@ export interface ContainerListItem { labels?: Record; } export type ListAllDockerContainersResponse = ContainerListItem[]; +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export declare enum StackServiceState { + /** (Swarm) All tasks OK */ + Healthy = "Healthy", + /** (Swarm) Some tasks don't match desired state */ + Unhealthy = "Unhealthy", + /** (Swarm) All tasks down. */ + Down = "Down", + /** (Container) Container is running */ + Running = "Running", + /** (Container) Container is created */ + Created = "Created", + /** (Container) Container is paused */ + Paused = "Paused", + /** (Container) Container is restarting */ + Restarting = "Restarting", + /** (Container) Container is exited */ + Exited = "Exited", + /** (Container) Container is stopping */ + Stopping = "Stopping", + /** (Container) Container is removing */ + Removing = "Removing", + /** (Container) Container is dead */ + Dead = "Dead", + /** Unknown case */ + Unknown = "Unknown" +} +/** A stack service, whether server or swarm based. */ export interface StackService { /** The stack which the service is a part of. */ stack_id: string; @@ -4912,6 +4943,8 @@ export interface StackService { container?: ContainerListItem; /** The service (Swarm mode) */ swarm_service?: SwarmServiceListItem; + /** The service state */ + state: StackServiceState; /** The service image digests */ image_digests?: ImageDigest[]; } @@ -8177,6 +8210,8 @@ export interface ListAllDockerContainers { * Supports wildcard matching syntax. */ containers?: string[]; + /** Filter by container state. */ + state?: ContainerStateStatusEnum[]; /** * Retrieve more results by incrementing the page. * `page: 0` is default. @@ -8205,6 +8240,8 @@ export interface ListAllStackServices { * Supports wildcard matching syntax. */ services?: string[]; + /** Filter by service state. */ + state?: StackServiceState[]; /** * Retrieve more results by incrementing the page. * `page: 0` is default. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index d48eebac66..cd205174e7 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -691,6 +691,37 @@ export var PortTypeEnum; PortTypeEnum["UDP"] = "udp"; PortTypeEnum["SCTP"] = "sctp"; })(PortTypeEnum || (PortTypeEnum = {})); +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export var StackServiceState; +(function (StackServiceState) { + /** (Swarm) All tasks OK */ + StackServiceState["Healthy"] = "Healthy"; + /** (Swarm) Some tasks don't match desired state */ + StackServiceState["Unhealthy"] = "Unhealthy"; + /** (Swarm) All tasks down. */ + StackServiceState["Down"] = "Down"; + /** (Container) Container is running */ + StackServiceState["Running"] = "Running"; + /** (Container) Container is created */ + StackServiceState["Created"] = "Created"; + /** (Container) Container is paused */ + StackServiceState["Paused"] = "Paused"; + /** (Container) Container is restarting */ + StackServiceState["Restarting"] = "Restarting"; + /** (Container) Container is exited */ + StackServiceState["Exited"] = "Exited"; + /** (Container) Container is stopping */ + StackServiceState["Stopping"] = "Stopping"; + /** (Container) Container is removing */ + StackServiceState["Removing"] = "Removing"; + /** (Container) Container is dead */ + StackServiceState["Dead"] = "Dead"; + /** Unknown case */ + StackServiceState["Unknown"] = "Unknown"; +})(StackServiceState || (StackServiceState = {})); export var ProcedureState; (function (ProcedureState) { /** Currently running */ From 4ae33939c9a3a9279cf880058ada2ff7ae483477 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 18 Jun 2026 13:05:05 -0700 Subject: [PATCH 035/203] get latest image digest timeout 10s --- bin/periphery/src/docker/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/periphery/src/docker/image.rs b/bin/periphery/src/docker/image.rs index fd910b550e..f054268210 100644 --- a/bin/periphery/src/docker/image.rs +++ b/bin/periphery/src/docker/image.rs @@ -198,7 +198,7 @@ pub async fn get_image_digest_from_registry( let log = run_komodo_standard_command( "", command, - CommandOptions::default().timeout(Duration::from_secs(3)), + CommandOptions::default().timeout(Duration::from_secs(10)), ) .await; if !log.success { From e3658912b7af32a36bbefcc714ae6f186f588178 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 18 Jun 2026 13:05:35 -0700 Subject: [PATCH 036/203] deploy 2.3.0-dev-10 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6daf296ff..86ed09bb56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5dd53e2142..93601f3684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-9" +version = "2.3.0-dev-10" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From ee0e02fc1345e55b7c24eb014366a8a18aceadff Mon Sep 17 00:00:00 2001 From: Remco Jongschaap Date: Sat, 20 Jun 2026 03:07:06 +0200 Subject: [PATCH 037/203] send keepalive pings on user websockets to survive idle proxy timeouts (#1486) --- bin/core/src/api/ws/mod.rs | 3 +++ bin/core/src/api/ws/terminal.rs | 18 +++++++++++++++++- bin/core/src/api/ws/update.rs | 12 +++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/ws/mod.rs b/bin/core/src/api/ws/mod.rs index 7c986a7ed1..9d71c7369c 100644 --- a/bin/core/src/api/ws/mod.rs +++ b/bin/core/src/api/ws/mod.rs @@ -24,6 +24,9 @@ use crate::{ mod terminal; mod update; +pub(super) const WS_KEEP_ALIVE_INTERVAL: std::time::Duration = + std::time::Duration::from_secs(30); + pub fn router() -> Router { Router::new() // Periphery facing diff --git a/bin/core/src/api/ws/terminal.rs b/bin/core/src/api/ws/terminal.rs index a6534d45f6..e7959c9a39 100644 --- a/bin/core/src/api/ws/terminal.rs +++ b/bin/core/src/api/ws/terminal.rs @@ -117,9 +117,25 @@ async fn forward_ws_channel( }; let periphery_to_core = async { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { // Already adheres to cancellation token - match periphery_receiver.recv().await { + let recv = tokio::select! { + // Websocket keep-alive ping + _ = keep_alive.tick() => { + if client_send + .send(ws::Message::Ping(Bytes::new())) + .await + .is_err() + { + break; + } + continue; + } + recv = periphery_receiver.recv() => recv, + }; + match recv { Ok(Ok(bytes)) => { if let Err(e) = client_send.send(ws::Message::Binary(bytes.into())).await diff --git a/bin/core/src/api/ws/update.rs b/bin/core/src/api/ws/update.rs index 74d7519176..b5503e9a59 100644 --- a/bin/core/src/api/ws/update.rs +++ b/bin/core/src/api/ws/update.rs @@ -3,6 +3,7 @@ use axum::{ extract::{WebSocketUpgrade, ws::Message}, response::IntoResponse, }; +use bytes::Bytes; use futures_util::{SinkExt, StreamExt}; use komodo_client::entities::{ ResourceTarget, permission::PermissionLevel, user::User, @@ -38,10 +39,19 @@ pub async fn handler( let cancel_clone = cancel.clone(); tokio::spawn(async move { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { - // poll for updates off the receiver / await cancel. + // poll for updates off the receiver / await cancel / send keep-alive ping. let update = select! { _ = cancel_clone.cancelled() => break, + _ = keep_alive.tick() => { + if ws_sender.send(Message::Ping(Bytes::new())).await.is_err() { + cancel_clone.cancel(); + break; + } + continue; + } update = receiver.recv() => {update.expect("failed to recv update msg")} }; From 083f998d16e6a56dfe1d1cd660300385bb5c1ca8 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 19 Jun 2026 18:54:08 -0700 Subject: [PATCH 038/203] bump mantine and other UI deps --- ui/package.json | 16 ++++----- ui/yarn.lock | 96 ++++++++++++++++++++++++------------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/ui/package.json b/ui/package.json index 03c1bf1605..82453f9c9e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,11 +10,11 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@mantine/core": "^9.3.1", - "@mantine/form": "^9.3.1", - "@mantine/hooks": "^9.3.1", - "@mantine/notifications": "^9.3.1", - "@mantine/spotlight": "^9.3.1", + "@mantine/core": "^9.3.2", + "@mantine/form": "^9.3.2", + "@mantine/hooks": "^9.3.2", + "@mantine/notifications": "^9.3.2", + "@mantine/spotlight": "^9.3.2", "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.101.0", "@tanstack/react-table": "^8.21.3", @@ -24,7 +24,7 @@ "jotai": "^2.20.1", "jotai-family": "^1.0.2", "jotai-location": "^0.6.2", - "lucide-react": "^1.18.0", + "lucide-react": "^1.21.0", "mogh_auth_client": "^1.5.0", "mogh_ui": "^0.6.1", "monaco-editor": "^0.55.1", @@ -32,14 +32,14 @@ "react": "^19.2.7", "react-dom": "^19.2.7", "react-minimal-pie-chart": "^9.1.2", - "react-router-dom": "^7.17.0", + "react-router-dom": "^7.18.0", "react-xtermjs": "^1.0.12", "recharts": "^3.8.1", "sanitize-html": "^2.17.5", "shell-quote": "^1.8.4" }, "devDependencies": { - "@types/node": "^25.9.3", + "@types/node": "^26.0.0", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/sanitize-html": "^2.16.1", diff --git a/ui/yarn.lock b/ui/yarn.lock index 3ab41bf7c3..1595be8885 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -70,10 +70,10 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== -"@mantine/core@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.3.1.tgz#6b56312e9660fe3594b842d6e6b782d83a77b5e2" - integrity sha512-4rBoHpggSohayE+Os7lKdbsHTw7m/uZRKYjk9DN6cKBhQIjdiULdcG+b4b5CMVZSqZDHPgT70uWGI7yqkn8Ufw== +"@mantine/core@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.3.2.tgz#cb23bea516281d96ad86e543827580a84820ce05" + integrity sha512-Upy/Z9Sj2eW2dGrFgUy/2kISVsxMTBYTDfP2TFdsIA3PdPSBkdqfSOPX/ug3d3F3a4bnwQSqcO6+aPEpBIh8dg== dependencies: "@floating-ui/react" "^0.27.19" clsx "^2.1.1" @@ -81,39 +81,39 @@ react-remove-scroll "^2.7.2" type-fest "^5.6.0" -"@mantine/form@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.3.1.tgz#45576b331c512ac85fb56d7f28000dce181e943d" - integrity sha512-dGJPcYGkfDnAPKHcnzgf2mSlnjwymaOyROiTEJNbbjE3OVoWbPaw8HwEKav1wATdxNYR9Bw1GHtbfbGTsm1Jww== +"@mantine/form@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.3.2.tgz#4aed07aa4aae09fa96921101bde1892fcfbf120f" + integrity sha512-jWEXcx8hhddOsny+q/lFtVToNS3VBJYICrkfFMWJsN+WAZamnPItWTPLqulTYVp1T8uma9MsEZ1LX4kqWUFANA== dependencies: "@standard-schema/spec" "^1.1.0" fast-deep-equal "^3.1.3" klona "^2.0.6" -"@mantine/hooks@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.3.1.tgz#b94c663b2ab2ba02a327cc06ebb0554b97c15b83" - integrity sha512-zAOlxV59j5CDgAnExN+ypaR6dVW1vwMKDvKUxIlUVd/e52qxYnPyYRD+shJmyOLaZRuQmVF0R/7mJAjt2jw9cA== +"@mantine/hooks@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.3.2.tgz#1a53767d3d2ec983c105498bf4e3805f08d1ddc3" + integrity sha512-jOjpUe0x1A/k3XUiu2/aaSCasXRI5ZKZOucm3ypwsvm9F2u8C1xzwBvagrzlbNZwJRF5vNYIJtCaT7NunPyc0A== -"@mantine/notifications@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.3.1.tgz#f9675dbc763fa42ad23602a99f59d965c85a1b2d" - integrity sha512-irkD9uszgVftthbCLd+4lS7M1jkhL4sVrZASv8+43aIkncW+oQkCFpBuaOAf6uRcMjmQ7XnKs2O8bXYzl/DqgQ== +"@mantine/notifications@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.3.2.tgz#c608bb5af801c7091f9eb2c15be51d1ec8251363" + integrity sha512-mnda8hE/DizPi5Bbiwd2M2jXxfSZSUeayvnLYMbFweGQKXM+QRmX1ootyDRVqWew8paltewTNp84cWLjj/6fKw== dependencies: - "@mantine/store" "9.3.1" + "@mantine/store" "9.3.2" react-transition-group "4.4.5" -"@mantine/spotlight@^9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.3.1.tgz#e0f64c8f360a31a6ada92b8c91941c87cbae67c8" - integrity sha512-NsNXywYhKuHnESoMCX1+e2za0GQJYQ43zMUIkr8XwHiyIzdU/tmFJh+k+jfUeFKPfnS5uRy+vJvC6z320aNbTw== +"@mantine/spotlight@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.3.2.tgz#3cab5fe280b1612577b8d0b18314ff287873e7ff" + integrity sha512-JAJhW51qvYNiPTEnX9ewZX521gKDsVsmaecuuR2iNyk0NbrOPB5SWF9ZZrPSMwFVtqnqYHziq5BLukzlf+kG/Q== dependencies: - "@mantine/store" "9.3.1" + "@mantine/store" "9.3.2" -"@mantine/store@9.3.1": - version "9.3.1" - resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.3.1.tgz#e6e657167feabeb7a51ac5cc3de294d3757c74b5" - integrity sha512-UZ8wGKfF/NFMHCDl1Rd0M8XtHdt4W1w3V4ohNpFSaP3nSjJ+Cu1FT+iIWtXwC0Ogha8XnuVQ7vm/dw2BUfpw+g== +"@mantine/store@9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.3.2.tgz#2b878741b04147fdaecb40fed0c157eefde95e3b" + integrity sha512-uNmvjkwfUHZJ5bvzQroLFvubuUAHjBM0rh69RBRsUCG5FR5K4f8y4SX8+lch0wS6UJuuvodsSIK8H5jvZuFSgQ== "@monaco-editor/loader@^1.5.0": version "1.7.0" @@ -418,12 +418,12 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== -"@types/node@^25.9.3": - version "25.9.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.3.tgz#11dfe7a33e68fa5c560f0aa76cc5595621ef26b9" - integrity sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg== +"@types/node@^26.0.0": + version "26.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.0.tgz#d4aece9e9412e9f2008d59bc2d74f5279316b665" + integrity sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA== dependencies: - undici-types ">=7.24.0 <7.24.7" + undici-types "~8.3.0" "@types/react-dom@^19.2.3": version "19.2.3" @@ -896,10 +896,10 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lucide-react@^1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.18.0.tgz#d1b8754d9c427061261c6a6a0b7fa6ecda36c84f" - integrity sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA== +lucide-react@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.21.0.tgz#ef99ccf355af9bf5aec74ac2f122f7da1a9ea608" + integrity sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ== marked@14.0.0: version "14.0.0" @@ -1149,17 +1149,17 @@ react-remove-scroll@^2.7.2: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router-dom@^7.17.0: - version "7.17.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.17.0.tgz#e77527b4b7862f7b47ff26dd5b9315fb897b82a7" - integrity sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw== +react-router-dom@^7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.0.tgz#71a657374610e00cebf2783804d7812facfdb1b5" + integrity sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA== dependencies: - react-router "7.17.0" + react-router "7.18.0" -react-router@7.17.0: - version "7.17.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.17.0.tgz#88bbe817c6e37ab36faf140623b5d4678bf81e41" - integrity sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ== +react-router@7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.0.tgz#e7d94b54745277aabe3cf93fac938cbebc9c1c5e" + integrity sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -1512,10 +1512,10 @@ typescript@^6.0.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== -"undici-types@>=7.24.0 <7.24.7": - version "7.24.6" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" - integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== use-callback-ref@^1.3.3: version "1.3.3" From 8920b3af492b43d73774d732fb598d65b06450d3 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 19 Jun 2026 18:54:35 -0700 Subject: [PATCH 039/203] deploy 2.3.0-dev-11 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86ed09bb56..82d7e1bf3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 93601f3684..630e0ea9fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-10" +version = "2.3.0-dev-11" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From c595dec51f69947f26e091d374e15c8180736c75 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 19 Jun 2026 19:10:02 -0700 Subject: [PATCH 040/203] fix containers page server filter --- ui/src/pages/containers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 83dce803a1..e5845555bb 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -40,7 +40,7 @@ export default function Containers() { const containers = useRead("ListAllDockerContainers", { containers: containersQuery, - servers: serverNames, + servers: selectedServers, tags, page, limit: 300, From 7c44ffc8eedc8fb08ceb5a178e1e13504f59f619 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 21 Jun 2026 16:56:39 -0700 Subject: [PATCH 041/203] show server config always if no swarms exist --- ui/src/resources/deployment/config/index.tsx | 2 +- ui/src/resources/stack/config/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/resources/deployment/config/index.tsx b/ui/src/resources/deployment/config/index.tsx index 4aa24a0c32..a3b1f8b08c 100644 --- a/ui/src/resources/deployment/config/index.tsx +++ b/ui/src/resources/deployment/config/index.tsx @@ -99,7 +99,7 @@ export default function DeploymentConfig({ { label: "Server", labelHidden: true, - hidden: !!currSwarmId, + hidden: swarmsExist && !!currSwarmId, fields: { server_id: (server_id, set) => { return ( diff --git a/ui/src/resources/stack/config/index.tsx b/ui/src/resources/stack/config/index.tsx index 80b6feb5ff..f01b5cd8ce 100644 --- a/ui/src/resources/stack/config/index.tsx +++ b/ui/src/resources/stack/config/index.tsx @@ -185,7 +185,7 @@ export default function StackConfig({ { label: "Server", labelHidden: true, - hidden: !!currSwarmId, + hidden: swarmsExist && !!currSwarmId, fields: { server_id: (server_id, set) => { return ( From 1d4a209e8fb291317fb5d703fed503aac92b9bec Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 26 Jun 2026 00:13:03 -0700 Subject: [PATCH 042/203] DeleteTerminal: fix incoming stack / deployment names to ids internally --- bin/core/src/api/write/terminal.rs | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/bin/core/src/api/write/terminal.rs b/bin/core/src/api/write/terminal.rs index 815847072d..a4aad5685a 100644 --- a/bin/core/src/api/write/terminal.rs +++ b/bin/core/src/api/write/terminal.rs @@ -171,10 +171,10 @@ impl Resolve for DeleteTerminal { ) )] async fn resolve( - self, + mut self, WriteArgs { user }: &WriteArgs, ) -> mogh_error::Result { - let server = match &self.target { + let server = match &mut self.target { TerminalTarget::Server { server } => { let server = server .as_ref() @@ -195,27 +195,31 @@ impl Resolve for DeleteTerminal { ) .await? } - TerminalTarget::Stack { stack, .. } => { - let server = get_check_permissions::( - stack, + TerminalTarget::Stack { + stack: stack_id, .. + } => { + let stack = get_check_permissions::( + stack_id, user, PermissionLevel::Read.terminal(), ) - .await? - .config - .server_id; - resource::get::(&server).await? + .await?; + // Must fix any incoming stack name to id + *stack_id = stack.id; + resource::get::(&stack.config.server_id).await? } - TerminalTarget::Deployment { deployment } => { - let server = get_check_permissions::( - deployment, + TerminalTarget::Deployment { + deployment: deployment_id, + } => { + let deployment = get_check_permissions::( + deployment_id, user, PermissionLevel::Read.terminal(), ) - .await? - .config - .server_id; - resource::get::(&server).await? + .await?; + // Must fix any incoming deployment name to id + *deployment_id = deployment.id; + resource::get::(&deployment.config.server_id).await? } }; From 50e04187cf52cf644be4c69fd888787a36e8eaa7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 22:29:30 -0700 Subject: [PATCH 043/203] single builder distribute to multiple specified servers --- bin/core/src/api/execute/build.rs | 2 +- bin/core/src/api/execute/repo.rs | 2 +- bin/core/src/api/read/mod.rs | 28 ++-- bin/core/src/api/write/build.rs | 61 ++------- bin/core/src/connection/server.rs | 2 +- bin/core/src/helpers/builder.rs | 146 +++++++++++++++++---- bin/core/src/resource/builder.rs | 12 +- bin/core/src/startup.rs | 2 +- bin/core/src/sync/resources.rs | 14 +- bin/core/src/sync/toml.rs | 12 +- client/core/rs/src/entities/builder.rs | 27 +++- client/core/ts/src/types.ts | 8 +- ui/public/client/types.d.ts | 8 +- ui/src/resources/builder/config/server.tsx | 86 ++++++++---- 14 files changed, 268 insertions(+), 142 deletions(-) diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index 4d511b82f3..cc0b00f468 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -216,7 +216,7 @@ impl Resolve for RunBuild { build.name.clone(), Some(build.config.version), builder, - &mut update, + Some(&mut update), ) .await { diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs index 0fcc1badd6..8e2864bf73 100644 --- a/bin/core/src/api/execute/repo.rs +++ b/bin/core/src/api/execute/repo.rs @@ -490,7 +490,7 @@ impl Resolve for BuildRepo { repo.name.clone(), None, builder, - &mut update, + Some(&mut update), ) .await { diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index bad446baec..be680bb646 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -381,7 +381,9 @@ impl Resolve for ListSecrets { ResourceTarget::Builder(id) => { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => None, - BuilderConfig::Server(config) => Some(config.server_id), + BuilderConfig::Server(config) => { + config.server_ids.first().cloned() + } BuilderConfig::Aws(config) => { secrets.extend(config.secrets); None @@ -436,11 +438,13 @@ impl Resolve for ListGitProvidersFromConfig { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => {} BuilderConfig::Server(config) => { - merge_git_providers_for_server( - &mut providers, - &config.server_id, - ) - .await?; + if let Some(server_id) = config.server_ids.first() { + merge_git_providers_for_server( + &mut providers, + server_id, + ) + .await?; + } } BuilderConfig::Aws(config) => { merge_git_providers( @@ -542,11 +546,13 @@ impl Resolve for ListDockerRegistriesFromConfig { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => {} BuilderConfig::Server(config) => { - merge_docker_registries_for_server( - &mut registries, - &config.server_id, - ) - .await?; + if let Some(server_id) = config.server_ids.first() { + merge_docker_registries_for_server( + &mut registries, + server_id, + ) + .await?; + } } BuilderConfig::Aws(config) => { merge_docker_registries( diff --git a/bin/core/src/api/write/build.rs b/bin/core/src/api/write/build.rs index 87d19fa585..273ce9781a 100644 --- a/bin/core/src/api/write/build.rs +++ b/bin/core/src/api/write/build.rs @@ -1,10 +1,8 @@ -use std::{path::PathBuf, time::Duration}; +use std::path::PathBuf; use anyhow::{Context, anyhow}; +use database::mongo_indexed::doc; use database::mungos::mongodb::bson::to_document; -use database::{ - mongo_indexed::doc, mungos::mongodb::bson::oid::ObjectId, -}; use formatting::format_serror; use komodo_client::{ api::write::*, @@ -15,7 +13,6 @@ use komodo_client::{ builder::{Builder, BuilderConfig}, permission::PermissionLevel, repo::Repo, - server::ServerState, update::Update, }, }; @@ -25,12 +22,11 @@ use periphery_client::api::build::{ }; use tokio::fs; +use crate::helpers::builder::connect_builder_periphery; use crate::{ config::core_config, - connection::PeripheryConnectionArgs, helpers::{ - git_token, periphery_client, - query::get_server_with_state, + git_token, update::{add_update, make_update}, }, periphery::PeripheryClient, @@ -476,48 +472,15 @@ async fn get_on_host_periphery( .await .context("Failed to get builder")?; - match builder.config { - BuilderConfig::Aws(_) => { - Err(anyhow!("Files on host doesn't work with AWS builder")) - } - BuilderConfig::Url(config) => { - // TODO: Ensure connection is actually established. - // Builder id no good because it may be active for multiple connections. - let periphery = PeripheryClient::new( - PeripheryConnectionArgs::from_url_builder( - &ObjectId::new().to_hex(), - &config, - ), - config.insecure_tls, - ) - .await?; - // Poll for connection to be estalished - let mut err = None; - for _ in 0..10 { - tokio::time::sleep(Duration::from_secs(1)).await; - match periphery.health_check().await { - Ok(_) => return Ok(periphery), - Err(e) => err = Some(e), - }; - } - Err(err.context("Missing error")?) - } - BuilderConfig::Server(config) => { - if config.server_id.is_empty() { - return Err(anyhow!( - "Builder is type server, but has no server attached" - )); - } - let (server, state) = - get_server_with_state(&config.server_id).await?; - if state != ServerState::Ok { - return Err(anyhow!( - "Builder server is disabled or not reachable" - )); - }; - periphery_client(&server).await - } + if let BuilderConfig::Aws(_) = builder.config { + return Err(anyhow!( + "Files on host doesn't work with AWS builder" + )); } + + connect_builder_periphery(build.name.clone(), None, builder, None) + .await + .map(|(periphery, _)| periphery) } /// The successful case will be included as Some(remote_contents). diff --git a/bin/core/src/connection/server.rs b/bin/core/src/connection/server.rs index ee5d6d4ad3..8631ca441e 100644 --- a/bin/core/src/connection/server.rs +++ b/bin/core/src/connection/server.rs @@ -503,7 +503,7 @@ async fn create_server_maybe_builder( name: server_query, config: PartialBuilderConfig::Server( PartialServerBuilderConfig { - server_id: Some(server.id.clone()), + server_ids: Some(vec![server.id.clone()]), }, ), }) diff --git a/bin/core/src/helpers/builder.rs b/bin/core/src/helpers/builder.rs index 19af31bc3a..e1c14ffbee 100644 --- a/bin/core/src/helpers/builder.rs +++ b/bin/core/src/helpers/builder.rs @@ -1,13 +1,16 @@ use std::time::Duration; use anyhow::{Context, anyhow}; -use database::mungos::mongodb::bson::oid::ObjectId; +use database::{bson::doc, mungos::mongodb::bson::oid::ObjectId}; use formatting::muted; +use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ Version, + build::{Build, BuildState}, builder::{AwsBuilderConfig, Builder, BuilderConfig}, komodo_timestamp, - server::Server, + repo::{Repo, RepoState}, + server::{Server, ServerState}, update::{Log, Update}, }; use periphery_client::api::{self, GetVersionResponse}; @@ -23,7 +26,8 @@ use crate::{ connection::PeripheryConnectionArgs, helpers::update::update_update, periphery::PeripheryClient, - resource, + resource::{self, list_all_resources}, + state::{build_state_cache, repo_state_cache, server_status_cache}, }; use super::periphery_client; @@ -37,25 +41,26 @@ const BUILDER_POLL_MAX_TRIES: usize = 60; fields( resource_name, builder_id = builder.id, - update_id = update.id + update_id = update.as_ref().map(|u| u.id.as_str()) ) )] pub async fn connect_builder_periphery( - // build: &Build, resource_name: String, version: Option, builder: Builder, - update: &mut Update, + update: Option<&mut Update>, ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { match builder.config { + BuilderConfig::Aws(config) => { + get_aws_builder(&resource_name, version, config, update).await + } BuilderConfig::Url(config) => { if config.address.is_empty() { return Err(anyhow!( "Builder has not yet configured an address" )); } - // TODO: Dont use builder id, or will be problems - // with simultaneous spawned builders. + // Builder id no good because it may be active for multiple connections. let periphery = PeripheryClient::new( PeripheryConnectionArgs::from_url_builder( &ObjectId::new().to_hex(), @@ -64,23 +69,109 @@ pub async fn connect_builder_periphery( config.insecure_tls, ) .await?; - periphery - .health_check() - .await - .context("Url Builder failed health check")?; - Ok((periphery, BuildCleanupData::Url)) + // Poll for connection to be estalished + let mut err = None; + for _ in 0..10 { + tokio::time::sleep(Duration::from_secs(1)).await; + match periphery + .health_check() + .await + .context("Url Builder failed health check") + { + Ok(_) => return Ok((periphery, BuildCleanupData::Url)), + Err(e) => err = Some(e), + }; + } + Err(err.context("Missing error")?) } BuilderConfig::Server(config) => { - if config.server_id.is_empty() { - return Err(anyhow!("Builder has not configured a server")); + if config.server_ids.is_empty() { + return Err(anyhow!( + "Server Builder has no configured Servers" + )); + } + + // Short path for single configured builder + if config.server_ids.len() == 1 { + let server = + resource::get::(&config.server_ids[0]).await?; + let periphery = periphery_client(&server).await?; + return Ok((periphery, BuildCleanupData::Server)); } - let server = resource::get::(&config.server_id).await?; + + // Query for builds / repos using the builder + let (builds, repos) = tokio::join!( + list_all_resources::( + doc! { "config.builder_id": &builder.id }, + ), + list_all_resources::( + doc! { "config.builder_id": &builder.id }, + ) + ); + + // Count currently building resources + let mut building = 0; + + let build_state_cache = build_state_cache(); + for build in builds + .inspect_err(|e| { + warn!("Failed to query for Builds using Builder | {e:#}") + }) + .unwrap_or_default() + { + if build_state_cache + .get(&build.id) + .await + .map(|s| matches!(s, BuildState::Building)) + .unwrap_or_default() + { + building += 1; + } + } + let repo_state_cache = repo_state_cache(); + for repo in repos + .inspect_err(|e| { + warn!("Failed to query for Repos using Builder | {e:#}") + }) + .unwrap_or_default() + { + if repo_state_cache + .get(&repo.id) + .await + .map(|s| matches!(s, RepoState::Building)) + .unwrap_or_default() + { + building += 1; + } + } + + // Get filtered list of available Servers + let server_status_cache = server_status_cache(); + let available_server_ids = config + .server_ids + .iter() + .map(|server_id| async move { + server_status_cache + .get(server_id) + .await + .map(|s| matches!(s.state, ServerState::Ok)) + .unwrap_or_default() + .then_some(server_id) + }) + .collect::>() + .collect::>() + .await + .into_iter() + .flatten() + .collect::>(); + + // Select server based on building count modulo available length. + let index = building % available_server_ids.len(); + let server = + resource::get::(available_server_ids[index]).await?; let periphery = periphery_client(&server).await?; Ok((periphery, BuildCleanupData::Server)) } - BuilderConfig::Aws(config) => { - get_aws_builder(&resource_name, version, config, update).await - } } } @@ -89,14 +180,14 @@ pub async fn connect_builder_periphery( skip_all, fields( resource_name, - update_id = update.id, + update_id = update.as_ref().map(|u| u.id.as_str()), ) )] async fn get_aws_builder( resource_name: &str, version: Option, config: AwsBuilderConfig, - update: &mut Update, + mut update: Option<&mut Update>, ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { let start_create_ts = komodo_timestamp(); @@ -114,9 +205,10 @@ async fn get_aws_builder( ..Default::default() }; - update.logs.push(log); - - update_update(update.clone()).await?; + if let Some(update) = &mut update { + update.logs.push(log); + update_update((*update).clone()).await?; + } let protocol = if config.use_https { "wss" } else { "ws" }; @@ -153,8 +245,10 @@ async fn get_aws_builder( end_ts: komodo_timestamp(), ..Default::default() }; - update.logs.push(connect_log); - update_update(update.clone()).await?; + if let Some(update) = update { + update.logs.push(connect_log); + update_update(update.clone()).await?; + } return Ok(( periphery, BuildCleanupData::Aws { diff --git a/bin/core/src/resource/builder.rs b/bin/core/src/resource/builder.rs index ebee6399d3..ebc30d5152 100644 --- a/bin/core/src/resource/builder.rs +++ b/bin/core/src/resource/builder.rs @@ -54,7 +54,7 @@ impl super::KomodoResource for Builder { } BuilderConfig::Server(config) => ( BuilderConfigVariant::Server.to_string(), - Some(config.server_id), + Some(config.server_ids.join(", ")), ), BuilderConfig::Aws(config) => ( BuilderConfigVariant::Aws.to_string(), @@ -184,10 +184,11 @@ async fn validate_config( config: &mut PartialBuilderConfig, user: &User, ) -> anyhow::Result<()> { - match config { - PartialBuilderConfig::Server(PartialServerBuilderConfig { - server_id: Some(server_id), - }) if !server_id.is_empty() => { + if let PartialBuilderConfig::Server(PartialServerBuilderConfig { + server_ids: Some(server_ids), + }) = config + { + for server_id in server_ids { let server = super::get_check_permissions::( server_id, user, @@ -196,7 +197,6 @@ async fn validate_config( .await?; *server_id = server.id; } - _ => {} } Ok(()) } diff --git a/bin/core/src/startup.rs b/bin/core/src/startup.rs index 9c778f0fea..530bcd2fcb 100644 --- a/bin/core/src/startup.rs +++ b/bin/core/src/startup.rs @@ -281,7 +281,7 @@ async fn ensure_first_server_and_builder() { name: name.to_string(), config: PartialBuilderConfig::Server( PartialServerBuilderConfig { - server_id: Some(server.id), + server_ids: Some(vec![server.id]), }, ), }) diff --git a/bin/core/src/sync/resources.rs b/bin/core/src/sync/resources.rs index ddba0cf0f5..9b5ae695ea 100644 --- a/bin/core/src/sync/resources.rs +++ b/bin/core/src/sync/resources.rs @@ -210,14 +210,16 @@ impl ResourceSyncTrait for Builder { mut original: Self::Config, update: Self::PartialConfig, ) -> anyhow::Result { - // need to replace server builder id with name + // need to replace builder server ids with names if let BuilderConfig::Server(config) = &mut original { let all = all_resources_cache().load(); - config.server_id = all - .servers - .get(&config.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); + for server_id in &mut config.server_ids { + *server_id = all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } } Ok(original.partial_diff(update)) diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index 1b0eb50254..d9992043de 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -433,13 +433,13 @@ impl ToToml for Builder { fn replace_ids(resource: &mut Resource) { if let BuilderConfig::Server(config) = &mut resource.config { let all = all_resources_cache().load(); - config.server_id.clone_from( - all + for server_id in &mut config.server_ids { + *server_id = all .servers - .get(&config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ) + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } } } diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index 46e0f235c7..78d7296bc2 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -311,7 +311,9 @@ impl MergePartial for BuilderConfig { PartialBuilderConfig::Server(partial) => match self { BuilderConfig::Server(config) => { let config = ServerBuilderConfig { - server_id: partial.server_id.unwrap_or(config.server_id), + server_ids: partial + .server_ids + .unwrap_or(config.server_ids), }; BuilderConfig::Server(config) } @@ -456,14 +458,27 @@ pub type _PartialServerBuilderConfig = PartialServerBuilderConfig; #[diff_derive(Serialize, Deserialize, Debug, Clone, Default)] #[partial(skip_serializing_none, from, diff)] pub struct ServerBuilderConfig { - /// The server id of the builder - #[serde(default, alias = "server")] - #[partial_attr(serde(alias = "server"))] + /// The server ids of the builders. + /// If multiple are given, builds will overflow + /// to later specified servers as needed. + #[serde( + default, + alias = "server_id", + alias = "server", + alias = "servers", + deserialize_with = "string_list_deserializer" + )] + #[partial_attr(serde( + alias = "server_id", + alias = "server", + alias = "servers", + deserialize_with = "option_string_list_deserializer" + ))] #[cfg_attr( feature = "schemars", - partial_attr(schemars(rename = "server")) + partial_attr(schemars(rename = "servers")) )] - pub server_id: String, + pub server_ids: Vec, } impl ServerBuilderConfig { diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 6bbafe8f32..31665f7433 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -10024,8 +10024,12 @@ export interface SendAlert { /** Configuration for a Komodo Server Builder. */ export interface ServerBuilderConfig { - /** The server id of the builder */ - server_id?: string; + /** + * The server ids of the builders. + * If multiple are given, builds will overflow + * to later specified servers as needed. + */ + server_ids?: string[]; } /** The health of a part of the server. */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index fc73d82341..2add2b1a31 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -9486,8 +9486,12 @@ export interface SendAlert { } /** Configuration for a Komodo Server Builder. */ export interface ServerBuilderConfig { - /** The server id of the builder */ - server_id?: string; + /** + * The server ids of the builders. + * If multiple are given, builds will overflow + * to later specified servers as needed. + */ + server_ids?: string[]; } /** The health of a part of the server. */ export interface ServerHealthState { diff --git a/ui/src/resources/builder/config/server.tsx b/ui/src/resources/builder/config/server.tsx index d033190e61..00afe066db 100644 --- a/ui/src/resources/builder/config/server.tsx +++ b/ui/src/resources/builder/config/server.tsx @@ -1,11 +1,11 @@ import { usePermissions, useRead, useWrite } from "@/lib/hooks"; -import ResourceLink from "@/resources/link"; import ResourceSelector from "@/resources/selector"; import { Config } from "mogh_ui"; import { ConfigItem } from "mogh_ui"; -import { Group } from "@mantine/core"; +import { ActionIcon, Button, Group } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; +import { ICONS } from "@/lib/icons"; export default function ServerBuilderConfig({ id }: { id: string }) { const { canWrite } = usePermissions({ type: "Builder", id }); @@ -36,33 +36,71 @@ export default function ServerBuilderConfig({ id }: { id: string }) { label: "Server", labelHidden: true, fields: { - server_id: (serverId, set) => { + server_ids: (serverIds, set) => { return ( - Server: - + {serverIds?.map((serverId, index) => { + return ( + + + set({ + server_ids: [ + ...serverIds.map((id, i) => + i === index ? server_id : id, + ), + ], + }) + } + disabled={disabled} + targetProps={{ w: "90%", maw: "" }} + clearable={false} /> + {!disabled && ( + + + set({ + server_ids: [ + ...serverIds?.filter( + (_, i) => i !== index, + ), + ], + }) + } + /> + + )} - ) : ( - "Select Server" - ) - } - description="Select the Server to build on." - > - set({ server_id })} - disabled={disabled} - clearable - /> + ); + })} + {!disabled && ( + + )} ); }, From 0ad94bfe33cbd79363fbc728f842302679872ff2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 22:29:58 -0700 Subject: [PATCH 044/203] deploy 2.3.0-dev-12 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82d7e1bf3b..a69a99d1cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 630e0ea9fa..5c475df700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-11" +version = "2.3.0-dev-12" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 091f2d5ec1f2214af871a83d920f180839c459a8 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 22:42:00 -0700 Subject: [PATCH 045/203] fix server type builder config comment --- ui/src/resources/builder/config/server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/resources/builder/config/server.tsx b/ui/src/resources/builder/config/server.tsx index 00afe066db..4e1cbad1b7 100644 --- a/ui/src/resources/builder/config/server.tsx +++ b/ui/src/resources/builder/config/server.tsx @@ -40,7 +40,7 @@ export default function ServerBuilderConfig({ id }: { id: string }) { return ( {serverIds?.map((serverId, index) => { From 77cdd11c51c6e3f78ed9c91d2b8d94413d4b0e37 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 22:50:08 -0700 Subject: [PATCH 046/203] fix builder toml server_ids => server/s --- bin/core/src/sync/toml.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index d9992043de..95727e0151 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -476,8 +476,14 @@ impl ToToml for Builder { .into_iter() .map(|(key, value)| { match key.as_str() { - "server_id" => { - return (String::from("server"), value); + "server_ids" => { + if let serde_json::Value::Array(arr) = &value + && arr.len() > 1 + { + return (String::from("servers"), value); + } else { + return (String::from("server"), value); + } } _ => {} } From 2d10b9e79bddde53a1596e7d831e90e731b68110 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:25:38 -0700 Subject: [PATCH 047/203] sync: fix alerter resources / except_resources using ids instead of names --- bin/core/src/api/read/toml.rs | 5 +- bin/core/src/sync/deploy.rs | 8 +- bin/core/src/sync/execute.rs | 2 +- bin/core/src/sync/mod.rs | 15 +- bin/core/src/sync/replace_ids.rs | 252 +++++++++++++++++++++++++++++++ bin/core/src/sync/resources.rs | 230 ++-------------------------- bin/core/src/sync/toml.rs | 176 +-------------------- bin/core/src/sync/view.rs | 6 +- 8 files changed, 292 insertions(+), 402 deletions(-) create mode 100644 bin/core/src/sync/replace_ids.rs diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index 66901f21f4..d12eab179d 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -28,6 +28,7 @@ use crate::{ resource, state::db_client, sync::{ + replace_ids::ReplaceIds, toml::{ToToml, convert_resource}, user_groups::{convert_user_groups, user_group_to_toml}, variables::variable_to_toml, @@ -142,7 +143,7 @@ impl Resolve for ExportResourcesToToml { PermissionLevel::Read.into(), ) .await?; - $Type::replace_ids(&mut resource); + $Type::replace_ids(&mut resource.config); let (deploy, after) = existing .as_ref() .and_then(|e| { @@ -200,7 +201,7 @@ impl Resolve for ExportResourcesToToml { || !sync.config.repo.is_empty() || !sync.config.linked_repo.is_empty()) { - ResourceSync::replace_ids(&mut sync); + ResourceSync::replace_ids(&mut sync.config); res.resource_syncs.push(convert_resource::( sync, false, diff --git a/bin/core/src/sync/deploy.rs b/bin/core/src/sync/deploy.rs index 3eec59b3e3..7b6fb3eb03 100644 --- a/bin/core/src/sync/deploy.rs +++ b/bin/core/src/sync/deploy.rs @@ -338,9 +338,7 @@ fn build_cache_for_deployment<'a>( // Makes sure things that aren't defined in toml (come through as None) actually get removed. let config: DeploymentConfig = deployment.config.clone().into(); - let mut config: PartialDeploymentConfig = config.into(); - - Deployment::validate_partial_config(&mut config); + let config: PartialDeploymentConfig = config.into(); let mut diff = Deployment::get_diff(original.config.clone(), config)?; @@ -572,9 +570,7 @@ fn build_cache_for_stack<'a>( // Merge toml resource config (partial) onto default resource config. // Makes sure things that aren't defined in toml (come through as None) actually get removed. let config: StackConfig = stack.config.clone().into(); - let mut config: PartialStackConfig = config.into(); - - Stack::validate_partial_config(&mut config); + let config: PartialStackConfig = config.into(); let mut diff = Stack::get_diff(original.config.clone(), config)?; diff --git a/bin/core/src/sync/execute.rs b/bin/core/src/sync/execute.rs index 4f9e527180..1b09bf5535 100644 --- a/bin/core/src/sync/execute.rs +++ b/bin/core/src/sync/execute.rs @@ -74,7 +74,7 @@ pub async fn get_updates_for_execution< let config: Resource::Config = resource.config.into(); resource.config = config.into(); - Resource::validate_partial_config(&mut resource.config); + // Resource::validate_partial_config(&mut resource.config); let mut diff = Resource::get_diff( original.config.clone(), diff --git a/bin/core/src/sync/mod.rs b/bin/core/src/sync/mod.rs index fb59471ffa..978558165d 100644 --- a/bin/core/src/sync/mod.rs +++ b/bin/core/src/sync/mod.rs @@ -7,6 +7,7 @@ use komodo_client::entities::{ tag::Tag, toml::{ResourceToml, ResourcesToml}, }; +use partial_derive2::PartialDiff as _; use toml::ToToml; use crate::resource::KomodoResource; @@ -15,6 +16,7 @@ pub mod deploy; pub mod execute; pub mod file; pub mod remote; +pub mod replace_ids; pub mod resources; pub mod toml; pub mod user_groups; @@ -87,16 +89,19 @@ pub trait ResourceSyncTrait: ToToml + Sized { ) } - /// Apply any changes to incoming toml partial config - /// before it is diffed against existing config - fn validate_partial_config(_config: &mut Self::PartialConfig) {} + // /// Apply any changes to incoming toml partial config + // /// before it is diffed against existing config + // fn validate_partial_config(_config: &mut Self::PartialConfig) {} /// Diffs the declared toml (partial) against the full existing config. /// Removes all fields from toml (partial) that haven't changed. fn get_diff( - original: Self::Config, + mut original: Self::Config, update: Self::PartialConfig, - ) -> anyhow::Result; + ) -> anyhow::Result { + Self::replace_ids(&mut original); + Ok(original.partial_diff(update)) + } /// Apply any changes to computed config diff /// before logging diff --git a/bin/core/src/sync/replace_ids.rs b/bin/core/src/sync/replace_ids.rs new file mode 100644 index 0000000000..c390896040 --- /dev/null +++ b/bin/core/src/sync/replace_ids.rs @@ -0,0 +1,252 @@ +use komodo_client::entities::{ + action::Action, + alerter::Alerter, + build::Build, + builder::{Builder, BuilderConfig}, + deployment::{Deployment, DeploymentImage}, + procedure::Procedure, + repo::Repo, + server::Server, + stack::Stack, + swarm::Swarm, + sync::ResourceSync, +}; + +use crate::{ + helpers::procedure::replace_procedure_stage_ids_with_names, + resource::KomodoResource, state::all_resources_cache, +}; + +/// Trait implementing replacing of linked resource ids with names for Resource Sync TOML. +pub trait ReplaceIds: KomodoResource { + /// Replace linked ids (server_id, build_id, etc) with the resource name. + fn replace_ids(_config: &mut Self::Config) {} +} + +// These have no linked resource ids to replace +impl ReplaceIds for Server {} +impl ReplaceIds for Action {} + +impl ReplaceIds for ResourceSync { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Swarm { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.server_ids.iter_mut().for_each(|server_id| { + *server_id = all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + }); + let mut res = Vec::with_capacity(config.server_ids.capacity()); + for server_id in &config.server_ids { + res.push( + all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(), + ); + } + } +} + +impl ReplaceIds for Stack { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.swarm_id.clone_from( + all + .swarms + .get(&config.swarm_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Deployment { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.swarm_id.clone_from( + all + .swarms + .get(&config.swarm_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + if let DeploymentImage::Build { build_id, .. } = &mut config.image + { + build_id.clone_from( + all + .builds + .get(build_id) + .map(|b| &b.name) + .unwrap_or(&String::new()), + ); + } + } +} + +impl ReplaceIds for Build { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.builder_id.clone_from( + all + .builders + .get(&config.builder_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Repo { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + config.builder_id.clone_from( + all + .builders + .get(&config.builder_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Builder { + fn replace_ids(config: &mut Self::Config) { + if let BuilderConfig::Server(config) = config { + let all = all_resources_cache().load(); + for server_id in &mut config.server_ids { + *server_id = all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } + } + } +} + +impl ReplaceIds for Procedure { + fn replace_ids(config: &mut Self::Config) { + replace_procedure_stage_ids_with_names(&mut config.stages); + } +} + +/// Replaces the inner ids of [ResourceTarget] variants with the +/// referenced resource's name, looking each up in [AllResourcesById]. +/// The `System` variant carries no id and is a no-op. +macro_rules! replace_resource_target_ids { + ($target:expr, $all:expr, { $( $variant:ident => $field:ident ),* $(,)? }) => { + match $target { + komodo_client::entities::ResourceTarget::System(_) => {} + $( + komodo_client::entities::ResourceTarget::$variant(id) => { + *id = $all + .$field + .get(id) + .map(|r| r.name.clone()) + .unwrap_or_default(); + } + )* + } + }; +} + +impl ReplaceIds for Alerter { + fn replace_ids(config: &mut Self::Config) { + if config.resources.is_empty() + && config.except_resources.is_empty() + { + return; + } + + let all = all_resources_cache().load(); + + for resource in &mut config.resources { + replace_resource_target_ids!(resource, all, { + Swarm => swarms, + Server => servers, + Stack => stacks, + Deployment => deployments, + Build => builds, + Repo => repos, + Procedure => procedures, + Action => actions, + Builder => builders, + Alerter => alerters, + ResourceSync => syncs, + }); + } + for resource in &mut config.except_resources { + replace_resource_target_ids!(resource, all, { + Swarm => swarms, + Server => servers, + Stack => stacks, + Deployment => deployments, + Build => builds, + Repo => repos, + Procedure => procedures, + Action => actions, + Builder => builders, + Alerter => alerters, + ResourceSync => syncs, + }); + } + } +} diff --git a/bin/core/src/sync/resources.rs b/bin/core/src/sync/resources.rs index 9b5ae695ea..ee3b7f2471 100644 --- a/bin/core/src/sync/resources.rs +++ b/bin/core/src/sync/resources.rs @@ -2,28 +2,16 @@ use std::collections::HashMap; use formatting::{Color, bold, colored, muted}; use komodo_client::entities::{ - ResourceTargetVariant, - action::Action, - alerter::Alerter, - build::Build, - builder::{Builder, BuilderConfig}, - deployment::{Deployment, DeploymentImage}, - procedure::Procedure, - repo::Repo, - server::Server, - stack::Stack, - swarm::Swarm, - sync::ResourceSync, - tag::Tag, - update::Log, + ResourceTargetVariant, action::Action, alerter::Alerter, + build::Build, builder::Builder, deployment::Deployment, + procedure::Procedure, repo::Repo, server::Server, stack::Stack, + swarm::Swarm, sync::ResourceSync, tag::Tag, update::Log, user::sync_user, }; -use partial_derive2::{MaybeNone, PartialDiff}; +use partial_derive2::MaybeNone; use crate::{ - helpers::procedure::replace_procedure_stage_ids_with_names, resource::{KomodoResource, ResourceMetaUpdate}, - state::all_resources_cache, sync::{ToUpdateItem, execute::run_update_meta}, }; @@ -33,127 +21,19 @@ use super::{ include_resource_by_tags, }; -impl ResourceSyncTrait for Server { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Server {} impl ExecuteResourceSync for Server {} -impl ResourceSyncTrait for Swarm { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.server_ids.iter_mut().for_each(|server_id| { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - }); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Swarm {} impl ExecuteResourceSync for Swarm {} -impl ResourceSyncTrait for Deployment { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.swarm_id = all - .swarms - .get(&original.swarm_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - if let DeploymentImage::Build { build_id, version } = - &original.image - { - original.image = DeploymentImage::Build { - build_id: all - .builds - .get(build_id) - .map(|b| b.name.clone()) - .unwrap_or_default(), - version: *version, - }; - } - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Deployment {} impl ExecuteResourceSync for Deployment {} -impl ResourceSyncTrait for Stack { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.swarm_id = all - .swarms - .get(&original.swarm_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Stack {} impl ExecuteResourceSync for Stack {} impl ResourceSyncTrait for Build { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - original.builder_id = all - .builders - .get(&original.builder_id) - .map(|b| b.name.clone()) - .unwrap_or_default(); - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } - fn validate_diff(diff: &mut Self::ConfigDiff) { if let Some((_, to)) = &diff.version { // When setting a build back to "latest" version, @@ -165,78 +45,18 @@ impl ResourceSyncTrait for Build { } } } - impl ExecuteResourceSync for Build {} -impl ResourceSyncTrait for Repo { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - // Need to replace server id with name - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - // Need to replace builder id with name - original.builder_id = all - .builders - .get(&original.builder_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Repo {} impl ExecuteResourceSync for Repo {} -impl ResourceSyncTrait for Alerter { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Alerter {} impl ExecuteResourceSync for Alerter {} -impl ResourceSyncTrait for Builder { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - // need to replace builder server ids with names - if let BuilderConfig::Server(config) = &mut original { - let all = all_resources_cache().load(); - for server_id in &mut config.server_ids { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - } - } - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Builder {} impl ExecuteResourceSync for Builder {} -impl ResourceSyncTrait for Action { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Action {} impl ExecuteResourceSync for Action {} impl ResourceSyncTrait for ResourceSync { @@ -318,33 +138,11 @@ impl ResourceSyncTrait for ResourceSync { // The sync must be files on host mode OR git repo mode (files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty() || !config.linked_repo.as_deref().unwrap_or_default().is_empty()) } - - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } } impl ExecuteResourceSync for ResourceSync {} -impl ResourceSyncTrait for Procedure { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - replace_procedure_stage_ids_with_names(&mut original.stages); - Ok(original.partial_diff(update)) - } -} +impl ResourceSyncTrait for Procedure {} impl ExecuteResourceSync for Procedure { async fn execute_sync_updates( diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index 95727e0151..45c9bc1ebf 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -6,7 +6,7 @@ use komodo_client::entities::{ action::Action, alerter::Alerter, build::Build, - builder::{Builder, BuilderConfig, PartialBuilderConfig}, + builder::{Builder, PartialBuilderConfig}, deployment::{Deployment, DeploymentImage}, procedure::Procedure, repo::Repo, @@ -21,8 +21,7 @@ use komodo_client::entities::{ use partial_derive2::{MaybeNone, PartialDiff}; use crate::{ - helpers::procedure::replace_procedure_stage_ids_with_names, - resource::KomodoResource, state::all_resources_cache, + resource::KomodoResource, sync::replace_ids::ReplaceIds, }; pub const TOML_PRETTY_OPTIONS: toml_pretty::Options = @@ -35,11 +34,7 @@ pub const TOML_PRETTY_OPTIONS: toml_pretty::Options = inline_array: false, }; -pub trait ToToml: KomodoResource { - /// Replace linked ids (server_id, build_id, etc) with the resource name. - fn replace_ids(_resource: &mut Resource) { - } - +pub trait ToToml: ReplaceIds { fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -108,7 +103,7 @@ pub fn resource_push_to_toml( toml: &mut String, all_tags: &HashMap, ) -> anyhow::Result<()> { - R::replace_ids(&mut resource); + R::replace_ids(&mut resource.config); if !toml.is_empty() { toml.push_str("\n\n##\n\n"); } @@ -157,48 +152,12 @@ pub fn convert_resource( } } -// These have no linked resource ids to replace -impl ToToml for Alerter {} impl ToToml for Server {} impl ToToml for Action {} - -impl ToToml for ResourceSync { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } -} +impl ToToml for Alerter {} +impl ToToml for ResourceSync {} impl ToToml for Swarm { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.server_ids.iter_mut().for_each(|server_id| { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - }); - let mut res = - Vec::with_capacity(resource.config.server_ids.capacity()); - for server_id in &resource.config.server_ids { - res.push( - all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(), - ); - } - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -220,34 +179,6 @@ impl ToToml for Swarm { } impl ToToml for Stack { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.swarm_id.clone_from( - all - .swarms - .get(&resource.config.swarm_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -268,38 +199,6 @@ impl ToToml for Stack { } impl ToToml for Deployment { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.swarm_id.clone_from( - all - .swarms - .get(&resource.config.swarm_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - if let DeploymentImage::Build { build_id, .. } = - &mut resource.config.image - { - build_id.clone_from( - all - .builds - .get(build_id) - .map(|b| &b.name) - .unwrap_or(&String::new()), - ); - } - } - fn edit_config_object( resource: &ResourceToml, config: IndexMap, @@ -341,24 +240,6 @@ impl ToToml for Deployment { } impl ToToml for Build { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.builder_id.clone_from( - all - .builders - .get(&resource.config.builder_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( resource: &ResourceToml, config: IndexMap, @@ -391,24 +272,6 @@ impl ToToml for Build { } impl ToToml for Repo { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - resource.config.builder_id.clone_from( - all - .builders - .get(&resource.config.builder_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -430,19 +293,6 @@ impl ToToml for Repo { } impl ToToml for Builder { - fn replace_ids(resource: &mut Resource) { - if let BuilderConfig::Server(config) = &mut resource.config { - let all = all_resources_cache().load(); - for server_id in &mut config.server_ids { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - } - } - } - fn push_additional( resource: ResourceToml, toml: &mut String, @@ -477,13 +327,7 @@ impl ToToml for Builder { .map(|(key, value)| { match key.as_str() { "server_ids" => { - if let serde_json::Value::Array(arr) = &value - && arr.len() > 1 - { - return (String::from("servers"), value); - } else { - return (String::from("server"), value); - } + return (String::from("servers"), value); } _ => {} } @@ -502,12 +346,6 @@ impl ToToml for Builder { } impl ToToml for Procedure { - fn replace_ids(resource: &mut Resource) { - replace_procedure_stage_ids_with_names( - &mut resource.config.stages, - ); - } - fn push_to_toml_string( mut resource: ResourceToml, toml: &mut String, diff --git a/bin/core/src/sync/view.rs b/bin/core/src/sync/view.rs index be6f59c252..d6dcea8e50 100644 --- a/bin/core/src/sync/view.rs +++ b/bin/core/src/sync/view.rs @@ -84,9 +84,9 @@ pub async fn push_updates_for_view( proposed_resource.config.into(); proposed_resource.config = propsed_config.into(); - Resource::validate_partial_config( - &mut proposed_resource.config, - ); + // Resource::validate_partial_config( + // &mut proposed_resource.config, + // ); let proposed = super::toml::resource_toml_to_toml_string::< Resource, From 6308df958645204a88c524ec05188210608dcd91 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:40:46 -0700 Subject: [PATCH 048/203] fix multi server builder resource page header --- ui/src/resources/builder/index.tsx | 50 ++++++++++++++-------- ui/src/resources/builder/instance-type.tsx | 8 ++-- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index 9ca55a67d9..178f6bab88 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -5,7 +5,6 @@ import { Types } from "komodo_client"; import ResourceLink from "@/resources/link"; import NewResource from "@/resources/new"; import BuilderTable from "./table"; -import { useServer } from "../server"; import { serverStateIntention } from "@/lib/color"; import ResourceHeader from "../header"; import BuilderConfig from "./config"; @@ -77,20 +76,31 @@ export const BuilderComponents: RequiredResourceComponents< ResourcePageHeader: ({ id }) => { const builder = useBuilder(id); - const server = useServer( - builder?.info.builder_type === "Server" - ? builder.info.instance_type - : undefined, + const configured = + (builder?.info.builder_type === "Server" && + builder.info.instance_type?.split(",").map((id) => id.trim())) || + []; + const servers = useRead("ListServers", {}).data?.filter((s) => + configured.includes(s.id), ); const coreVersion = useRead("GetVersion", {}).data?.version; - const intent = server?.info.state - ? serverStateIntention( - server.info.state, - !!coreVersion && - !!server.info.version && - coreVersion !== server.info.version, - ) - : "Neutral"; + const intent = !servers?.length + ? "Neutral" + : servers.length === 1 + ? serverStateIntention( + servers[0].info.state, + !!coreVersion && + !!servers[0].info.version && + coreVersion !== servers[0].info.version, + ) + : // All servers connected + servers.every((s) => s.info.state === Types.ServerState.Ok) + ? "Good" + : // No servers connected + servers.every((s) => s.info.state !== Types.ServerState.Ok) + ? "Critical" + : // Some servers connected + "Warning"; return ( - ) : undefined + builder?.info.builder_type === "Aws" + ? builder?.info.instance_type + : builder?.info.builder_type === "Server" + ? builder?.info.instance_type + ?.split(",") + .slice(0, 3) + .map((id) => ) + : undefined } /> ); diff --git a/ui/src/resources/builder/instance-type.tsx b/ui/src/resources/builder/instance-type.tsx index ee02cabeff..f3e77211b1 100644 --- a/ui/src/resources/builder/instance-type.tsx +++ b/ui/src/resources/builder/instance-type.tsx @@ -6,11 +6,9 @@ import { Group } from "@mantine/core"; export default function BuilderInstanceType({ id }: { id: string }) { let info = useBuilder(id)?.info; if (info?.builder_type === "Server") { - return ( - info.instance_type && ( - - ) - ); + return info.instance_type + ?.split(",") + .map((id) => ); } else { return ( From 6f88586edd9715e79af8759bba51dc8adfcfb46e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:41:13 -0700 Subject: [PATCH 049/203] deploy 2.3.0-dev-13 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a69a99d1cc..70089420f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5c475df700..c57876b06e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-12" +version = "2.3.0-dev-13" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 67ea0ecce1216270fd4ead04777ff395247fb612 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:49:25 -0700 Subject: [PATCH 050/203] fix builder resource header with multiple servers styling --- ui/src/resources/builder/index.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index 178f6bab88..07e1ddc677 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -10,7 +10,7 @@ import ResourceHeader from "../header"; import BuilderConfig from "./config"; import BatchExecutions from "@/components/batch-executions"; import { useState } from "react"; -import { Select } from "@mantine/core"; +import { Group, Select } from "@mantine/core"; export function useBuilder(id: string | undefined, useName?: boolean) { return useRead("ListBuilders", {}).data?.find((r) => @@ -111,14 +111,18 @@ export const BuilderComponents: RequiredResourceComponents< name={builder?.name} state={builder?.info.builder_type} status={ - builder?.info.builder_type === "Aws" - ? builder?.info.instance_type - : builder?.info.builder_type === "Server" - ? builder?.info.instance_type - ?.split(",") - .slice(0, 3) - .map((id) => ) - : undefined + builder?.info.builder_type === "Aws" ? ( + builder?.info.instance_type + ) : builder?.info.builder_type === "Server" ? ( + + {builder?.info.instance_type + ?.split(",") + .slice(0, 3) + .map((id) => ( + + ))} + + ) : undefined } /> ); From 5ab736154d03e69bd033cac92f6c7cb9cb6f8445 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:52:19 -0700 Subject: [PATCH 051/203] run dkf -> dku --- action/{deploy-fe.ts => deploy-ui.ts} | 2 +- runfile.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename action/{deploy-fe.ts => deploy-ui.ts} (51%) diff --git a/action/deploy-fe.ts b/action/deploy-ui.ts similarity index 51% rename from action/deploy-fe.ts rename to action/deploy-ui.ts index 8732a40bd0..1f3311e875 100644 --- a/action/deploy-fe.ts +++ b/action/deploy-ui.ts @@ -1,4 +1,4 @@ -const cmd = "km run -y action deploy-komodo-fe-change"; +const cmd = "km run -y action deploy-komodo-ui-change"; new Deno.Command("bash", { args: ["-c", cmd], }).spawn(); \ No newline at end of file diff --git a/runfile.toml b/runfile.toml index 07b76e26ad..8ef787f5d5 100644 --- a/runfile.toml +++ b/runfile.toml @@ -72,9 +72,9 @@ cmd = "cargo doc --no-deps -p komodo_client && http-server -p 8050 target/doc" alias = "dk" cmd = "deno run --allow-all action/deploy.ts" -[deploy-komodo-fe-only] -alias = "dkf" -cmd = "deno run --allow-all action/deploy-fe.ts" +[deploy-komodo-ui-only] +alias = "dku" +cmd = "deno run --allow-all action/deploy-ui.ts" [build-komodo] alias = "bk" From 9c61ebd4a8ac1eb25ec517b30794c2f55fe99fb1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 28 Jun 2026 23:59:29 -0700 Subject: [PATCH 052/203] white background favicon + gen resources schema --- ui/index.html | 1 - ui/public/favicon-96x96.png | Bin 2457 -> 0 bytes ui/public/favicon.ico | Bin 15086 -> 15086 bytes ui/public/favicon.svg | 85 ++++++-------------------------- ui/public/schema/resources.json | 21 ++++++-- 5 files changed, 31 insertions(+), 76 deletions(-) delete mode 100644 ui/public/favicon-96x96.png diff --git a/ui/index.html b/ui/index.html index 5971c5936f..580ff00550 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,6 @@ - diff --git a/ui/public/favicon-96x96.png b/ui/public/favicon-96x96.png deleted file mode 100644 index f248e8eff4ed2c632432c8f1481117921a1705a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2457 zcmV;K31;?*P)Px;Q%OWYRCr$PUE6ZoHV_5Xd`_HawJ(t}{V&`9)rpmvPV48`5BZpm*e(UoAhp;% z3ob8lrI*Y&6u3BNF9*PqYQpWaDgw=_cfZ{{+z43N32p@32*45ec-SB4>fJ7M|DMm` z#C`kq^vwPpzteJ8ZZZOLpxZWlj&O9K8$PF|3E$}Y?_ZxE4BuPy(#;T%KyR9GAHv1M zwND!YT*R6tJZ_PQRS6)_BjAMf8|W5fHI_#IPeg1fDpnyN10M2pBgW9>TZxE839tiC z=21BCACEv;n}|gSume9M;>^%%k}x{~pT6uL+O{jKoBp(IIIbaIXCuJu_1F2N)ADT_ z9{&j;{3vMqq@Kbq9Cnf7TEf>LVHN@+_)irsej1xp%SItp9~xI65hLW3=$2&^TGpCKw2jRH3U?v8s(IR{W0&g$`{ zNI*$gG74M>NZ=!ha7nM{?9oVw4zySlOeVkp{wiM2k$~9z^7O1?^YQon;q{McJFn_Z zB(Fv$vUILTq7W&{Xd0rirfl+66M#%2%D`0?I1&+v?I;11(&&7mmV_z-#uF$NUUU+O z*Lq!pfNzaj$^^ACx{83i`$xynnj$MfG+R1;UJxatgUp$ zxSF>`LI_9^&hHcJA$`xZk~R^5gzwtmdmLxh%pu8JOXPH^&XQZT@U1c2a)0UC8ZR%6 zXZ8LxPEv)I@52OW-oF<3oD{y4CHXL?1isaS^KnuXSg?Qz(7fNmx`)>*9?!L>e5|WDMA}3^5s(H^b&-QASm?O9Fwh)SBtT0)+Tde@hvN-BI+0np zXICJb5a__i`_mtTW=&#rSoE!Se4{mUMr5*&7>OhHrqWrYVGOiCNOSP8;)&>Y5kjKJfN{x0(rD<33WV zX@U(~Am9xGX9Btul=^BewWm2qk0qJNkT32JU{YHN7XdY*gHu8DHz)#TSOIrUyBroY zxwOBZjMGxqWgPIhk&lL}lP@BGx*52|rzZhi{k%#+GY}va^o!Rur`2mh$H?+qD1u%;Oipc))SoHg264-7HW;G0|pniB$1W3^o3Ajhs z6`#KR_RzLxb2Oj;()j1|c_2l}S9X=JIN&1RBLYkyLLoLe z9l2`BR!9YD6Sc_DQZYr-Paf7+c7b{ywKtQZSA`fIBiUP;w+jKNN0qd2Cn7KMlO}5N zF7$zrj5v7wQBUb0)uCz3g#cyq4DR@f%#Jfs>rZ;&=t+=d%}t z8X~2i+HQj-2#{s-hojcWW!=)4Z994n0;H(oBFHOuTT+^J-ZBA&rM#wGq?K$|wr3fm0k0n40b!lS2HUeB2Ij`1iOW)Ce;>J!&i0Tt_Fw=Ef(BDh**JL;M zDhAV`ytJp0CWQ3=pmn5Fn9KLLtOKSjpz?X-8-(&1o!s!36R zqzY^L;kFX;R7>8iy-dWKe$u`!>u6f2a}!KPN=ksjIPcj<sV8+7gb*y9>1T;FOC%>Kubewcv{K(YE;h@&_~j^IvuLr_KK>3qrhvjl6>@z zFvRAP_b0`ui2;Ow;+ZZPxP>SnCt1S|ZOwy3rAmO7hO9f9h*w#!S&M+89FFd?2x<)&$t|%UZ=oK{O@Q|{h=~Ai_U8u6dwebL*VIE30kIZj zQNXtdu3415K~Lk&0lujQ7*mlc56%Ue*SHAywdvO!0ZDlv4^_N}NC3fyRnOQdstEYM zkbo52n!KNrP)z_0f_g`7czUG}>o`$tKu^+7jq*tZ7)kgk>VmBFhN5FC_|-*t>`@=T z>>nEDXG*xH3J;^L9*tiA>9BujFTZ1^e!S)}IRY#)ZIJt#-Q)MKe{JFy?&OqnEw#PC zcZ+~}64w1uwKDMKN1!~xcPGGF6B2RhF`}h5(=0_ckJ>pl9Nkh-ZYZ*4QA)CNkde-$ zEx%JavTwGH##socO-T$XgSQ+aM~(^XsLvrODOKEN=2|r?0oj6WG7zdP@xQ?3MXs7z zvlHML4cc7X{@=UMiV@~!H8{{<^$b2{1P+Zqz+D({N#J_{f=|x(O7=Ng>+9Snn{<(z zYXHAG0i)Dpc_U&4?5wU|B*$yUE>;6HL(&?8{;mBnPURl`Yby~oh1lzH&87&*CzKj| zBO%rZ>0ik0_2Zx(U~^+^mH=eJx#r)JS0lD>rUqDyeUI1v)DiF=2)PlkGK)6?Rwm$I XML^`ciCYAT00000NkvXXu0mjfm0pXr diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico index 1c900b85f3ba4aaec30296def7d2bc2ca9c973f5..ee480c56b409b7fdfab314c6633f8d022a054a57 100644 GIT binary patch literal 15086 zcmds;X^>UL6@Z_bDyveOn5z6Rzog1aRr14V%ETB6qNps&F6hW041$O#3IaNc!XS&t zA}GkBCwDjOF!4uDO;6wMK3m`0 zefsq2ewoZ!nR7CiU6zr&R%YfOGnp$gnM|!(+42RM%ucDhKyuo<{3??fEC_!R93jb+ zNus_cz!-C}q;g41C2f+lSJIgRJmB?~q@I#4t_fXT9>TMYq>m;2Qh=;dSBKMi79RB^ zZ7x*l?K`%ag>TF=&rf^6@QGU0XJ8!1>_UMsR-q}WUsE~A4A^i}%^CZ14DHYy$ z>vdClN6WnW=IaxEDe$|X)34L|Anj*MTAZf%t}l0*j$KR7SZ}{y-5=~?4jkN9P$%CoUrehxiS3d!KQJmdO<$dH@?ce zxpYwuy;m_Lq`%1>xI({Dn z`<)O4@2+c)J96ao>EF{-eYGn@QdHfS!lTzC_vY|tUZg;!tH0ni58C-u0XP z%dB1hehB@H>SoQI7L&vL=+^VjG@cE$4}!EZgxKZ&vKY^jvSw!A{+d{FL(+`F7T3G! z%9zYveY%B_MQ8Mv#y?2PvGe06J`>~9r1f>?;GqLaylSj|`_64Sec;;!0G%(C)WvUO zKDzd;Zo|Ku9lO5p(f(&;#-}Tv=P$N@9+HRd^ZkZ9y0(eYv-Y1Z95}dN?E3zq$*0Yn z6w~3J2Rep$p!*uPVdbh7F}jrT=aMBpGY&UxDcbL`{O;@BIi}0H4KY>0p`ywnXa9BX zc6*Fw&)2)%=Axf@kKgRwQ#2g}*b4k59ZT2~4`RCS+_gQzg=X~l5gxiRKmPcGIeg?$ zPJO6i@4j!$sAq>-|L&{ZF;OA(VBORiFUE9A^D%szv#V-{<@w$B#|&!$KEk~Zbh7K& z>JNe@H>Lyr9X6bI*tL6ySX-e5^?ra^kfx|dUgSVHz z5yOWj&Hf%ccGOIt{gPpf^WCbee~j3(hfuxl?|oNP?>^v*D-Jo|-35Fzx4m-ZGPgPB z8UMn?=XvRhH*@f)U@y@0mg^(@vb%0IAAYnp!sY!bFTQ06R9nO zZ|~DnK6LnC#N)cg|1?jJjgK*<={Kl6!c&==W50ZO>E(&$>iU;knk3(F!^U+H&)IWd z_F0%`pBovqBP$715Aq2wjxo$r7e3jvF=wv3G@C#B)J3;@;023cjbwlQO?bU==jY^U z&&POB7&GCN{*2uuXwtbai&0Uzs<(JcdLF2?@yYpNo~v{ZDTwoL3>#jzzg}($G$rj@c_qPzm(^hzHimj z&FkIxk4W~M*OL8%TiZ3Uc&dw^m$Yvh@xgZq!TtlM)*K*&hrYIl!<_dgs06 zo@%FxD%0D`p?l0R$Io?TCaot&zCCPxtmDVOH;b0c6MwW_jLzke_Dugn^YN7CC!dHB z2lj&KUg_fU4qSP}mI?*d-pyM+vFI~jnHu>7j4k#dteY>-nry${^x0GFSYtf6zH*W* z{8RiS570d)IHBu=_9LIK?D33>e7mF=q4FkgeS2eb;>7WY&VL}*i0>PK$x>J1*Zck~ z_Kmpn=>q#r#@L9_PZ)fAS9g78J|{uXhwm{7^BUcMBk3ofcHG3LV{)3bzTRLvYXaDp z!cTSAoPDYf_*Ef)P4Lk9x7iq6a5Lq{ocYc*)d$uo)mwA?a~~3P2W^=X|7_^fX}QNk zcgN)50|w$Sie7N;#}yj2sGCDe%p$a3$RbAZ+w3=3lEWZd;rD^}#r~Fk_UQm|8b3}u zWJLdBSS`_auelIkCMkDwl;t{jctz|x=r{by+} z570$b#HLvXef>|j%=uFhQ10s+Dht1n`FOHV^6gVGeQ4RHky*Jqw)E!{IV_}WQk^jC zE-B|+!xyr5-`95U?8ffW`hg8*JQV@x&?`yLw0fayCMg~;ND+)5!V z{C|Q9I^*=OlCsnPD$4SH^nL03`#1G#0FXa-ql0u7I=8E z*e$%dOZC*~A!fDj9WpadPH{v1g=+;-lb(sKW(!24h>3qzdPjo;#{d%(sO*|9x}yFVQ9%wUhC_#*zh z_66P^Ae%XA!M&f5bRMU;#pIlIsty_l$4{q$jIYYFvp~hcEK7Jf<^w_snCt{aLdl=3u z7~6U716h9j>)q!*Da{;3VLsW807dDMr*ar z2Mz5TlaF0=Veyg$w%tF|d-AErQ{}rncvih8PkSzE!an+OXyR82Kge0+$T34Lt$#MJ z=jDp4xysS9<~O#y#`3|A;J>@)c}hc09FkZwW0h~CXFtSqT_DB+z7NWaapQv8V&+Fj z@BWd}`1!P1lcKw*d_xx)3u*HGJ3WJk^|ScydgRLsl{GfQ`K}9UW8FHQ_573h?gH`R z5dT8!{PQyIBk-+!Al?E`wLjXdC1#LUJj3W-xle|RTx zXHO~uz;DT}64nt&xvk=HH%EO7cRaK2!S{8*{@zN;muelq_24%h++%gmUa|Sy0e~O( zh8*~_JWr*Jec~L}_m>@9yLRCZVArz%?QYgI?obgM_Q7B4bluMSGrPejegRMHwCk5( zn#SMdy~Dl(5m#tXw6{#P=NmPFHVM7?!33H;hq+C ze!_3jckG;hRo~sc%=*9Zo4@cC^MtJR?7#R80(J@Ch8jp4Cc(spbZYqY-> zarcf`0sCI!C%OmXOqD&8z*b4adEkBkewWXKHh?znU;?`Dl1DZt7fP}RgL*HH@2hvN z>93x>dVA_U&@zc6=L6cY#JM}?TJF2Uuk?I|uLS=u(F8on@urSGBe5p-aZzQo1x+0Vi3>@PRj^^3tgo*T-c>Hc!W zW{?{Wv76hu(cnytXSFGX@wkqR$Im~1emQw9pDoXqXUpua+n7Qg`&C+=b&>=B2*-n5-AnlGaXDYZ?pcYB>M==G48T&=cl4GKVu)@-D0%;DNNPV*%*i9 zzvM_`mt^SDQ*oo0+|G@$@7`3;|Hv{L#x@k= zStEODPwpch#F8`fY}vTzsa9j7_21*XAV7&d@1vP>U^gr0ujEueTE%u10J-pNF6R;M z4ZF|0n0wl`n9`qe+8GxPKWF_S|0zC366ejbjkl#lohqhQAxv`0a=7CRnFblHLC8gG z)#jdSAL!`5;F^A+&&YAE*Ouo@>ht+?kG1b%aIDR#oOmtyRuA{~h*x3gn&x?hssYPB zvh$ZV%tus%2NZ!{73@_AupKC5O2S{31uT zB@b>x{tg)YvI$(z%ZOi?o2(zgp1(MA4C=8hX9c9#JwmD->pIqUC;uLf`D@NG4N@`k z89TX-4S&&l1ZU`nT4Kx-{GGG1Gmd`7w$A{}Ys`V+Mq6)>^vaq(HvdIG`bOJ*wKC_w zvqrY>D$HITI)B}dx?Zt8wHA5*_Umk&?e^e5^^GZGsE>M&*n-Nvvd=+c&phKhx2zxP zbLzcTC%3*1o`3JR(sS!^wDaE2XI&a0Dx zZ27YvrgO9CSw5W?`DmMddRNVt`?0ajPNY$Gajx%~_-Io3MGe$=r6+jAf(z!Pva{OA zwbaBC$E9tY3foZOUbpIFGWX7`gu*te` z>VET>HR@cV4rYDK;E-RnHgW&P`Z}AY71{-77b{wgZ-G3cR!Ipf* z+}7s0FXwXDan1L@%{o-^cT5_u(_M>aw7;`O1J1bf+#?zdfBI9L27e#OXu@`PjYmLu z?~S}i+mXu50G@SNznZ6VKUx!W#;lp@d+)75V<_A!{(Y#{tm|+bTIaR!HKELL^lil%rW~L8IP=F?Gj$S?W4jwj9L&mS;2-PX zW6*n0bskDx^=eD%5WJ(kRp6z zt=3`>?$Gb%UDrDE`v7j)cB&CD=k`W?&Z*XewbbDrV5dCWxN7dNuVz#FJ}$BFydpo{ zE7&jcec7ISrhYd%rt6qxW;`-xHKha1Je3;-`jM@lk1*q~0dKlFkUX9nQ{9Xc*YP#09xcX+e3H(lO=cXmdn)wb&j~9t`-G(!=2}Tu~km!)BGz z_GB3D7b)GlTnvltNlJICVexPbISh*da`O}95#;T|$?!6z+c(?e_mFQOzq^6F-5y_G zr*yr!KfW>f@*VnjUmvOb^6*IdDN()w&yjo&T#i%TtK2{y1OIM&nHF!!Ym-)x_lt+L z_$TFXc>;+z?oZYd*Y;#3ac+MHiJ&nBdAYh1xjDM@Jgqp6ybY8?LWU?e$P6VvMe^^o zPH`=9?fg3}=*T{pT2{{I&jD2TP|Pa1kr8$^XmK!U#=m*c??-ah^IXA2%xDtZ`%MtR z)1VO+4)AGbD}2x2L-^g<_$Z>HPVy;AbpSSA?gQ{E_uGQr2?hQ4Mia>N)+M*Kl z?7XhyWggo2U?V*Je2>oy7^8;(JL+-XKh)!;zBM+)B=?DoQiF+U9MYWW`&h}x7r4Jf z1$UNZJ!$tSjT^ty%W69sj^eU&Zal!}jNzbw(S08kK4wuR|vX@UQD` zu@gIKj|Pfr*2u7tcD-x=sV zz`u*=x<(z#jh>kn6KzYJ&z2br^FFR)iX?n>&Gh^U4xq2GJMXXt#L(Y8$@x|7d*x*{ zW_U7Ywc(sxIxlkZG0KsDJWmID<{$ZSyXcFYLk;9yVVu~GLmBROSCVts2`8^*kKfK{ z&6(uWYNPNLJkPcBq7TP3|Kz}r`Nw@s`4&CNE9%uHs``EfrU6mV(hHAz3h znOL-SeanLS6hDZkVeGTZ}CMfjGpr7muc@PdRm5=NP=qQfOok=|jo?&XPn|mSrRB=iU$dDgB zA7tLR>pE#3s#w9x`pA8$JCBEGtc%{7aIRn(Zw}&P)U)K6)r{u=GXMTJ_@-rTP5-p- zeWr8JXgef^J)gw2b@3c%?t`i5N?shmC6v%)uX;ai>rkG99W2;!$hyeEQaJctZ2sn% z^Nuhla~_CO)xw;W&UqeWEn^M1sc}^if4V}!DfJ+Gd8so7VCvGdA+1r{pQuGs8H*aC znJ<$$4`pvlZFz0GrmU;(yFvEp&b;mQ4NiE4`odXKVyo6U^RqL;*USkDQq{4Tdt*&H ztjj$c_E@g*utta->jd8gwY2)Ej~ES5zK>*mn0JrY;6%0jc!U$E20z8HJ6NW3>+yQ> z+5j&lhAX^i!Rv#E!>iTy;HVVc=zwmKK=dopFaK5 z4^N+d^NLqWyuN*j3$H!?hU>@c%Yz@^Z4ZCC9S+{#4V(YnukP?t;}+Q8!3SUH;8hTP z;=}h8PjH>$I*^}e^gbRMB@ZO;qk(nTXH1z>V|@-S1A2yKqvU_9DRpf3HmxO|Q!h07SLcvDf&$HGFI@s;wKFtig^}dUJsL$p3K8C!>y@yoA4)c6&V4pdq zBtLDd2g!lNhj?MnIr5+`o}J(N8gNfD7R{}F&mcGG=kGJJet4#n_&$D38{*K@1$=m} z#Rj#)apuALI5&81+1ce-#wC8g1CD&x(inB$gB#aFFXn|gx9_cmsA?!Z`b!bTx$?S~cKr5@uwD-N0)ypMhLO-ldx7XK&p jtCary2mGJZ-=y^SU*r01N - - - - - - - \ No newline at end of file +RealFaviconGeneratorhttps://realfavicongenerator.net + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/ui/public/schema/resources.json b/ui/public/schema/resources.json index c3a8609551..b53179e1e1 100644 --- a/ui/public/schema/resources.json +++ b/ui/public/schema/resources.json @@ -617,6 +617,16 @@ "null" ] }, + "auto_update_skip_services": { + "description": "Ignore certain services during Global Auto Update polling.\nServices listed here are skipped only in the global auto-update flow.\nManual checks still include all services.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "destroy_before_deploy": { "description": "Whether to run `docker compose down` before `compose up`.", "type": [ @@ -5703,12 +5713,15 @@ "PartialServerBuilderConfig": { "type": "object", "properties": { - "server": { - "description": "The server id of the builder", + "servers": { + "description": "The server ids of the builders.\nIf multiple are given, builds will overflow\nto later specified servers as needed.", "type": [ - "string", + "array", "null" - ] + ], + "items": { + "type": "string" + } } } }, From ab1b8ca3f8a99370d3a5ff5401d21caa07a080ff Mon Sep 17 00:00:00 2001 From: ChanningHe <52875777+ChanningHe@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:38:20 +0900 Subject: [PATCH 053/203] feat: break out cache/buffers and ZFS ARC from used memory (#1489) --- bin/core/src/monitor/record.rs | 4 + bin/periphery/src/stats/mem.rs | 149 ++++++++++++++++++ bin/periphery/src/{stats.rs => stats/mod.rs} | 15 +- client/core/rs/src/entities/stats.rs | 35 +++- client/core/ts/src/types.ts | 45 +++++- ui/public/client/types.d.ts | 45 +++++- ui/src/resources/server/stats/current/ram.tsx | 23 ++- ui/src/resources/server/stats/historical.tsx | 109 ++++++++++++- 8 files changed, 406 insertions(+), 19 deletions(-) create mode 100644 bin/periphery/src/stats/mem.rs rename bin/periphery/src/{stats.rs => stats/mod.rs} (92%) diff --git a/bin/core/src/monitor/record.rs b/bin/core/src/monitor/record.rs index 3d3eb626c9..066782d602 100644 --- a/bin/core/src/monitor/record.rs +++ b/bin/core/src/monitor/record.rs @@ -23,6 +23,10 @@ pub async fn record_server_stats(ts: i64) { load_average: stats.load_average.clone(), mem_total_gb: stats.mem_total_gb, mem_used_gb: stats.mem_used_gb, + mem_buff_cache_gb: stats.mem_buff_cache_gb, + mem_zfs_arc_gb: stats.mem_zfs_arc_gb, + swap_total_gb: stats.swap_total_gb, + swap_used_gb: stats.swap_used_gb, disk_total_gb, disk_used_gb, disks: stats.disks.clone(), diff --git a/bin/periphery/src/stats/mem.rs b/bin/periphery/src/stats/mem.rs new file mode 100644 index 0000000000..a351c42425 --- /dev/null +++ b/bin/periphery/src/stats/mem.rs @@ -0,0 +1,149 @@ +/// Memory breakdown, all fields in bytes. +#[derive(Clone, Copy)] +pub struct MemBreakdown { + /// Total physical memory. + pub total: u64, + /// Free memory (`MemFree`). + pub free: u64, + /// Reclaimable page cache + buffers. + pub buff_cache: u64, + /// Full ZFS ARC size (`0` when ZFS is not present). + pub zfs_arc: u64, + /// Real application-used memory: `(total - available) - zfs_arc`. + pub used: u64, +} + +/// Read the full memory breakdown. Prefers `/proc` on Linux, falling +/// back to `sysinfo` when the files are unavailable (non-Linux, or read +/// failure). +pub fn read(system: &sysinfo::System) -> MemBreakdown { + #[cfg(target_os = "linux")] + if let Some(breakdown) = read_linux() { + return breakdown; + } + read_sysinfo(system) +} + +/// Cross-platform fallback using sysinfo. No buff/cache or ARC breakdown +/// is available here. +fn read_sysinfo(system: &sysinfo::System) -> MemBreakdown { + let total = system.total_memory(); + let available = system.available_memory(); + MemBreakdown { + total, + free: system.free_memory(), + buff_cache: 0, + zfs_arc: 0, + used: total.saturating_sub(available), + } +} + +/// Compute the breakdown from the Linux `/proc` files. Returns `None` if +/// `/proc/meminfo` can't be read (then we fall back to sysinfo). +/// +/// `MemAvailable` does not count the ZFS ARC as reclaimable, so a large +/// ARC would inflate "used" and cause false high-memory alerts. ARC is +/// reclaimable, so we subtract it from used and report it separately. +#[cfg(target_os = "linux")] +fn read_linux() -> Option { + let meminfo = + parse_meminfo(&std::fs::read_to_string("/proc/meminfo").ok()?)?; + // Absent when ZFS isn't loaded => arc stays zero. + let zfs_arc = std::fs::read_to_string("/proc/spl/kstat/zfs/arcstats") + .ok() + .map(|c| parse_zfs_arc_size(&c)) + .unwrap_or(0); + + // Cached + SReclaimable + Buffers - Shmem + let buff_cache = meminfo + .cached + .saturating_add(meminfo.s_reclaimable) + .saturating_add(meminfo.buffers) + .saturating_sub(meminfo.shmem); + + let raw_used = meminfo.total.saturating_sub(meminfo.available); + + Some(MemBreakdown { + total: meminfo.total, + free: meminfo.free, + buff_cache, + zfs_arc, + used: raw_used.saturating_sub(zfs_arc), + }) +} + +#[cfg(target_os = "linux")] +#[derive(Default)] +struct MemInfo { + total: u64, + free: u64, + available: u64, + buffers: u64, + cached: u64, + s_reclaimable: u64, + shmem: u64, +} + +/// Parse the relevant fields from `/proc/meminfo` contents. Values there +/// are in kibibytes; we convert to bytes. Returns `None` if `MemTotal` is +/// missing (i.e. not a real meminfo file). +#[cfg(target_os = "linux")] +fn parse_meminfo(contents: &str) -> Option { + let mut info = MemInfo::default(); + let mut have_total = false; + let mut have_available = false; + + for line in contents.lines() { + let Some((key, rest)) = line.split_once(':') else { + continue; + }; + // rest looks like " 12345 kB" + let Some(kb) = rest.split_whitespace().next() else { + continue; + }; + let Ok(kb) = kb.parse::() else { + continue; + }; + let bytes = kb.saturating_mul(1024); + match key { + "MemTotal" => { + info.total = bytes; + have_total = true; + } + "MemFree" => info.free = bytes, + "MemAvailable" => { + info.available = bytes; + have_available = true; + } + "Buffers" => info.buffers = bytes, + "Cached" => info.cached = bytes, + "SReclaimable" => info.s_reclaimable = bytes, + "Shmem" => info.shmem = bytes, + _ => {} + } + } + + if !have_total { + return None; + } + // Older kernels (<3.14) lack MemAvailable; fall back to free. + if !have_available { + info.available = info.free; + } + + Some(info) +} + +/// Parse the ZFS ARC `size` (bytes) from `/proc/spl/kstat/zfs/arcstats` +/// contents. The line looks like: `size 4 4294967296` (name, type, data). +/// Returns `0` if not found. +#[cfg(target_os = "linux")] +fn parse_zfs_arc_size(contents: &str) -> u64 { + for line in contents.lines() { + let mut fields = line.split_whitespace(); + if fields.next() == Some("size") { + return fields.nth(1).and_then(|v| v.parse().ok()).unwrap_or(0); + } + } + 0 +} diff --git a/bin/periphery/src/stats.rs b/bin/periphery/src/stats/mod.rs similarity index 92% rename from bin/periphery/src/stats.rs rename to bin/periphery/src/stats/mod.rs index 2a82a37912..88a62dfa0f 100644 --- a/bin/periphery/src/stats.rs +++ b/bin/periphery/src/stats/mod.rs @@ -9,6 +9,8 @@ use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; use crate::{config::periphery_config, state::stats_client}; +mod mem; + /// This should be called before starting the server in main.rs. /// Keeps the cached stats up to date pub fn spawn_polling_thread() { @@ -78,8 +80,7 @@ impl StatsClient { } pub fn get_system_stats(&self) -> SystemStats { - let total_mem = self.system.total_memory(); - let available_mem = self.system.available_memory(); + let memory = mem::read(&self.system); let mut network_ingress_bytes: u64 = 0; let mut network_egress_bytes: u64 = 0; @@ -98,9 +99,13 @@ impl StatsClient { 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, + mem_free_gb: memory.free as f64 / BYTES_PER_GB, + mem_used_gb: memory.used as f64 / BYTES_PER_GB, + mem_total_gb: memory.total as f64 / BYTES_PER_GB, + mem_buff_cache_gb: memory.buff_cache as f64 / BYTES_PER_GB, + mem_zfs_arc_gb: memory.zfs_arc as f64 / BYTES_PER_GB, + swap_total_gb: self.system.total_swap() as f64 / BYTES_PER_GB, + swap_used_gb: self.system.used_swap() as f64 / BYTES_PER_GB, network_ingress_bytes: network_ingress_bytes as f64, network_egress_bytes: network_egress_bytes as f64, disks: self.get_disks(), diff --git a/client/core/rs/src/entities/stats.rs b/client/core/rs/src/entities/stats.rs index da22cb9c1f..b6541665c6 100644 --- a/client/core/rs/src/entities/stats.rs +++ b/client/core/rs/src/entities/stats.rs @@ -50,6 +50,22 @@ pub struct SystemStatsRecord { pub mem_used_gb: f64, /// Total memory in GB pub mem_total_gb: f64, + /// [2.3.0+] + /// Reclaimable page cache + buffers in GB. + #[serde(default)] + pub mem_buff_cache_gb: f64, + /// [2.3.0+] + /// ZFS ARC cache in GB. 0 when ZFS is not present. + #[serde(default)] + pub mem_zfs_arc_gb: f64, + /// [2.3.0+] + /// Total swap in GB. + #[serde(default)] + pub swap_total_gb: f64, + /// [2.3.0+] + /// Used swap in GB. + #[serde(default)] + pub swap_used_gb: f64, /// Disk used in GB pub disk_used_gb: f64, /// Total disk size in GB @@ -83,10 +99,27 @@ pub struct SystemStats { /// It may be different than mem_total_gb - mem_used_gb. #[serde(default)] pub mem_free_gb: f64, - /// Used memory in GB. 'Total' - 'Available' (not free) memory. + /// Used memory in GB. 'Total' - 'Available' (not free) memory, + /// with the (reclaimable) ZFS ARC cache subtracted out. pub mem_used_gb: f64, /// Total memory in GB pub mem_total_gb: f64, + /// [2.3.0+] + /// Reclaimable page cache + buffers in GB. + #[serde(default)] + pub mem_buff_cache_gb: f64, + /// [2.3.0+] + /// ZFS ARC cache in GB. 0 when ZFS is not present. + #[serde(default)] + pub mem_zfs_arc_gb: f64, + /// [2.3.0+] + /// Total swap in GB. + #[serde(default)] + pub swap_total_gb: f64, + /// [2.3.0+] + /// Used swap in GB. + #[serde(default)] + pub swap_used_gb: f64, /// Breakdown of individual disks, ie their usages, sizes, and mount points pub disks: Vec, /// Network ingress usage in MB diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 31665f7433..d322e70880 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -2834,10 +2834,33 @@ export interface SystemStats { * It may be different than mem_total_gb - mem_used_gb. */ mem_free_gb?: number; - /** Used memory in GB. 'Total' - 'Available' (not free) memory. */ + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Breakdown of individual disks, ie their usages, sizes, and mount points */ disks: SingleDiskUsage[]; /** Network ingress usage in MB */ @@ -7960,6 +7983,26 @@ export interface SystemStatsRecord { mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Disk used in GB */ disk_used_gb: number; /** Total disk size in GB */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 2add2b1a31..6398ebcac6 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -2960,10 +2960,33 @@ export interface SystemStats { * It may be different than mem_total_gb - mem_used_gb. */ mem_free_gb?: number; - /** Used memory in GB. 'Total' - 'Available' (not free) memory. */ + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Breakdown of individual disks, ie their usages, sizes, and mount points */ disks: SingleDiskUsage[]; /** Network ingress usage in MB */ @@ -7614,6 +7637,26 @@ export interface SystemStatsRecord { mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Disk used in GB */ disk_used_gb: number; /** Total disk size in GB */ diff --git a/ui/src/resources/server/stats/current/ram.tsx b/ui/src/resources/server/stats/current/ram.tsx index b449617553..21d38382f9 100644 --- a/ui/src/resources/server/stats/current/ram.tsx +++ b/ui/src/resources/server/stats/current/ram.tsx @@ -2,6 +2,7 @@ import { ICONS } from "@/lib/icons"; import { StatBar } from "mogh_ui"; import { Types } from "komodo_client"; import { useFullServer } from "@/resources/server"; +import { Group, Text } from "@mantine/core"; export function ServerRamUsage({ id, @@ -13,16 +14,30 @@ export function ServerRamUsage({ const server = useFullServer(id); const usedRam = stats?.mem_used_gb; const totalRam = stats?.mem_total_gb; + const zfsArc = stats?.mem_zfs_arc_gb ?? 0; + const buffCache = stats?.mem_buff_cache_gb ?? 0; + // = free + reclaimable cache + ARC + const availableRam = + usedRam !== undefined && totalRam !== undefined + ? Math.max(0, totalRam - usedRam) + : undefined; return ( } description={ - usedRam && - totalRam && ( + usedRam !== undefined && + totalRam !== undefined && ( <> - {usedRam?.toFixed(1)} GB of {totalRam?.toFixed(1)} GB{" "} - in use + + Used {usedRam.toFixed(1)} GB of{" "} + {totalRam.toFixed(1)} GB + + + {availableRam?.toFixed(1)} GB available + {zfsArc > 0 && · ZFS ARC {zfsArc.toFixed(1)} GB} + {buffCache > 0 && · Cache {buffCache.toFixed(1)} GB} + ) } diff --git a/ui/src/resources/server/stats/historical.tsx b/ui/src/resources/server/stats/historical.tsx index 1c62675fc3..65e222f7e3 100644 --- a/ui/src/resources/server/stats/historical.tsx +++ b/ui/src/resources/server/stats/historical.tsx @@ -9,6 +9,7 @@ import { ShowHideButton } from "mogh_ui"; import { Center, Group, + lighten, Loader, Select, SimpleGrid, @@ -144,6 +145,34 @@ function StatChart({ { label: "15m", data: fifteen }, ]; } + if (type === "Memory") { + // Stacked GB composition, bottom -> top: Used, then reclaimable bands. + const series: { label: string; data: StatDatapoint[] }[] = [ + { + label: "Used", + data: records.map((s) => ({ date: s.ts, value: s.mem_used_gb ?? 0 })), + }, + ]; + if (records.some((s) => (s.mem_buff_cache_gb ?? 0) > 0)) { + series.push({ + label: "Cache/Buffers", + data: records.map((s) => ({ + date: s.ts, + value: s.mem_buff_cache_gb ?? 0, + })), + }); + } + if (records.some((s) => (s.mem_zfs_arc_gb ?? 0) > 0)) { + series.push({ + label: "ZFS ARC", + data: records.map((s) => ({ + date: s.ts, + value: s.mem_zfs_arc_gb ?? 0, + })), + }); + } + return series; + } const single = records.map((stat) => ({ date: stat.ts, value: getStat(stat, type), @@ -165,6 +194,15 @@ function StatChart({ "15m": hexColorByIntention("Warning")!, }; } + if (type === "Memory") { + // Yellow ramp: base for Used, lighter for the reclaimable bands. + const base = hexColorByIntention("Warning")!; + return { + Used: base, + "Cache/Buffers": lighten(base, 0.3), + "ZFS ARC": lighten(base, 0.55), + }; + } return { [type]: getColor(type) }; }, [type]); @@ -181,10 +219,24 @@ function StatChart({ const isNetwork = type === "Network Ingress" || type === "Network Egress"; const isLoadAvg = type === "Load Average"; + const isMemory = type === "Memory"; + + // Cap Y axis at total RAM so the gap above the stack reads as free. + const memTotal = useMemo( + () => + isMemory + ? (data?.stats ?? []).reduce( + (max, s) => Math.max(max, s.mem_total_gb ?? 0), + 0, + ) + : 0, + [data, isMemory], + ); const yTickFormatter = (value: number) => { if (isNetwork) return fmtSizeBytes(value); if (isLoadAvg) return value.toFixed(1); + if (isMemory) return `${value.toFixed(0)} GB`; return `${value.toFixed(0)}%`; }; @@ -266,8 +318,14 @@ function StatChart({ tick={{ fontSize: 10, fill: "var(--mantine-color-dimmed)" }} axisLine={false} tickLine={false} - width={isNetwork ? 70 : 42} - domain={isNetwork || isLoadAvg ? ["auto", "auto"] : [0, 100]} + width={isNetwork ? 70 : isMemory ? 52 : 42} + domain={ + isNetwork || isLoadAvg + ? ["auto", "auto"] + : isMemory + ? [0, memTotal > 0 ? memTotal : "auto"] + : [0, 100] + } /> new Date(label).toLocaleString()} + // Memory tooltip lists top-of-stack first, matching the chart. + itemSorter={ + isMemory + ? (item) => + -seriesData.findIndex((s) => s.label === item.dataKey) + : undefined + } formatter={(value, name) => { const v = Number(value); const formatted = isNetwork ? fmtSizeBytes(v) : isLoadAvg ? v.toFixed(2) - : `${v.toFixed(1)}%`; + : isMemory + ? `${v.toFixed(2)} GB` + : `${v.toFixed(1)}%`; return [formatted, name]; }} /> - {isLoadAvg && ( + {(isLoadAvg || isMemory) && ( ( + + {[...(payload ?? [])].reverse().map((entry) => ( + + + + {entry.value} + + + ))} + + ) + : undefined + } /> )} {seriesData.map((series) => ( @@ -299,8 +389,15 @@ function StatChart({ key={series.label} type="monotone" dataKey={series.label} + // Stack only for Memory; other charts overlay. + stackId={isMemory ? "mem" : undefined} stroke={colors[series.label]} - fill={`url(#${gradientId(series.label)})`} + fill={ + isMemory + ? colors[series.label] + : `url(#${gradientId(series.label)})` + } + fillOpacity={isMemory ? 0.45 : undefined} strokeWidth={2} dot={false} activeDot={{ r: 3 }} @@ -316,7 +413,6 @@ function StatChart({ function getStat(stat: Types.SystemStatsRecord, type: StatType) { if (type === "Cpu") return stat.cpu_perc || 0; - if (type === "Memory") return (100 * stat.mem_used_gb) / stat.mem_total_gb; if (type === "Disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb; if (type === "Network Ingress") return stat.network_ingress_bytes || 0; if (type === "Network Egress") return stat.network_egress_bytes || 0; @@ -325,7 +421,6 @@ function getStat(stat: Types.SystemStatsRecord, type: StatType) { function getColor(type: StatType) { if (type === "Cpu") return hexColorByIntention("Good")!; - if (type === "Memory") return hexColorByIntention("Warning")!; if (type === "Disk") return hexColorByIntention("Neutral")!; if (type === "Network Ingress") return hexColorByIntention("Good")!; if (type === "Network Egress") return hexColorByIntention("Critical")!; From 44d437abf20bbbc5ba13965d2ce467f36bce63e2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 29 Jun 2026 22:39:59 -0700 Subject: [PATCH 054/203] deploy 2.3.0-dev-14 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70089420f2..fe95b38974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index c57876b06e..5b49eaa893 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-13" +version = "2.3.0-dev-14" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From 30f111b5a4a7231287e9b61b730b849466d7b02c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 29 Jun 2026 23:03:32 -0700 Subject: [PATCH 055/203] onmisearch improve finding containers / services --- ui/src/app/topbar/omni-search/hooks.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index dfe86fda92..1e9f8c2bfe 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -53,7 +53,9 @@ export function useOmniSearch(): { const containersQuery = useMemo( () => ({ - containers: debouncedTerms, + containers: debouncedTerms + // Allows search like 'cont my name' to return containers matching 'my name' + .filter((term) => !"container".includes(term)), limit: 300, page: 0, }), @@ -66,7 +68,9 @@ export function useOmniSearch(): { const servicesQuery = useMemo( () => ({ - services: debouncedTerms, + services: debouncedTerms + // Allows search like 'serv my name' to return services matching 'my name' + .filter((term) => !"service".includes(term)), limit: 300, page: 0, }), From 28554c01e5fdbafdb0be8c8ccc8f7aebe0a89cfb Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 29 Jun 2026 23:28:03 -0700 Subject: [PATCH 056/203] refine service / container search --- bin/cli/src/command/container.rs | 11 ++++++----- bin/cli/src/command/terminal.rs | 2 +- bin/core/src/api/read/docker.rs | 16 +++------------- bin/core/src/api/read/stack.rs | 17 +++-------------- client/core/rs/src/api/read/docker.rs | 8 ++++---- client/core/rs/src/api/read/stack.rs | 8 ++++---- .../src/entities/config/cli/args/container.rs | 4 ++-- client/core/ts/src/types.ts | 12 ++++++------ ui/public/client/types.d.ts | 12 ++++++------ ui/src/app/topbar/omni-search/hooks.tsx | 12 ++++++------ ui/src/pages/containers.tsx | 6 +++--- 11 files changed, 44 insertions(+), 64 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index ade3b150b3..35e7236a6a 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -44,7 +44,7 @@ async fn list_containers( down, links, reverse, - containers: names, + containers: terms, images, networks, servers, @@ -64,10 +64,11 @@ async fn list_containers( client.read(ListAllDockerContainers { servers: Default::default(), tags: Default::default(), - containers: names.clone(), + terms: terms.clone(), state: Default::default(), - limit: 300, - page: *page, + limit: 100, + // Page is more naturally given starting as 1, 2, 3. + page: if *page == 0 { 0 } else { *page - 1 }, }), )?; @@ -151,7 +152,7 @@ pub async fn inspect_container( client.read(ListAllDockerContainers { servers: inspect.servers.clone(), tags: Default::default(), - containers: vec![inspect.container.clone()], + terms: vec![inspect.container.clone()], state: Default::default(), limit: 300, page: 0, diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index 721ddc7c8e..86420c63f6 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -130,7 +130,7 @@ async fn get_server( .read(ListAllDockerContainers { servers: Default::default(), tags: Default::default(), - containers: vec![container.to_string()], + terms: vec![container.to_string()], state: Default::default(), limit: 300, page: 0, diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 20a91fddca..36f04dda2d 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -96,14 +96,6 @@ impl Resolve for ListAllDockerContainers { ) .await?; - let terms = self - .containers - .iter() - .flat_map(|term| { - anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) - }) - .collect::>(); - let mut containers = Vec::::new(); let mut skipped = 0; let limit_usize = self.limit as usize; @@ -122,15 +114,13 @@ impl Resolve for ListAllDockerContainers { // Apply state filter if defined. (self.state.is_empty() || self.state.contains(&container.state)) && // Apply terms filter if defined - (terms.is_empty() + (self.terms.is_empty() // Match when all terms contained within a name. - || terms.iter().all(|(term, _)| container.name.contains(*term)) - // Match when any wildcard term directly matches. - || terms.iter().any(|(_, wc)| wc.is_match(container.name.as_bytes()))) + || self.terms.iter().all(|term| container.name.contains(term))) }); for container in more { if skipped < self.limit * self.page { - // Eg. page 1 skips until after 300 containers, page 2 after 600. + // Eg. page 1 skips until after 100 containers, page 2 after 200. skipped += 1; } else { // push and maybe early return diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index e474d9d9da..89549d5396 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -22,7 +22,6 @@ use periphery_client::api::{ container::InspectContainer, }; use reqwest::StatusCode; -use wildcard::Wildcard; use crate::{ helpers::{ @@ -92,14 +91,6 @@ impl Resolve for ListAllStackServices { ) .await?; - let terms = self - .services - .iter() - .flat_map(|term| { - anyhow::Ok((term, Wildcard::new(term.as_bytes())?)) - }) - .collect::>(); - let mut services = Vec::::new(); let mut skipped = 0; let limit_usize = self.limit as usize; @@ -113,15 +104,13 @@ impl Resolve for ListAllStackServices { // Apply state filter if defined. (self.state.is_empty() || self.state.contains(&service.state)) && // Apply terms filter if defined - terms.is_empty() + (self.terms.is_empty() // Match when all terms contained within a name. - || terms.iter().all(|(term, _)| service.service.contains(*term)) - // Match when any wildcard term directly matches. - || terms.iter().any(|(_, wc)| wc.is_match(service.service.as_bytes())) + || self.terms.iter().all(|term| service.service.contains(term))) }); for service in more { if skipped < self.limit * self.page { - // Eg. page 1 skips until after 300 services, page 2 after 600. + // Eg. page 1 skips until after 100 services, page 2 after 200. skipped += 1; } else { // push and maybe early return diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 121a905585..3e7d7a1f60 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -91,9 +91,9 @@ pub struct ListAllDockerContainers { pub tags: Vec, /// Filter by container name. - /// Supports wildcard matching syntax. + /// Returned containers have names which contain all terms. #[serde(default)] - pub containers: Vec, + pub terms: Vec, /// Filter by container state. #[serde(default)] @@ -105,7 +105,7 @@ pub struct ListAllDockerContainers { pub page: U64, /// Set the limit for number of containers per-page. - /// `limit: 300` is default. + /// `limit: 100` is default. /// /// Note: the page logic relies on this being consistent /// across queries for more pages. @@ -114,7 +114,7 @@ pub struct ListAllDockerContainers { } fn default_limit() -> u64 { - 300 + 100 } #[typeshare] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 646c9d293b..065fa8896d 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -108,9 +108,9 @@ pub struct ListAllStackServices { pub tags: Vec, /// Filter by service name. - /// Supports wildcard matching syntax. + /// Returned services have names which contain all terms. #[serde(default)] - pub services: Vec, + pub terms: Vec, /// Filter by service state. #[serde(default)] @@ -122,7 +122,7 @@ pub struct ListAllStackServices { pub page: U64, /// Set the limit for number of services per-page. - /// `limit: 300` is default. + /// `limit: 100` is default. /// /// Note: the page logic relies on this being consistent /// across queries for more pages. @@ -131,7 +131,7 @@ pub struct ListAllStackServices { } fn default_limit() -> u64 { - 300 + 100 } #[typeshare] diff --git a/client/core/rs/src/entities/config/cli/args/container.rs b/client/core/rs/src/entities/config/cli/args/container.rs index e4b3dd1a11..88c122dca2 100644 --- a/client/core/rs/src/entities/config/cli/args/container.rs +++ b/client/core/rs/src/entities/config/cli/args/container.rs @@ -37,8 +37,8 @@ pub struct Container { /// Specify the format of the output. #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)] pub format: super::CliFormat, - /// Specify page for container list - #[arg(long, short = 'p', default_value_t = 300)] + /// Specify page for container list, starting at 1. + #[arg(long, short = 'p', default_value_t = 1)] pub page: u64, } diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index d322e70880..462c35ac6a 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -8662,9 +8662,9 @@ export interface ListAllDockerContainers { tags?: string[]; /** * Filter by container name. - * Supports wildcard matching syntax. + * Returned containers have names which contain all terms. */ - containers?: string[]; + terms?: string[]; /** Filter by container state. */ state?: ContainerStateStatusEnum[]; /** @@ -8674,7 +8674,7 @@ export interface ListAllDockerContainers { page?: U64; /** * Set the limit for number of containers per-page. - * `limit: 300` is default. + * `limit: 100` is default. * * Note: the page logic relies on this being consistent * across queries for more pages. @@ -8693,9 +8693,9 @@ export interface ListAllStackServices { tags?: string[]; /** * Filter by service name. - * Supports wildcard matching syntax. + * Returned services have names which contain all terms. */ - services?: string[]; + terms?: string[]; /** Filter by service state. */ state?: StackServiceState[]; /** @@ -8705,7 +8705,7 @@ export interface ListAllStackServices { page?: U64; /** * Set the limit for number of services per-page. - * `limit: 300` is default. + * `limit: 100` is default. * * Note: the page logic relies on this being consistent * across queries for more pages. diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 6398ebcac6..e30fe3dee0 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -8250,9 +8250,9 @@ export interface ListAllDockerContainers { tags?: string[]; /** * Filter by container name. - * Supports wildcard matching syntax. + * Returned containers have names which contain all terms. */ - containers?: string[]; + terms?: string[]; /** Filter by container state. */ state?: ContainerStateStatusEnum[]; /** @@ -8262,7 +8262,7 @@ export interface ListAllDockerContainers { page?: U64; /** * Set the limit for number of containers per-page. - * `limit: 300` is default. + * `limit: 100` is default. * * Note: the page logic relies on this being consistent * across queries for more pages. @@ -8280,9 +8280,9 @@ export interface ListAllStackServices { tags?: string[]; /** * Filter by service name. - * Supports wildcard matching syntax. + * Returned services have names which contain all terms. */ - services?: string[]; + terms?: string[]; /** Filter by service state. */ state?: StackServiceState[]; /** @@ -8292,7 +8292,7 @@ export interface ListAllStackServices { page?: U64; /** * Set the limit for number of services per-page. - * `limit: 300` is default. + * `limit: 100` is default. * * Note: the page logic relies on this being consistent * across queries for more pages. diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 1e9f8c2bfe..721b82e760 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -51,12 +51,12 @@ export function useOmniSearch(): { const debouncedTerms = useDebounce(searchTerms, 700); - const containersQuery = useMemo( + const containersQuery: Types.ListAllDockerContainers = useMemo( () => ({ - containers: debouncedTerms + terms: debouncedTerms // Allows search like 'cont my name' to return containers matching 'my name' .filter((term) => !"container".includes(term)), - limit: 300, + limit: 10, page: 0, }), [debouncedTerms], @@ -66,12 +66,12 @@ export function useOmniSearch(): { refetchInterval: 15_000, }).data; - const servicesQuery = useMemo( + const servicesQuery: Types.ListAllStackServices = useMemo( () => ({ - services: debouncedTerms + terms: debouncedTerms // Allows search like 'serv my name' to return services matching 'my name' .filter((term) => !"service".includes(term)), - limit: 300, + limit: 10, page: 0, }), [debouncedTerms], diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index e5845555bb..a130788670 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -19,7 +19,7 @@ export default function Containers() { const [selectedServers, setSelectedServers] = useState([]); const debouncedSearch = useDebounce(search, 700); - const containersQuery = useMemo( + const searchTerms = useMemo( () => debouncedSearch.split(" ").filter((term) => term), [debouncedSearch], ); @@ -39,11 +39,11 @@ export default function Containers() { const containers = useRead("ListAllDockerContainers", { - containers: containersQuery, + terms: searchTerms, servers: selectedServers, tags, page, - limit: 300, + limit: 100, }).data ?? []; const Table = useMemo(() => { From f37c5e53e685385795dffed6e187b51d8c2dd795 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 29 Jun 2026 23:28:32 -0700 Subject: [PATCH 057/203] deploy 2.3.0-dev-15 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe95b38974..0bac27dee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5b49eaa893..b1b6ecd5d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-14" +version = "2.3.0-dev-15" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" From a816b0c7365a878600b698d6b4f419a8c132e61d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 29 Jun 2026 23:43:51 -0700 Subject: [PATCH 058/203] deploy 2.3.0-dev-16 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- bin/core/src/api/read/docker.rs | 1 - bin/core/src/helpers/builder.rs | 19 ++++++++++--------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bac27dee7..2e63287344 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "komodo_client", @@ -3381,7 +3381,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "bcrypt", @@ -3411,7 +3411,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "async_timing_util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "arc-swap", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "arc-swap", @@ -4640,7 +4640,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "encoding", @@ -6999,7 +6999,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "axum", @@ -8015,7 +8015,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index b1b6ecd5d0..abebc546d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-15" +version = "2.3.0-dev-16" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 36f04dda2d..f5702ad728 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -29,7 +29,6 @@ use periphery_client::api::{ ImageHistory, InspectImage, InspectNetwork, InspectVolume, }, }; -use wildcard::Wildcard; use crate::{ api::read::ReadArgs, diff --git a/bin/core/src/helpers/builder.rs b/bin/core/src/helpers/builder.rs index e1c14ffbee..399f9921d8 100644 --- a/bin/core/src/helpers/builder.rs +++ b/bin/core/src/helpers/builder.rs @@ -6,10 +6,10 @@ use formatting::muted; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ Version, - build::{Build, BuildState}, + build::Build, builder::{AwsBuilderConfig, Builder, BuilderConfig}, komodo_timestamp, - repo::{Repo, RepoState}, + repo::Repo, server::{Server, ServerState}, update::{Log, Update}, }; @@ -27,7 +27,7 @@ use crate::{ helpers::update::update_update, periphery::PeripheryClient, resource::{self, list_all_resources}, - state::{build_state_cache, repo_state_cache, server_status_cache}, + state::{action_states, server_status_cache}, }; use super::periphery_client; @@ -111,34 +111,35 @@ pub async fn connect_builder_periphery( // Count currently building resources let mut building = 0; + let action_states = action_states(); - let build_state_cache = build_state_cache(); for build in builds .inspect_err(|e| { warn!("Failed to query for Builds using Builder | {e:#}") }) .unwrap_or_default() { - if build_state_cache + if action_states + .build .get(&build.id) .await - .map(|s| matches!(s, BuildState::Building)) + .and_then(|s| s.get().map(|s| s.building).ok()) .unwrap_or_default() { building += 1; } } - let repo_state_cache = repo_state_cache(); for repo in repos .inspect_err(|e| { warn!("Failed to query for Repos using Builder | {e:#}") }) .unwrap_or_default() { - if repo_state_cache + if action_states + .repo .get(&repo.id) .await - .map(|s| matches!(s, RepoState::Building)) + .and_then(|s| s.get().map(|s| s.building).ok()) .unwrap_or_default() { building += 1; From e6448bdc71bacb91934c66b88f3396813100a91a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 30 Jun 2026 13:35:15 -0700 Subject: [PATCH 059/203] bump mogh auth server, remove authors from cargo toml in line with newer spec --- Cargo.lock | 91 +++++++++++++++++++------------- Cargo.toml | 19 ++++--- bin/cli/Cargo.toml | 1 - bin/core/Cargo.toml | 1 - bin/periphery/Cargo.toml | 1 - client/core/rs/Cargo.toml | 1 - client/periphery/rs/Cargo.toml | 1 - example/alerter/Cargo.toml | 1 - example/update_logger/Cargo.toml | 1 - lib/command/Cargo.toml | 1 - lib/database/Cargo.toml | 1 - lib/encoding/Cargo.toml | 1 - lib/environment/Cargo.toml | 1 - lib/formatting/Cargo.toml | 1 - lib/git/Cargo.toml | 1 - lib/interpolate/Cargo.toml | 1 - lib/transport/Cargo.toml | 1 - xtask/Cargo.toml | 1 - 18 files changed, 62 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e63287344..172866c9db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "approx" @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.232.0" +version = "1.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed1c72b10833ce8a789a21c1b95df11387932d7e9ee9a2931635bdb6770a0ad" +checksum = "eeada8269fdf43855385c77d1b620512e28e6efad13d5429f58d66daca8c7842" dependencies = [ "arc-swap", "aws-credential-types", @@ -547,7 +547,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -806,7 +806,7 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "pin-project-lite", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "bcrypt" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905" +checksum = "7f3c067aa24dd4ed5c79cf222a38f260c8f23d3b82a062fba3f28c6fe563b753" dependencies = [ "base64 0.22.1", "blowfish", @@ -1076,9 +1076,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -2915,7 +2915,7 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -3364,9 +3364,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "aws-lc-rs", "base64 0.22.1", @@ -3377,6 +3377,7 @@ dependencies = [ "serde_json", "signature", "simple_asn1", + "zeroize", ] [[package]] @@ -3399,7 +3400,7 @@ dependencies = [ "mogh_logger", "mogh_pki", "mogh_secret_file", - "rustls 0.23.40", + "rustls 0.23.41", "serde", "serde_json", "serde_qs", @@ -3498,7 +3499,7 @@ dependencies = [ "periphery_client", "regex", "reqwest 0.13.4", - "rustls 0.23.40", + "rustls 0.23.41", "serde", "serde_json", "serde_qs", @@ -3556,7 +3557,7 @@ dependencies = [ "mogh_server", "periphery_client", "portable-pty", - "rustls 0.23.40", + "rustls 0.23.41", "serde", "serde_json", "serde_yaml_ng", @@ -3773,9 +3774,9 @@ dependencies = [ [[package]] name = "mogh_auth_server" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d23b444f9f1a591f1fd44641b894ca3068a9ce3db3ea99896a5f757fa2fe" +checksum = "51cb74a7edc51da8ef00767691d071fcab84c9dc36ceb8f1ec6b0c514ec7de9b" dependencies = [ "anyhow", "arc-swap", @@ -4054,7 +4055,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "rustc_version_runtime", - "rustls 0.23.40", + "rustls 0.23.41", "serde", "serde_bytes", "serde_with", @@ -4942,7 +4943,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.40", + "rustls 0.23.41", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -4963,7 +4964,7 @@ dependencies = [ "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -5213,7 +5214,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-native-certs", "rustls-pki-types", "serde", @@ -5254,7 +5255,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -5527,9 +5528,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -5574,7 +5575,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", @@ -6409,9 +6410,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.3" +version = "0.39.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96" +checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" dependencies = [ "libc", "memchr", @@ -6619,7 +6620,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.40", + "rustls 0.23.41", "tokio", ] @@ -6643,7 +6644,7 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -6770,9 +6771,9 @@ dependencies = [ [[package]] name = "totp-rs" -version = "5.7.1" +version = "5.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b36a9dd327e9f401320a2cb4572cc76ff43742bcfc3291f871691050f140ba" +checksum = "50e69a15e21b2ff22c415446983978bded3244195f17d59cb113551c1e806f91" dependencies = [ "base32", "constant_time_eq", @@ -7012,7 +7013,7 @@ dependencies = [ "periphery_client", "pin-project-lite", "rand 0.10.1", - "rustls 0.23.40", + "rustls 0.23.41", "serde", "sha1 0.11.0", "sha2 0.11.0", @@ -7042,7 +7043,7 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.40", + "rustls 0.23.41", "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.18", @@ -7253,9 +7254,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -8092,6 +8093,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index abebc546d6..f062b3bbdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ members = [ [workspace.package] version = "2.3.0-dev-16" edition = "2024" -authors = ["mbecker20 "] license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" homepage = "https://komo.do" @@ -35,7 +34,7 @@ mogh_error = { version = "1.0.4", default-features = false } derive_default_builder = "0.1.8" async_timing_util = "1.1.0" mogh_auth_client = "1.5.0" -mogh_auth_server = "1.5.0" +mogh_auth_server = "1.6.0" mogh_secret_file = "1.0.1" mogh_validations = "1.0.1" mogh_rate_limit = "1.0.1" @@ -58,7 +57,7 @@ tokio-util = { version = "0.7.18", features = ["io", "codec"] } tokio-stream = { version = "0.1.18", features = ["sync"] } pin-project-lite = "0.2.17" futures-util = "0.3.32" -arc-swap = "1.9.1" +arc-swap = "1.9.2" # SERVER tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } @@ -82,7 +81,7 @@ serde_qs = "1.1.2" url = "2.5.8" # ERROR -anyhow = "1.0.102" +anyhow = "1.0.103" thiserror = "2.0.18" # SYSTEM @@ -97,11 +96,11 @@ dotenvy = "0.15.7" envy = "0.4.2" # CRYPTO / AUTH -uuid = { version = "1.23.3", features = ["v4", "fast-rng", "serde"] } -rustls = { version = "0.23.40", features = ["aws-lc-rs"] } +uuid = { version = "1.23.4", features = ["v4", "fast-rng", "serde"] } +rustls = { version = "0.23.41", features = ["aws-lc-rs"] } data-encoding = "2.11.0" urlencoding = "2.1.3" -bcrypt = "0.19.1" +bcrypt = "0.19.2" hmac = "0.13.0" sha1 = "0.11.0" sha2 = "0.11.0" @@ -114,12 +113,12 @@ portable-pty = "0.9.0" shell-escape = "0.1.5" crossterm = "0.29.0" bollard = "0.21.0" -sysinfo = "0.39.3" +sysinfo = "0.39.5" shlex = "2.0.1" # CLOUD aws-config = "1.8.18" -aws-sdk-ec2 = "1.232.0" +aws-sdk-ec2 = "1.235.0" aws-credential-types = "1.2.14" ## CRON @@ -136,7 +135,7 @@ comfy-table = "7.2.2" typeshare = "1.0.5" wildcard = "0.3.0" colored = "3.1.1" -bytes = "1.11.1" +bytes = "1.12.0" regex = "1.12.4" [profile.release] diff --git a/bin/cli/Cargo.toml b/bin/cli/Cargo.toml index 8e13a4476b..04a3926276 100644 --- a/bin/cli/Cargo.toml +++ b/bin/cli/Cargo.toml @@ -3,7 +3,6 @@ name = "komodo_cli" description = "Command line tool for Komodo" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml index 19cd11b64e..a9e1f4847a 100644 --- a/bin/core/Cargo.toml +++ b/bin/core/Cargo.toml @@ -2,7 +2,6 @@ name = "komodo_core" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/bin/periphery/Cargo.toml b/bin/periphery/Cargo.toml index f814f2d3ea..1834939083 100644 --- a/bin/periphery/Cargo.toml +++ b/bin/periphery/Cargo.toml @@ -2,7 +2,6 @@ name = "komodo_periphery" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/client/core/rs/Cargo.toml b/client/core/rs/Cargo.toml index 2523df66d3..b0acef8502 100644 --- a/client/core/rs/Cargo.toml +++ b/client/core/rs/Cargo.toml @@ -3,7 +3,6 @@ name = "komodo_client" description = "Client for the Komodo build and deployment system" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/client/periphery/rs/Cargo.toml b/client/periphery/rs/Cargo.toml index 4fa2768591..b7e7a1691e 100644 --- a/client/periphery/rs/Cargo.toml +++ b/client/periphery/rs/Cargo.toml @@ -2,7 +2,6 @@ name = "periphery_client" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/example/alerter/Cargo.toml b/example/alerter/Cargo.toml index 58a1dbc818..d2c9d71c27 100644 --- a/example/alerter/Cargo.toml +++ b/example/alerter/Cargo.toml @@ -2,7 +2,6 @@ name = "alerter" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/example/update_logger/Cargo.toml b/example/update_logger/Cargo.toml index fa51460570..afe4397c24 100644 --- a/example/update_logger/Cargo.toml +++ b/example/update_logger/Cargo.toml @@ -2,7 +2,6 @@ name = "update_logger" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/lib/command/Cargo.toml b/lib/command/Cargo.toml index 981fbb4b00..802eede407 100644 --- a/lib/command/Cargo.toml +++ b/lib/command/Cargo.toml @@ -2,7 +2,6 @@ name = "command" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/database/Cargo.toml b/lib/database/Cargo.toml index 26d38492ef..4421fe006f 100644 --- a/lib/database/Cargo.toml +++ b/lib/database/Cargo.toml @@ -2,7 +2,6 @@ name = "database" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/encoding/Cargo.toml b/lib/encoding/Cargo.toml index c2178cadd5..63abb3dc7f 100644 --- a/lib/encoding/Cargo.toml +++ b/lib/encoding/Cargo.toml @@ -2,7 +2,6 @@ name = "encoding" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/environment/Cargo.toml b/lib/environment/Cargo.toml index a9d821fc72..ce7e91d5ce 100644 --- a/lib/environment/Cargo.toml +++ b/lib/environment/Cargo.toml @@ -2,7 +2,6 @@ name = "environment" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/formatting/Cargo.toml b/lib/formatting/Cargo.toml index abc2fe23d1..b6d17731dd 100644 --- a/lib/formatting/Cargo.toml +++ b/lib/formatting/Cargo.toml @@ -2,7 +2,6 @@ name = "formatting" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/git/Cargo.toml b/lib/git/Cargo.toml index df618a85ab..8de2fa564b 100644 --- a/lib/git/Cargo.toml +++ b/lib/git/Cargo.toml @@ -2,7 +2,6 @@ name = "git" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/interpolate/Cargo.toml b/lib/interpolate/Cargo.toml index 15389a53bd..22d498469b 100644 --- a/lib/interpolate/Cargo.toml +++ b/lib/interpolate/Cargo.toml @@ -2,7 +2,6 @@ name = "interpolate" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/transport/Cargo.toml b/lib/transport/Cargo.toml index eba3756ee9..e83955371a 100644 --- a/lib/transport/Cargo.toml +++ b/lib/transport/Cargo.toml @@ -2,7 +2,6 @@ name = "transport" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 2534d698fc..c86c3eee0e 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -2,7 +2,6 @@ name = "xtask" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true From 3564f72bd50394bdea317a8c100a7ab4101a112b Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 30 Jun 2026 13:36:04 -0700 Subject: [PATCH 060/203] deploy 2.3.0-dev-17 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 172866c9db..7d8c63ad0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index f062b3bbdb..9cf20847fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-16" +version = "2.3.0-dev-17" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From ba7e0137735d5ac3ec570f25a1eadee748e060b9 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 30 Jun 2026 15:45:09 -0700 Subject: [PATCH 061/203] improve selection of available building server, works when multiple builds sent simultaneously --- bin/core/src/cloud/mod.rs | 5 +- bin/core/src/helpers/builder.rs | 110 +++++++++++++++----------------- bin/core/src/state.rs | 15 ++++- 3 files changed, 67 insertions(+), 63 deletions(-) diff --git a/bin/core/src/cloud/mod.rs b/bin/core/src/cloud/mod.rs index 60e9aa4ac5..48851fce4b 100644 --- a/bin/core/src/cloud/mod.rs +++ b/bin/core/src/cloud/mod.rs @@ -2,8 +2,9 @@ pub mod aws; #[derive(Debug)] pub enum BuildCleanupData { - /// Nothing to clean up - Server, + /// Store the builder id if it needs + /// to be removed from builder_usage_cache. + Server(Option), /// Cleanup Periphery connection Url, /// Clean up AWS instance diff --git a/bin/core/src/helpers/builder.rs b/bin/core/src/helpers/builder.rs index 399f9921d8..d37ba1de67 100644 --- a/bin/core/src/helpers/builder.rs +++ b/bin/core/src/helpers/builder.rs @@ -1,19 +1,18 @@ -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use anyhow::{Context, anyhow}; -use database::{bson::doc, mungos::mongodb::bson::oid::ObjectId}; +use database::mungos::mongodb::bson::oid::ObjectId; use formatting::muted; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ Version, - build::Build, builder::{AwsBuilderConfig, Builder, BuilderConfig}, komodo_timestamp, - repo::Repo, server::{Server, ServerState}, update::{Log, Update}, }; use periphery_client::api::{self, GetVersionResponse}; +use tokio::sync::Mutex; use crate::{ cloud::{ @@ -26,8 +25,8 @@ use crate::{ connection::PeripheryConnectionArgs, helpers::update::update_update, periphery::PeripheryClient, - resource::{self, list_all_resources}, - state::{action_states, server_status_cache}, + resource, + state::{builder_usage_cache, server_status_cache}, }; use super::periphery_client; @@ -96,54 +95,7 @@ pub async fn connect_builder_periphery( let server = resource::get::(&config.server_ids[0]).await?; let periphery = periphery_client(&server).await?; - return Ok((periphery, BuildCleanupData::Server)); - } - - // Query for builds / repos using the builder - let (builds, repos) = tokio::join!( - list_all_resources::( - doc! { "config.builder_id": &builder.id }, - ), - list_all_resources::( - doc! { "config.builder_id": &builder.id }, - ) - ); - - // Count currently building resources - let mut building = 0; - let action_states = action_states(); - - for build in builds - .inspect_err(|e| { - warn!("Failed to query for Builds using Builder | {e:#}") - }) - .unwrap_or_default() - { - if action_states - .build - .get(&build.id) - .await - .and_then(|s| s.get().map(|s| s.building).ok()) - .unwrap_or_default() - { - building += 1; - } - } - for repo in repos - .inspect_err(|e| { - warn!("Failed to query for Repos using Builder | {e:#}") - }) - .unwrap_or_default() - { - if action_states - .repo - .get(&repo.id) - .await - .and_then(|s| s.get().map(|s| s.building).ok()) - .unwrap_or_default() - { - building += 1; - } + return Ok((periphery, BuildCleanupData::Server(None))); } // Get filtered list of available Servers @@ -166,12 +118,16 @@ pub async fn connect_builder_periphery( .flatten() .collect::>(); - // Select server based on building count modulo available length. - let index = building % available_server_ids.len(); - let server = - resource::get::(available_server_ids[index]).await?; + let selected = builder_usage_cache() + .get_or_insert_default(&builder.id) + .await + .select(&available_server_ids) + .await + .context("Server builder has no available servers")?; + + let server = resource::get::(selected).await?; let periphery = periphery_client(&server).await?; - Ok((periphery, BuildCleanupData::Server)) + Ok((periphery, BuildCleanupData::Server(Some(server.id)))) } } } @@ -289,9 +245,17 @@ pub async fn cleanup_builder_instance( update: &mut Update, ) { match cleanup_data { - BuildCleanupData::Server => { + BuildCleanupData::Server(None) => { // Nothing to clean up } + BuildCleanupData::Server(Some(builder_id)) => { + // Release periphery (server) id from builder + builder_usage_cache() + .get_or_insert_default(&builder_id) + .await + .release(&periphery.id) + .await + } BuildCleanupData::Url => { periphery.cleanup().await; } @@ -347,3 +311,29 @@ pub fn start_aws_builder_log( ] .join("\n") } + +#[derive(Default)] +pub struct BuilderUsage(Mutex>); + +impl BuilderUsage { + pub async fn select<'a>( + &self, + available: &'a [&String], + ) -> Option<&'a str> { + let mut lock = self.0.lock().await; + let selected = *available.iter().min_by_key(|key| { + lock.get(key.as_str()).copied().unwrap_or(0) + })?; + *lock.entry(selected.clone()).or_insert(0) += 1; + Some(selected.as_str()) + } + + pub async fn release(&self, key: &str) { + let mut lock = self.0.lock().await; + if let Some(count) = lock.get_mut(key) + && *count > 0 + { + *count -= 1; + } + } +} diff --git a/bin/core/src/state.rs b/bin/core/src/state.rs index 9c2fd47467..21290db152 100644 --- a/bin/core/src/state.rs +++ b/bin/core/src/state.rs @@ -25,7 +25,7 @@ use crate::{ connection::PeripheryConnections, helpers::{ action_state::ActionStates, all_resources::AllResourcesById, - image_digest::ImageDigestCache, + builder::BuilderUsage, image_digest::ImageDigestCache, }, }; @@ -208,6 +208,7 @@ pub fn action_state_cache() -> &'static ActionStateCache { ACTION_STATE_CACHE.get_or_init(Default::default) } +/// Store all resources in local cache for fast lookup pub fn all_resources_cache() -> &'static ArcSwap { static ALL_RESOURCES: OnceLock> = OnceLock::new(); @@ -222,3 +223,15 @@ pub fn image_digest_cache() -> &'static ImageDigestCache { OnceLock::new(); IMAGE_DIGEST_CACHE.get_or_init(ImageDigestCache::new) } + +/// Maps Builder id => downstream count map (eg server id => active count) +type BuilderUsageCache = CloneCache>; + +/// For builders with multiple downstream machines to choose. +/// Stores active build count for each downstream. +/// Maps Builder id => downstream count map +pub fn builder_usage_cache() -> &'static BuilderUsageCache { + static BUILDER_USAGE_CACHE: OnceLock = + OnceLock::new(); + BUILDER_USAGE_CACHE.get_or_init(Default::default) +} From aa2de7e7d9537c03e929f832776d4f1afe49c078 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 30 Jun 2026 15:45:39 -0700 Subject: [PATCH 062/203] deploy 2.3.0-dev-18 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d8c63ad0a..3a99bcb58f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 9cf20847fb..6f22414147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-17" +version = "2.3.0-dev-18" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 9b22e5b9344e83a368a4f8bade832c36e48a7a9d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 30 Jun 2026 23:59:48 -0700 Subject: [PATCH 063/203] show stack services even when down --- ui/src/resources/stack/services.tsx | 18 +++--------------- ui/src/resources/stack/tabs.tsx | 9 +++------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/ui/src/resources/stack/services.tsx b/ui/src/resources/stack/services.tsx index bb30a3a1ad..2055754c29 100644 --- a/ui/src/resources/stack/services.tsx +++ b/ui/src/resources/stack/services.tsx @@ -19,21 +19,9 @@ export default function StackServices({ titleOther?: ReactNode; }) { const info = useStack(id)?.info; - const state = info?.state ?? Types.StackState.Unknown; - const services = useRead( - "ListStackServices", - { stack: id }, - { refetchInterval: 10_000 }, - ).data; - - if ( - !services || - services.length === 0 || - [Types.StackState.Unknown, Types.StackState.Down].includes(state) - ) { - // return
No Services Available
; - return null; - } + const services = + useRead("ListStackServices", { stack: id }, { refetchInterval: 10_000 }) + .data ?? []; return (
diff --git a/ui/src/resources/stack/tabs.tsx b/ui/src/resources/stack/tabs.tsx index 340420358b..ec0320e4eb 100644 --- a/ui/src/resources/stack/tabs.tsx +++ b/ui/src/resources/stack/tabs.tsx @@ -34,11 +34,11 @@ export default function StackTabs({ id }: { id: string }) { const state = info?.state; const hideInfo = !info?.files_on_host && !info?.repo && !info?.linked_repo; - const hideServices = + const hideLogs = state === undefined || state === Types.StackState.Unknown || - state === Types.StackState.Down; - const hideLogs = hideServices || !specificLogs; + state === Types.StackState.Down || + !specificLogs; const terminalDisabled = !specificTerminal || containerTerminalsDisabled || @@ -53,7 +53,6 @@ export default function StackTabs({ id }: { id: string }) { const view = (_view === "Info" && hideInfo) || - (_view === "Services" && hideServices) || (_view === "Terminals" && terminalDisabled) || (_view === "Log" && hideLogs) ? "Config" @@ -72,7 +71,6 @@ export default function StackTabs({ id }: { id: string }) { }, { value: "Services", - disabled: hideServices, icon: ICONS.Service, }, { @@ -89,7 +87,6 @@ export default function StackTabs({ id }: { id: string }) { ], [ hideInfo, - hideServices, specificLogs, hideLogs, specificTerminal, From eca4ebd235b6f29a8fedc296340388231d66adc6 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 00:12:46 -0700 Subject: [PATCH 064/203] improve stack services view when down --- ui/src/components/stack-service-link.tsx | 13 ++++++++++--- ui/src/resources/stack/services.tsx | 23 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ui/src/components/stack-service-link.tsx b/ui/src/components/stack-service-link.tsx index 82346448cc..acf15c41d2 100644 --- a/ui/src/components/stack-service-link.tsx +++ b/ui/src/components/stack-service-link.tsx @@ -1,8 +1,10 @@ import { containerStateIntention, swarmStateIntention } from "@/lib/color"; import { useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; +import { useStack } from "@/resources/stack"; import { Group, Text } from "@mantine/core"; -import { hexColorByIntention } from "mogh_ui"; +import { Types } from "komodo_client"; +import { ColorIntention, hexColorByIntention } from "mogh_ui"; import { Link } from "react-router-dom"; export interface StackServiceLinkProps { @@ -14,15 +16,20 @@ export default function StackServiceLink({ id, service: _service, }: StackServiceLinkProps) { + const isUnknown = useStack(id)?.info.state === Types.StackState.Unknown; const services = useRead( "ListStackServices", { stack: id }, { refetchInterval: 10_000 }, ).data; const service = services?.find((s) => s.service === _service); - const intention = service?.swarm_service?.State + const intention: ColorIntention = service?.swarm_service?.State ? swarmStateIntention(service?.swarm_service?.State) - : containerStateIntention(service?.container?.state); + : service?.container?.state + ? containerStateIntention(service?.container?.state) + : isUnknown + ? "Unknown" + : "Neutral"; const color = hexColorByIntention(intention); return ( { const state = row.original.swarm_service?.State; return ( - + ); }, }, @@ -114,6 +124,7 @@ function StackServicesServer({ serverId: string; services: Types.ListStackServicesResponse; }) { + const isUnknown = useStack(stackId)?.info.state === Types.StackState.Unknown; return ( ); }, From 98761fb1df8b32ff49d2371c7f2aa42aaee6300a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 00:13:14 -0700 Subject: [PATCH 065/203] deploy 2.3.0-dev-19 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a99bcb58f..05592b75a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6f22414147..502bc20953 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-18" +version = "2.3.0-dev-19" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From f62dcb1540bac2c3ac0ad297bada71001739c07f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 01:07:44 -0700 Subject: [PATCH 066/203] mogh ui 0.7.0 including login page back button if already logged in --- Cargo.lock | 8 +- Cargo.toml | 4 +- client/core/ts/package.json | 2 +- client/core/ts/yarn.lock | 8 +- ui/package.json | 32 +-- ui/yarn.lock | 449 ++++++++++++++++++------------------ 6 files changed, 254 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05592b75a8..0ff8837e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3756,9 +3756,9 @@ dependencies = [ [[package]] name = "mogh_auth_client" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6b46f9e9317ea68ddf608ebd187d0d3ed6adb6d12c15dcc84ea70428e8745e" +checksum = "d4b22cf7d0f27783c7bb10c7f87fd7de3952bda65e2166d34c3285c698b00d89" dependencies = [ "anyhow", "mogh_error", @@ -3774,9 +3774,9 @@ dependencies = [ [[package]] name = "mogh_auth_server" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51cb74a7edc51da8ef00767691d071fcab84c9dc36ceb8f1ec6b0c514ec7de9b" +checksum = "a97971cfe27a593a432d773b37764d37819984fbfaff57c11d55730467f2a6ba" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 502bc20953..b85ac8e996 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,8 @@ slack = { version = "2.0.0", package = "slack_client_rs", default-features = fal mogh_error = { version = "1.0.4", default-features = false } derive_default_builder = "0.1.8" async_timing_util = "1.1.0" -mogh_auth_client = "1.5.0" -mogh_auth_server = "1.6.0" +mogh_auth_client = "1.7.0" +mogh_auth_server = "1.7.0" mogh_secret_file = "1.0.1" mogh_validations = "1.0.1" mogh_rate_limit = "1.0.1" diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 7632d7cc6f..061356db56 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -13,7 +13,7 @@ "build": "tsc" }, "dependencies": { - "mogh_auth_client": "^1.5.0" + "mogh_auth_client": "^1.7.0" }, "devDependencies": { "typescript": "^6.0.2" diff --git a/client/core/ts/yarn.lock b/client/core/ts/yarn.lock index 45934a5002..64671311dc 100644 --- a/client/core/ts/yarn.lock +++ b/client/core/ts/yarn.lock @@ -7,10 +7,10 @@ jwt-decode@^4.0.0: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== -mogh_auth_client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.5.0.tgz#f54b8ae35648d5bf1e60d7b67fcdcef45b0ccd7a" - integrity sha512-eUQmx682AzJRaY4QJSALtIGxXzg2r2Tr8exnDeFMrNe0Ibn4Oqm3h9SkOzYiqqZjKQ13wvHm0RoxT2QKo+908g== +mogh_auth_client@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.0.tgz#a016c8dafd0f73633098d41aa95889c444e481d9" + integrity sha512-v4TJVNIiFYBLd70yGw/eS37E15he4I5w3poj5X7W75D+m6ry9gbhtlePp5elRbqjDZ/WXkOZ/XYCHqGYb/PQ2A== dependencies: jwt-decode "^4.0.0" diff --git a/ui/package.json b/ui/package.json index 82453f9c9e..ed8c52d1ec 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,13 +10,13 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@mantine/core": "^9.3.2", - "@mantine/form": "^9.3.2", - "@mantine/hooks": "^9.3.2", - "@mantine/notifications": "^9.3.2", - "@mantine/spotlight": "^9.3.2", + "@mantine/core": "^9.4.1", + "@mantine/form": "^9.4.1", + "@mantine/hooks": "^9.4.1", + "@mantine/notifications": "^9.4.1", + "@mantine/spotlight": "^9.4.1", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.101.0", + "@tanstack/react-query": "^5.101.2", "@tanstack/react-table": "^8.21.3", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", @@ -24,34 +24,34 @@ "jotai": "^2.20.1", "jotai-family": "^1.0.2", "jotai-location": "^0.6.2", - "lucide-react": "^1.21.0", - "mogh_auth_client": "^1.5.0", - "mogh_ui": "^0.6.1", + "lucide-react": "^1.22.0", + "mogh_auth_client": "^1.7.0", + "mogh_ui": "^0.7.0", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.5.1", "react": "^19.2.7", "react-dom": "^19.2.7", "react-minimal-pie-chart": "^9.1.2", - "react-router-dom": "^7.18.0", + "react-router-dom": "^7.18.1", "react-xtermjs": "^1.0.12", - "recharts": "^3.8.1", + "recharts": "^3.9.1", "sanitize-html": "^2.17.5", - "shell-quote": "^1.8.4" + "shell-quote": "^1.9.0" }, "devDependencies": { - "@types/node": "^26.0.0", + "@types/node": "^26.0.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/sanitize-html": "^2.16.1", "@types/shell-quote": "^1.7.5", - "@vitejs/plugin-react": "^6.0.2", + "@vitejs/plugin-react": "^6.0.3", "dotenv": "^17.4.2", - "postcss": "^8.5.15", + "postcss": "^8.5.16", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "sass-embedded": "^1.100.0", "typescript": "^6.0.3", - "vite": "^8.0.16" + "vite": "^8.1.2" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/ui/yarn.lock b/ui/yarn.lock index 1595be8885..304604f4bb 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -12,25 +12,25 @@ resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.11.0.tgz#3ec3985c9074b23aea337957225fe15a0e845f8e" integrity sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ== -"@emnapi/core@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" - integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== +"@emnapi/core@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6" + integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ== dependencies: - "@emnapi/wasi-threads" "1.2.1" + "@emnapi/wasi-threads" "1.2.2" tslib "^2.4.0" -"@emnapi/runtime@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" - integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== +"@emnapi/runtime@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24" + integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" - integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== +"@emnapi/wasi-threads@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a" + integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA== dependencies: tslib "^2.4.0" @@ -70,50 +70,50 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== -"@mantine/core@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.3.2.tgz#cb23bea516281d96ad86e543827580a84820ce05" - integrity sha512-Upy/Z9Sj2eW2dGrFgUy/2kISVsxMTBYTDfP2TFdsIA3PdPSBkdqfSOPX/ug3d3F3a4bnwQSqcO6+aPEpBIh8dg== +"@mantine/core@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.4.1.tgz#071684a01ad302116bd3ed2d3727d873f7baabdd" + integrity sha512-lZWEICrum4+vwKxzh/mk4RB1N6BqZ0Cshdl6lhm7OYLiQzmOaxKtOgyaWz+vr65G8mqTXjalZalB+wmMVBYK2Q== dependencies: "@floating-ui/react" "^0.27.19" clsx "^2.1.1" react-number-format "^5.4.5" react-remove-scroll "^2.7.2" - type-fest "^5.6.0" + type-fest "^5.7.0" -"@mantine/form@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.3.2.tgz#4aed07aa4aae09fa96921101bde1892fcfbf120f" - integrity sha512-jWEXcx8hhddOsny+q/lFtVToNS3VBJYICrkfFMWJsN+WAZamnPItWTPLqulTYVp1T8uma9MsEZ1LX4kqWUFANA== +"@mantine/form@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.4.1.tgz#e5ef1f08e58ecdcbcea7e77eb910e5770d8cd5fd" + integrity sha512-e4tqC/D8ii7maPySWIWY306MNljPRprHPhu0/jaZrX7sRyJy+C8V9aU+ZFKYliM1CeiH1UX88+q4hWFUWVEdQw== dependencies: "@standard-schema/spec" "^1.1.0" fast-deep-equal "^3.1.3" klona "^2.0.6" -"@mantine/hooks@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.3.2.tgz#1a53767d3d2ec983c105498bf4e3805f08d1ddc3" - integrity sha512-jOjpUe0x1A/k3XUiu2/aaSCasXRI5ZKZOucm3ypwsvm9F2u8C1xzwBvagrzlbNZwJRF5vNYIJtCaT7NunPyc0A== +"@mantine/hooks@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.4.1.tgz#3d04f2f1d09df3ba962aa2724898fa95a2150973" + integrity sha512-eTI8wmzPx3r98zgKIEuvukmoGTHBhmtI6+9E6o2DbTmEU2eM1bCdjE2vFdf0op2AlRO0KEYEcZhNQi4T/SJk0A== -"@mantine/notifications@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.3.2.tgz#c608bb5af801c7091f9eb2c15be51d1ec8251363" - integrity sha512-mnda8hE/DizPi5Bbiwd2M2jXxfSZSUeayvnLYMbFweGQKXM+QRmX1ootyDRVqWew8paltewTNp84cWLjj/6fKw== +"@mantine/notifications@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.4.1.tgz#c730908c11b59f09502153c573d36a05513b34c6" + integrity sha512-Sm1g8E8RkFmesxpNz9zFvOjkTVxOa7dJJjMGoHNIXH+9/wO0s9kInny4mVnxTbLJJsD6nTqIQlk1v8tq2C4Ftw== dependencies: - "@mantine/store" "9.3.2" + "@mantine/store" "9.4.1" react-transition-group "4.4.5" -"@mantine/spotlight@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.3.2.tgz#3cab5fe280b1612577b8d0b18314ff287873e7ff" - integrity sha512-JAJhW51qvYNiPTEnX9ewZX521gKDsVsmaecuuR2iNyk0NbrOPB5SWF9ZZrPSMwFVtqnqYHziq5BLukzlf+kG/Q== +"@mantine/spotlight@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.4.1.tgz#f67c89bb2fa4a68c47385439c95fd892e74dfd60" + integrity sha512-gJ+7DdnGULeobtGXVGbVgvJSG3TeecxLXNxzpZo+ohl1OetPftCBGJOUrX2tYOOC6nJE1eHSIr/kpB31YIRjhQ== dependencies: - "@mantine/store" "9.3.2" + "@mantine/store" "9.4.1" -"@mantine/store@9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.3.2.tgz#2b878741b04147fdaecb40fed0c157eefde95e3b" - integrity sha512-uNmvjkwfUHZJ5bvzQroLFvubuUAHjBM0rh69RBRsUCG5FR5K4f8y4SX8+lch0wS6UJuuvodsSIK8H5jvZuFSgQ== +"@mantine/store@9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.4.1.tgz#c071271624c70acca1cb432d8c26d8a10c2ff452" + integrity sha512-/DCZq0aqqIdm03J0RJ7pbzoau2cwY8ndSOjskHOKHhTeWMnOBdCKXA3oMO5aIDldDqJ9T7Io4Boh4Pt461zNQw== "@monaco-editor/loader@^1.5.0": version "1.7.0" @@ -129,17 +129,17 @@ dependencies: "@monaco-editor/loader" "^1.5.0" -"@napi-rs/wasm-runtime@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" - integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== +"@napi-rs/wasm-runtime@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5" + integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg== dependencies: - "@tybys/wasm-util" "^0.10.1" + "@tybys/wasm-util" "^0.10.3" -"@oxc-project/types@=0.133.0": - version "0.133.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8" - integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA== +"@oxc-project/types@=0.137.0": + version "0.137.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.137.0.tgz#56e77f8bb221fa05f18b1cd34d73f94f0954a773" + integrity sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA== "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" @@ -242,86 +242,86 @@ redux-thunk "^3.1.0" reselect "^5.1.0" -"@rolldown/binding-android-arm64@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592" - integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw== - -"@rolldown/binding-darwin-arm64@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc" - integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA== - -"@rolldown/binding-darwin-x64@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad" - integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg== - -"@rolldown/binding-freebsd-x64@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1" - integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g== - -"@rolldown/binding-linux-arm-gnueabihf@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f" - integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw== - -"@rolldown/binding-linux-arm64-gnu@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1" - integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw== - -"@rolldown/binding-linux-arm64-musl@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069" - integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q== - -"@rolldown/binding-linux-ppc64-gnu@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8" - integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg== - -"@rolldown/binding-linux-s390x-gnu@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763" - integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg== - -"@rolldown/binding-linux-x64-gnu@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7" - integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg== - -"@rolldown/binding-linux-x64-musl@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3" - integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow== - -"@rolldown/binding-openharmony-arm64@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309" - integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg== - -"@rolldown/binding-wasm32-wasi@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b" - integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg== - dependencies: - "@emnapi/core" "1.10.0" - "@emnapi/runtime" "1.10.0" - "@napi-rs/wasm-runtime" "^1.1.4" - -"@rolldown/binding-win32-arm64-msvc@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad" - integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g== - -"@rolldown/binding-win32-x64-msvc@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb" - integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA== - -"@rolldown/pluginutils@^1.0.0": +"@rolldown/binding-android-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz#cc6153029c3d9afc9caaae2dc362d899ae94ac4f" + integrity sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g== + +"@rolldown/binding-darwin-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz#3af681a5d7610340257b3ac7753353b23e884765" + integrity sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw== + +"@rolldown/binding-darwin-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz#80ada35e9f35efb7e48a887444ce2052f615d645" + integrity sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw== + +"@rolldown/binding-freebsd-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz#65b2bbb82f005f08aeeff0b6d81e19be68360201" + integrity sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw== + +"@rolldown/binding-linux-arm-gnueabihf@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz#7e8d34ad0c7bcfd3baed268e9798571e3888ca71" + integrity sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg== + +"@rolldown/binding-linux-arm64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz#52bbf400ff219bda1e56c042160d96deb08bfecc" + integrity sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA== + +"@rolldown/binding-linux-arm64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz#9e82899186f73329f3d8155fa1618ae2e86ffa2a" + integrity sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w== + +"@rolldown/binding-linux-ppc64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz#050520177316586ccad816eb466ea11015e17ba7" + integrity sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw== + +"@rolldown/binding-linux-s390x-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz#50efa7b20219c6e31235fded0fd3427f36123e5a" + integrity sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA== + +"@rolldown/binding-linux-x64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz#c5973445113dff50d4077d0edaa4b8a69533dc6f" + integrity sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg== + +"@rolldown/binding-linux-x64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz#ddb023f7fc98ccbb8c1b683545216ca7b4e7ebdd" + integrity sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g== + +"@rolldown/binding-openharmony-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz#832a6da3472722427c73d178c75681858b76aeed" + integrity sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ== + +"@rolldown/binding-wasm32-wasi@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz#256cdcc06ad9ada611606526f319642fc0830b0f" + integrity sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg== + dependencies: + "@emnapi/core" "1.11.1" + "@emnapi/runtime" "1.11.1" + "@napi-rs/wasm-runtime" "^1.1.6" + +"@rolldown/binding-win32-arm64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz#e735c7024a5e17ebaf13689112fa0bdfc6886c38" + integrity sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g== + +"@rolldown/binding-win32-x64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz#f06c09db5c8ad4b6904b4d406c9b6f17f392b5c6" + integrity sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA== + +"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== @@ -336,17 +336,17 @@ resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== -"@tanstack/query-core@5.101.0": - version "5.101.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.101.0.tgz#45391ce143c270b7e7a55bd1a6696918f20782d1" - integrity sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow== +"@tanstack/query-core@5.101.2": + version "5.101.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.101.2.tgz#c901fb76b68169ff5e1e44017404e7e2e90ce1af" + integrity sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw== -"@tanstack/react-query@^5.101.0": - version "5.101.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.101.0.tgz#2dd56c5b96ec816d6b6ec9cee5c80ed298974733" - integrity sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg== +"@tanstack/react-query@^5.101.2": + version "5.101.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.101.2.tgz#d886331bcd29df84bc04c45d93192b8207d17c7f" + integrity sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg== dependencies: - "@tanstack/query-core" "5.101.0" + "@tanstack/query-core" "5.101.2" "@tanstack/react-table@^8.21.3": version "8.21.3" @@ -360,10 +360,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== -"@tybys/wasm-util@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" - integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== +"@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== dependencies: tslib "^2.4.0" @@ -418,10 +418,10 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== -"@types/node@^26.0.0": - version "26.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.0.tgz#d4aece9e9412e9f2008d59bc2d74f5279316b665" - integrity sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA== +"@types/node@^26.0.1": + version "26.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3" + integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw== dependencies: undici-types "~8.3.0" @@ -459,12 +459,12 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== -"@vitejs/plugin-react@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz#f70cb8ed0ce225dbc3055d78070f820d8aa35eda" - integrity sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg== +"@vitejs/plugin-react@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz#55f1d7f558534d10aef03c007dc208b7c3771ce4" + integrity sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg== dependencies: - "@rolldown/pluginutils" "^1.0.0" + "@rolldown/pluginutils" "^1.0.1" "@vscode/l10n@^0.0.18": version "0.0.18" @@ -736,16 +736,16 @@ htmlparser2@^10.1, htmlparser2@^10.1.0: domutils "^3.2.2" entities "^7.0.1" -immer@^10.1.1: - version "10.2.0" - resolved "https://registry.yarnpkg.com/immer/-/immer-10.2.0.tgz#88a4ce06a1af64172d254b70f7cb04df51c871b1" - integrity sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw== - immer@^11.0.0: version "11.1.4" resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.4.tgz#37aee86890b134a8f1a2fadd44361fb86c6ae67e" integrity sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw== +immer@^11.1.8: + version "11.1.8" + resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.8.tgz#08a6426f7019dbce8d6dff8c4a43bb25c550a575" + integrity sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA== + immutable@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" @@ -896,27 +896,27 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lucide-react@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.21.0.tgz#ef99ccf355af9bf5aec74ac2f122f7da1a9ea608" - integrity sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ== +lucide-react@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.22.0.tgz#f2930b21a1d3941fbee8a1017a5973d06ec4860b" + integrity sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg== marked@14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/marked/-/marked-14.0.0.tgz#79a1477358a59e0660276f8fec76de2c33f35d83" integrity sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ== -mogh_auth_client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.5.0.tgz#f54b8ae35648d5bf1e60d7b67fcdcef45b0ccd7a" - integrity sha512-eUQmx682AzJRaY4QJSALtIGxXzg2r2Tr8exnDeFMrNe0Ibn4Oqm3h9SkOzYiqqZjKQ13wvHm0RoxT2QKo+908g== +mogh_auth_client@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.0.tgz#a016c8dafd0f73633098d41aa95889c444e481d9" + integrity sha512-v4TJVNIiFYBLd70yGw/eS37E15he4I5w3poj5X7W75D+m6ry9gbhtlePp5elRbqjDZ/WXkOZ/XYCHqGYb/PQ2A== dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.6.1.tgz#c03db66bc4f830706bb17c2733eb541566930b29" - integrity sha512-OpEN7HAYOvj/RWLOtkNem4eU82etribTQrfVi0bmNyBh5Zw0GY2FoanAHU0cCB2a5Q2rZD6keWb2axQhWog30A== +mogh_ui@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.0.tgz#a2049bd0b33cf9e76c1cf9afb08062e0d9074806" + integrity sha512-epMZtg+wLBsIJ8vm048nD1qfvEau106asNbMjT6/Qv6ebF0lqvynk7K3vGbcAhOvVEFK0tde51CmfY/UcXt2+Q== monaco-editor@^0.55.1: version "0.55.1" @@ -1070,10 +1070,10 @@ postcss@^8.3.11: picocolors "^1.1.1" source-map-js "^1.2.1" -postcss@^8.5.15: - version "8.5.15" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" - integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== +postcss@^8.5.16: + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== dependencies: nanoid "^3.3.12" picocolors "^1.1.1" @@ -1149,17 +1149,17 @@ react-remove-scroll@^2.7.2: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router-dom@^7.18.0: - version "7.18.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.0.tgz#71a657374610e00cebf2783804d7812facfdb1b5" - integrity sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA== +react-router-dom@^7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.1.tgz#0d1b138e291393059ad481c3e10e366385a978a4" + integrity sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg== dependencies: - react-router "7.18.0" + react-router "7.18.1" -react-router@7.18.0: - version "7.18.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.0.tgz#e7d94b54745277aabe3cf93fac938cbebc9c1c5e" - integrity sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ== +react-router@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.1.tgz#61259d1594b95c1ace299ee4c57453570f0c22f1" + integrity sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -1197,19 +1197,19 @@ readdirp@^5.0.0: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== -recharts@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.8.1.tgz#1784b14784dab9a27eb426c475e6a9187f14cf01" - integrity sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg== +recharts@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.9.1.tgz#6fe1b4617147e2016fa6f4cd681c8c818255955e" + integrity sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg== dependencies: "@reduxjs/toolkit" "^1.9.0 || 2.x.x" clsx "^2.1.1" decimal.js-light "^2.5.1" es-toolkit "^1.39.3" eventemitter3 "^5.0.1" - immer "^10.1.1" + immer "^11.1.8" react-redux "8.x.x || 9.x.x" - reselect "5.1.1" + reselect "5.2.0" tiny-invariant "^1.3.3" use-sync-external-store "^1.2.2" victory-vendor "^37.0.2" @@ -1224,34 +1224,39 @@ redux@^5.0.1: resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== -reselect@5.1.1, reselect@^5.1.0: +reselect@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.2.0.tgz#f380ef7664332d26ea06c1cba04bdbbdcaa955f1" + integrity sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw== + +reselect@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== -rolldown@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" - integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g== +rolldown@~1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.3.tgz#87072bfd0d1bdd02a66076a261a62e8e49b3f0e2" + integrity sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g== dependencies: - "@oxc-project/types" "=0.133.0" + "@oxc-project/types" "=0.137.0" "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.3" - "@rolldown/binding-darwin-arm64" "1.0.3" - "@rolldown/binding-darwin-x64" "1.0.3" - "@rolldown/binding-freebsd-x64" "1.0.3" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.3" - "@rolldown/binding-linux-arm64-gnu" "1.0.3" - "@rolldown/binding-linux-arm64-musl" "1.0.3" - "@rolldown/binding-linux-ppc64-gnu" "1.0.3" - "@rolldown/binding-linux-s390x-gnu" "1.0.3" - "@rolldown/binding-linux-x64-gnu" "1.0.3" - "@rolldown/binding-linux-x64-musl" "1.0.3" - "@rolldown/binding-openharmony-arm64" "1.0.3" - "@rolldown/binding-wasm32-wasi" "1.0.3" - "@rolldown/binding-win32-arm64-msvc" "1.0.3" - "@rolldown/binding-win32-x64-msvc" "1.0.3" + "@rolldown/binding-android-arm64" "1.1.3" + "@rolldown/binding-darwin-arm64" "1.1.3" + "@rolldown/binding-darwin-x64" "1.1.3" + "@rolldown/binding-freebsd-x64" "1.1.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.1.3" + "@rolldown/binding-linux-arm64-gnu" "1.1.3" + "@rolldown/binding-linux-arm64-musl" "1.1.3" + "@rolldown/binding-linux-ppc64-gnu" "1.1.3" + "@rolldown/binding-linux-s390x-gnu" "1.1.3" + "@rolldown/binding-linux-x64-gnu" "1.1.3" + "@rolldown/binding-linux-x64-musl" "1.1.3" + "@rolldown/binding-openharmony-arm64" "1.1.3" + "@rolldown/binding-wasm32-wasi" "1.1.3" + "@rolldown/binding-win32-arm64-msvc" "1.1.3" + "@rolldown/binding-win32-x64-msvc" "1.1.3" rxjs@^7.4.0: version "7.8.2" @@ -1420,10 +1425,10 @@ set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== -shell-quote@^1.8.4: - version "1.8.4" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190" - integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ== +shell-quote@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57" + integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" @@ -1500,10 +1505,10 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -type-fest@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.6.0.tgz#502f7a003b7309e96a7e17052cc2ab2c7e5c7a31" - integrity sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA== +type-fest@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" + integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== dependencies: tagged-tag "^1.0.0" @@ -1567,15 +1572,15 @@ victory-vendor@^37.0.2: d3-time "^3.0.0" d3-timer "^3.0.1" -vite@^8.0.16: - version "8.0.16" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6" - integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== +vite@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.1.2.tgz#3ac29b5868ccf28c59321391be1ebe906f135ebd" + integrity sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ== dependencies: lightningcss "^1.32.0" picomatch "^4.0.4" - postcss "^8.5.15" - rolldown "1.0.3" + postcss "^8.5.16" + rolldown "~1.1.3" tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" From ad970e14704249b35ddbddd342e31cf912dedfe7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 01:08:12 -0700 Subject: [PATCH 067/203] deploy 2.3.0-dev-20 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ff8837e20..7180ca2ae5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index b85ac8e996..7af1561022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-19" +version = "2.3.0-dev-20" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From b17b5be8547ac31b523aca58ff2accdd5aa938f1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 01:19:07 -0700 Subject: [PATCH 068/203] fix containers pagination boundary --- ui/src/pages/containers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index a130788670..705e5bfbe4 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -220,7 +220,7 @@ export default function Containers() { /> {/* PAGINATION */} = 300 ? page + 2 : page + 1} + total={containers.length >= 100 ? page + 2 : page + 1} value={page + 1} onChange={(page) => setPage(page - 1)} > From 05da009ce3c0701f0fe1899e084bff883c401985 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 09:45:05 -0700 Subject: [PATCH 069/203] fix update permissions on swarm --- bin/core/src/api/write/permissions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs index ad9724f50c..27e4981976 100644 --- a/bin/core/src/api/write/permissions.rs +++ b/bin/core/src/api/write/permissions.rs @@ -347,9 +347,9 @@ async fn extract_resource_target_with_validation( .find_one(filter) .await .context("Failed to query db for swarms")? - .context("No matching server found")? + .context("No matching swarm found")? .id; - Ok((ResourceTargetVariant::Server, id)) + Ok((ResourceTargetVariant::Swarm, id)) } ResourceTarget::Server(ident) => { let filter = match ObjectId::from_str(ident) { From 1dcc0cf0ad0856c11bcb8fec46917b61aab86401 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 09:45:32 -0700 Subject: [PATCH 070/203] deploy 2.3.0-dev-21 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7180ca2ae5..50fe6bb3e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7af1561022..803876f8fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-20" +version = "2.3.0-dev-21" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From daa5d55f1a1ca81d0314259b5097dbae1d0a0657 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 20:29:49 -0700 Subject: [PATCH 071/203] skip alerting check on initial server cache refresh after startup --- bin/core/src/monitor/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs index bc6be6ba3e..eb00500001 100644 --- a/bin/core/src/monitor/mod.rs +++ b/bin/core/src/monitor/mod.rs @@ -52,17 +52,18 @@ pub fn spawn_monitoring_loops() { fn spawn_server_monitoring_loop() { tokio::spawn(async move { - refresh_all_server_cache(komodo_timestamp()).await; + refresh_all_server_cache().await; let interval = monitoring_interval(); loop { let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await - ADDITIONAL_MS) as i64; - refresh_all_server_cache(ts).await; + refresh_all_server_cache().await; + tokio::join!(check_alerts(ts), record_server_stats(ts)); } }); } -async fn refresh_all_server_cache(ts: i64) { +async fn refresh_all_server_cache() { let servers = match find_collect(&db_client().servers, None, None).await { Ok(servers) => servers, @@ -77,7 +78,6 @@ async fn refresh_all_server_cache(ts: i64) { refresh_server_cache(&server, false).await; }); join_all(futures).await; - tokio::join!(check_alerts(ts), record_server_stats(ts)); } /// Makes sure cache for server doesn't update too frequently / simultaneously. From 0b2b4fe15fb43f43788028bcee20e9ee7274ee35 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 20:38:17 -0700 Subject: [PATCH 072/203] deploy 2.3.0-dev-22 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50fe6bb3e8..77d23ff956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 803876f8fc..68d28d3666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-21" +version = "2.3.0-dev-22" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 3850b3596abdefe8abdedd571e8765ad568b9a22 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 1 Jul 2026 23:25:59 -0700 Subject: [PATCH 073/203] Fix build - deployment flow when build doesn't publish semver image tags --- bin/core/src/api/execute/deployment.rs | 16 +--- client/core/rs/src/entities/build.rs | 117 +++++++++++++++++++------ 2 files changed, 94 insertions(+), 39 deletions(-) diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index e21f0694ce..1ca4df4db0 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -122,26 +122,16 @@ impl Resolve for Deploy { let (version, registry_token) = match &deployment.config.image { DeploymentImage::Build { build_id, version } => { let build = resource::get::(build_id).await?; - let image_names = build.get_image_names(); - let image_name = image_names - .first() - .context("No image name could be created") - .context("Failed to create image name")?; let version = if version.is_none() { build.config.version } else { *version }; - let version_str = version.to_string(); - // Potentially add the build image_tag postfix - let version_str = if build.config.image_tag.is_empty() { - version_str - } else { - format!("{version_str}-{}", build.config.image_tag) - }; + let image_name = build.get_deployment_image_name(); + let image_tag = build.get_deployment_image_tag(version); // replace image with corresponding build image. deployment.config.image = DeploymentImage::Image { - image: format!("{image_name}:{version_str}"), + image: format!("{image_name}:{image_tag}"), }; let first_registry = build .config diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index d2ae5ae794..a403178340 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -56,30 +56,7 @@ impl Build { } image_registry .iter() - .map( - |ImageRegistryConfig { - domain, - account, - organization, - }| { - match ( - !domain.is_empty(), - !organization.is_empty(), - !account.is_empty(), - ) { - // If organization and account provided, name under organization. - (true, true, true) => { - format!("{domain}/{organization}/{name}") - } - // Just domain / account provided - (true, false, true) => { - format!("{domain}/{account}/{name}") - } - // Otherwise, just use name (local only) - _ => name.to_string(), - } - }, - ) + .map(|registry| registry.full_image_name(name)) .collect() } @@ -93,7 +70,7 @@ impl Build { version, image_tag, include_latest_tag, - include_version_tags: include_version_tag, + include_version_tags, include_commit_tag, .. } = &self.config; @@ -118,7 +95,7 @@ impl Build { tags.push(format!("{image_name}:latest{image_tag_postfix}")); } // `:1.19.5` + `:1.19` etc. / `1.19.5-tag` - if *include_version_tag { + if *include_version_tags { tags .push(format!("{image_name}:{version}{image_tag_postfix}")); tags.push(format!( @@ -152,6 +129,70 @@ impl Build { } Ok(res) } + + /// Used in build -> deployment flow to choose + /// the associated image to deploy. + pub fn get_deployment_image_name(&self) -> String { + let Build { + name, + config: + BuildConfig { + image_name, + image_registry, + .. + }, + .. + } = self; + let name = if image_name.is_empty() { + name + } else { + image_name + }; + if let Some(registry) = image_registry.first() { + registry.full_image_name(name) + } else { + name.to_string() + } + } + + /// Used in build -> deployment flow to choose the + /// associated latest tag. + /// + /// Priority: + /// - Semver version + /// - Commit hash + /// - Latest tag + pub fn get_deployment_image_tag(&self, version: Version) -> String { + let Build { + config: + BuildConfig { + image_tag, + include_version_tags, + include_commit_tag, + repo, + linked_repo, + .. + }, + .. + } = self; + + let image_tag_postfix = if image_tag.is_empty() { + format_args!("") + } else { + format_args!("-{image_tag}") + }; + + if *include_version_tags { + format!("{version}{image_tag_postfix}") + } else if (!repo.is_empty() || !linked_repo.is_empty()) + && *include_commit_tag + && let Some(hash) = &self.info.built_hash + { + format!("{hash}{image_tag_postfix}") + } else { + String::from("latest") + } + } } #[typeshare] @@ -631,6 +672,30 @@ impl ImageRegistryConfig { static DEFAULT: OnceLock = OnceLock::new(); DEFAULT.get_or_init(Default::default) } + + pub fn full_image_name(&self, short_name: &str) -> String { + let Self { + domain, + organization, + account, + } = self; + match ( + !domain.is_empty(), + !organization.is_empty(), + !account.is_empty(), + ) { + // If organization and account provided, name under organization. + (true, true, true) => { + format!("{domain}/{organization}/{short_name}") + } + // Just domain / account provided + (true, false, true) => { + format!("{domain}/{account}/{short_name}") + } + // Otherwise, just use name (local only) + _ => short_name.to_string(), + } + } } #[typeshare] From f7ba3b6b33371c0e54003cf037dd8a600796c48e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 2 Jul 2026 23:55:44 -0700 Subject: [PATCH 074/203] move check button to left of update available button --- ui/src/resources/stack/update-available.tsx | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/src/resources/stack/update-available.tsx b/ui/src/resources/stack/update-available.tsx index f99f1a38fa..9950ffce52 100644 --- a/ui/src/resources/stack/update-available.tsx +++ b/ui/src/resources/stack/update-available.tsx @@ -109,18 +109,6 @@ export default function StackUpdateAvailable({ return ( <> - - - {updateAvailable && ( )} + + + ); } From 37df1c224da307591539e692d3031929ad4f6689 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 3 Jul 2026 11:26:27 -0700 Subject: [PATCH 075/203] omnisearch containers only fetch when query types --- ui/src/app/topbar/omni-search/hooks.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 721b82e760..2d8eb4c366 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -64,6 +64,8 @@ export function useOmniSearch(): { const containers = useRead("ListAllDockerContainers", containersQuery, { refetchInterval: 15_000, + // Only fetch when there is query typed + enabled: !!debouncedTerms.length, }).data; const servicesQuery: Types.ListAllStackServices = useMemo( @@ -79,6 +81,8 @@ export function useOmniSearch(): { const services = useRead("ListAllStackServices", servicesQuery, { refetchInterval: 15_000, + // Only fetch when there is query typed + enabled: !!debouncedTerms.length, }).data; const _terminals = useRead( From 18a310064c9243a8b88f26226750418d59ba800f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 00:53:17 -0700 Subject: [PATCH 076/203] Add cancel procedure / action apis --- bin/cli/src/command/execute.rs | 2 + bin/core/src/api/execute/action.rs | 94 +++++++++++++++++-- bin/core/src/api/execute/build.rs | 4 +- bin/core/src/api/execute/mod.rs | 2 + bin/core/src/api/execute/procedure.rs | 61 +++++++++++- bin/core/src/helpers/procedure.rs | 22 +++++ bin/core/src/helpers/update.rs | 2 + bin/core/src/resource/procedure.rs | 3 + bin/core/src/state.rs | 16 ++++ client/core/rs/src/api/execute/action.rs | 34 +++++++ client/core/rs/src/api/execute/mod.rs | 2 + client/core/rs/src/api/execute/openapi.rs | 2 + client/core/rs/src/api/execute/procedure.rs | 35 +++++++ client/core/rs/src/entities/mod.rs | 2 + client/core/rs/src/entities/update.rs | 17 ++-- client/core/ts/src/responses.ts | 2 + client/core/ts/src/types.ts | 32 +++++++ ui/public/client/responses.d.ts | 2 + ui/public/client/types.d.ts | 38 ++++++++ ui/public/client/types.js | 2 + .../resources/procedure/config/executions.tsx | 2 + 21 files changed, 353 insertions(+), 23 deletions(-) diff --git a/bin/cli/src/command/execute.rs b/bin/cli/src/command/execute.rs index 87abeb9552..ad49395487 100644 --- a/bin/cli/src/command/execute.rs +++ b/bin/cli/src/command/execute.rs @@ -81,7 +81,9 @@ pub async fn handle( let res = handle_execution!( execute: [ RunAction, + CancelAction, RunProcedure, + CancelProcedure, RunBuild, CancelBuild, Deploy, diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 536c726f35..5cc1c20ed6 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -4,23 +4,26 @@ use std::{ sync::OnceLock, }; -use anyhow::Context; +use anyhow::Context as _; use command::{CommandOptions, run_komodo_standard_command}; -use database::mungos::{ - by_id::update_one_by_id, mongodb::bson::to_document, +use database::{ + bson::doc, + mungos::{by_id::update_one_by_id, mongodb::bson::to_document}, }; use interpolate::Interpolator; use komodo_client::{ - api::execute::{BatchExecutionResponse, BatchRunAction, RunAction}, + api::execute::{ + BatchExecutionResponse, BatchRunAction, CancelAction, RunAction, + }, entities::{ - FileFormat, JsonObject, + FileFormat, JsonObject, Operation, action::Action, alert::{Alert, AlertData, SeverityLevel}, config::core::CoreConfig, komodo_timestamp, permission::PermissionLevel, random_string, - update::Update, + update::{Update, UpdateStatus}, user::action_user, }, parsers::parse_key_value_list, @@ -34,6 +37,7 @@ use mogh_auth_server::api::manage::api_key::{ use mogh_config::merge_objects; use mogh_resolver::Resolve; use tokio::fs; +use tokio_util::sync::CancellationToken; use crate::{ alert::send_alerts, @@ -46,7 +50,7 @@ use crate::{ }, permission::get_check_permissions, resource::refresh_action_state_cache, - state::{action_states, db_client}, + state::{action_cancel_cache, action_states, db_client}, }; use super::ExecuteArgs; @@ -197,6 +201,12 @@ impl Resolve for RunAction { "" }; + let cancel = CancellationToken::new(); + + action_cancel_cache() + .insert(update.id.clone(), cancel.clone()) + .await; + let mut res = run_komodo_standard_command( // Keep this stage name as is, the UI will find the latest update log by matching the stage name "Execute Action", @@ -204,7 +214,7 @@ impl Resolve for RunAction { "deno run --allow-all{https_cert_flag}{reload} {}", path.display() ), - CommandOptions::default(), + CommandOptions::default().cancel(cancel), ) .await; @@ -218,7 +228,7 @@ impl Resolve for RunAction { update.logs.push(res); update.finalize(); - mogh_error::Ok(update) + mogh_error::Ok(()) } .await; @@ -231,7 +241,9 @@ impl Resolve for RunAction { ); }; - let update = res?; + action_cancel_cache().remove(&update.id).await; + + res?; // Need to manually update the update before cache refresh, // and before broadcast with update_update. @@ -459,3 +471,65 @@ fn parse_action_arguments( .context("Failed to parse Json to action args"), } } + +impl Resolve for CancelAction { + #[instrument( + "CancelAction", + skip_all, + fields( + task_id = task_id.to_string(), + operator = user.id, + update_id = update.id, + action = self.action, + ) + )] + async fn resolve( + self, + ExecuteArgs { + user, + update, + task_id, + }: &ExecuteArgs, + ) -> Result { + let action = get_check_permissions::( + &self.action, + user, + PermissionLevel::Execute.into(), + ) + .await?; + + let update_id = if let Some(update_id) = self.update_id + && !update_id.is_empty() + { + update_id + } else { + db_client() + .updates + .find_one(doc! { + "target.type": "Action", + "target.id": &action.id, + "operation": Operation::RunAction.as_ref(), + "status": UpdateStatus::InProgress.as_ref(), + }) + .await? + .context("No active run to cancel found")? + .id + }; + + action_cancel_cache() + .get(&update_id) + .await + .context("Action run cancel token not found")? + .cancel(); + + let mut update = update.clone(); + update.push_simple_log( + "Cancel Triggered", + "The action cancel has been triggered", + ); + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index cc0b00f468..be539d39f7 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -561,8 +561,8 @@ impl Resolve for CancelBuild { let mut update = update.clone(); update.push_simple_log( - "cancel triggered", - "the build cancel has been triggered", + "Cancel Triggered", + "The build cancel has been triggered", ); update_update(update.clone()).await?; diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index 502120b092..e313b0ecd8 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -111,10 +111,12 @@ pub enum ExecuteRequest { // ==== PROCEDURE ==== RunProcedure(RunProcedure), BatchRunProcedure(BatchRunProcedure), + CancelProcedure(CancelProcedure), // ==== ACTION ==== RunAction(RunAction), BatchRunAction(BatchRunAction), + CancelAction(CancelAction), // ==== SYNC ==== RunSync(RunSync), diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index c659eba8ba..1178d436ec 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -1,12 +1,14 @@ use std::pin::Pin; +use anyhow::Context as _; use database::mungos::{ by_id::update_one_by_id, mongodb::bson::to_document, }; use formatting::{Color, bold, colored, format_serror, muted}; use komodo_client::{ api::execute::{ - BatchExecutionResponse, BatchRunProcedure, RunProcedure, + BatchExecutionResponse, BatchRunProcedure, CancelProcedure, + RunProcedure, }, entities::{ alert::{Alert, AlertData, SeverityLevel}, @@ -19,13 +21,14 @@ use komodo_client::{ }; use mogh_resolver::Resolve; use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use crate::{ alert::send_alerts, helpers::{procedure::execute_procedure, update::update_update}, permission::get_check_permissions, resource::refresh_procedure_state_cache, - state::{action_states, db_client}, + state::{action_states, db_client, procedure_cancel_cache}, }; use super::{ExecuteArgs, ExecuteRequest}; @@ -122,9 +125,17 @@ fn resolve_inner( update_update(update.clone()).await?; + let cancel = CancellationToken::new(); + + procedure_cancel_cache() + .insert(procedure.id.clone(), cancel.clone()) + .await; + let update = Mutex::new(update); - let res = execute_procedure(&procedure, &update).await; + let res = execute_procedure(&procedure, &update, cancel).await; + + procedure_cancel_cache().remove(&procedure.id).await; let mut update = update.into_inner(); @@ -184,3 +195,47 @@ fn resolve_inner( Ok(update) }) } + +impl Resolve for CancelProcedure { + #[instrument( + "CancelProcedure", + skip_all, + fields( + task_id = task_id.to_string(), + operator = user.id, + update_id = update.id, + procedure = self.procedure, + ) + )] + async fn resolve( + self, + ExecuteArgs { + user, + update, + task_id, + }: &ExecuteArgs, + ) -> Result { + let procedure = get_check_permissions::( + &self.procedure, + user, + PermissionLevel::Execute.into(), + ) + .await?; + + procedure_cancel_cache() + .get(&procedure.id) + .await + .context("Procedure run cancel token not found")? + .cancel(); + + let mut update = update.clone(); + update.push_simple_log( + "Cancel Triggered", + "The procedure cancel has been triggered", + ); + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index b06b3497df..74721d2f87 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -20,6 +20,7 @@ use komodo_client::{ }; use mogh_resolver::Resolve; use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use uuid::Uuid; use crate::{ @@ -36,8 +37,21 @@ use super::update::{init_execution_update, update_update}; pub async fn execute_procedure( procedure: &Procedure, update: &Mutex, + cancel: CancellationToken, ) -> anyhow::Result<()> { for stage in &procedure.config.stages { + if cancel.is_cancelled() { + add_line_to_update( + update, + &format!( + "{}: Cancelled before stage: '{}'", + muted("ERROR"), + bold(&stage.name) + ), + ) + .await; + return Err(anyhow!("Procedure cancelled before completion")); + } if !stage.enabled { continue; } @@ -274,6 +288,9 @@ async fn execute_execution( } resolve_execute!(RunProcedure, req) } + Execution::CancelProcedure(req) => { + resolve_execute!(CancelProcedure, req) + } // Special: write operation Execution::CommitSync(req) => req .resolve(&WriteArgs { user }) @@ -326,6 +343,9 @@ async fn execute_execution( } // Standard executions Execution::RunAction(req) => resolve_execute!(RunAction, req), + Execution::CancelAction(req) => { + resolve_execute!(CancelAction, req) + } Execution::RunBuild(req) => resolve_execute!(RunBuild, req), Execution::CancelBuild(req) => resolve_execute!(CancelBuild, req), Execution::Deploy(req) => resolve_execute!(Deploy, req), @@ -721,7 +741,9 @@ pub fn replace_procedure_stage_ids_with_names( replace_id_with_name!( RunProcedure => procedure, procedures; + CancelProcedure => procedure, procedures; RunAction => action, actions; + CancelAction => action, actions; RunBuild => build, builds; CancelBuild => build, builds; Deploy => deployment, deployments; diff --git a/bin/core/src/helpers/update.rs b/bin/core/src/helpers/update.rs index 03b727c783..61b43d8a03 100644 --- a/bin/core/src/helpers/update.rs +++ b/bin/core/src/helpers/update.rs @@ -226,8 +226,10 @@ pub async fn init_execution_update( (CancelRepoBuild, Repo, repo), // Procedure (RunProcedure, Procedure, procedure), + (CancelProcedure, Procedure, procedure), // Action (RunAction, Action, action), + (CancelAction, Action, action), // Resource Sync (RunSync, ResourceSync, sync), // Stack (simple) diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 3ee1f277f3..dd1c831466 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -272,6 +272,9 @@ async fn validate_config( execute: [ // Action (RunAction, Action, action), + (CancelAction, Action, action), + // Procedure + (CancelProcedure, Procedure, procedure), // Build (RunBuild, Build, build), (CancelBuild, Build, build), diff --git a/bin/core/src/state.rs b/bin/core/src/state.rs index 21290db152..d413c99ee0 100644 --- a/bin/core/src/state.rs +++ b/bin/core/src/state.rs @@ -19,6 +19,7 @@ use komodo_client::entities::{ swarm::SwarmState, }; use mogh_cache::CloneCache; +use tokio_util::sync::CancellationToken; use crate::{ config::core_config, @@ -235,3 +236,18 @@ pub fn builder_usage_cache() -> &'static BuilderUsageCache { OnceLock::new(); BUILDER_USAGE_CACHE.get_or_init(Default::default) } + +type CancelCache = CloneCache; + +/// Maps procedure id => CancellationToken +pub fn procedure_cancel_cache() -> &'static CancelCache { + static PROCEDURE_CANCEL_CACHE: OnceLock = + OnceLock::new(); + PROCEDURE_CANCEL_CACHE.get_or_init(Default::default) +} + +/// Maps update id => CancellationToken +pub fn action_cancel_cache() -> &'static CancelCache { + static ACTION_CANCEL_CACHE: OnceLock = OnceLock::new(); + ACTION_CANCEL_CACHE.get_or_init(Default::default) +} diff --git a/client/core/rs/src/api/execute/action.rs b/client/core/rs/src/api/execute/action.rs index 02fd43452e..b94a0baa5f 100644 --- a/client/core/rs/src/api/execute/action.rs +++ b/client/core/rs/src/api/execute/action.rs @@ -86,3 +86,37 @@ pub struct BatchRunAction { /// ``` pub pattern: String, } + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/CancelAction", + description = "Cancel an action run.", + request_body(content = CancelAction), + responses( + (status = 200, description = "The update", body = Update), + ), +)] +pub fn cancel_action() {} + +/// Cancels the target action run. Response: [Update] +#[typeshare] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Resolve, Parser, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +#[error(mogh_error::Error)] +pub struct CancelAction { + /// Id or name + pub action: String, + /// The update id associated with the specific + /// run to cancel + /// Must provide either `action` + /// or `update_id` + pub update_id: Option, +} diff --git a/client/core/rs/src/api/execute/mod.rs b/client/core/rs/src/api/execute/mod.rs index 963a035599..38b15d696f 100644 --- a/client/core/rs/src/api/execute/mod.rs +++ b/client/core/rs/src/api/execute/mod.rs @@ -139,12 +139,14 @@ pub enum Execution { #[clap(alias = "procedure", alias = "pr")] RunProcedure(RunProcedure), BatchRunProcedure(BatchRunProcedure), + CancelProcedure(CancelProcedure), // ACTION /// Run the target action. (alias: `action`, `ac`) #[clap(alias = "action", alias = "ac")] RunAction(RunAction), BatchRunAction(BatchRunAction), + CancelAction(CancelAction), // SYNC /// Execute a Resource Sync. (alias: `sync`) diff --git a/client/core/rs/src/api/execute/openapi.rs b/client/core/rs/src/api/execute/openapi.rs index d820c53d72..2ba6f02329 100644 --- a/client/core/rs/src/api/execute/openapi.rs +++ b/client/core/rs/src/api/execute/openapi.rs @@ -79,9 +79,11 @@ use crate::api::execute; // procedure execute::run_procedure, execute::batch_run_procedure, + execute::cancel_procedure, // action execute::run_action, execute::batch_run_action, + execute::cancel_action, // alerter execute::test_alerter, execute::send_alert, diff --git a/client/core/rs/src/api/execute/procedure.rs b/client/core/rs/src/api/execute/procedure.rs index c04ce92ae2..2ea51d1e21 100644 --- a/client/core/rs/src/api/execute/procedure.rs +++ b/client/core/rs/src/api/execute/procedure.rs @@ -73,3 +73,38 @@ pub struct BatchRunProcedure { /// ``` pub pattern: String, } + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/CancelProcedure", + description = "Cancel an procedure run.", + request_body(content = CancelProcedure), + responses( + (status = 200, description = "The update", body = Update), + ), +)] +pub fn cancel_procedure() {} + +/// Cancels the target procedure run. Response: [Update] +#[typeshare] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Resolve, Parser, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +#[error(mogh_error::Error)] +pub struct CancelProcedure { + /// Id or name + pub procedure: String, + /// The update id associated with the specific + /// run to cancel + /// Must provide either `action` + /// or `update_id` + #[arg(long, short = 'u')] + pub update_id: Option, +} diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index cd3a31b0e1..e159b41bf3 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -1281,6 +1281,7 @@ pub enum Operation { RenameProcedure, DeleteProcedure, RunProcedure, + CancelProcedure, // Action CreateAction, @@ -1288,6 +1289,7 @@ pub enum Operation { RenameAction, DeleteAction, RunAction, + CancelAction, // Sync CreateResourceSync, diff --git a/client/core/rs/src/entities/update.rs b/client/core/rs/src/entities/update.rs index 448f1234e1..257fb47b93 100644 --- a/client/core/rs/src/entities/update.rs +++ b/client/core/rs/src/entities/update.rs @@ -1,6 +1,6 @@ use async_timing_util::unix_timestamp_ms; use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; +use strum::{AsRefStr, Display, EnumString}; use typeshare::typeshare; use crate::entities::{ @@ -208,17 +208,18 @@ impl Log { /// An update's status #[typeshare] #[derive( + Default, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, Serialize, Deserialize, - Debug, Display, EnumString, - PartialEq, - Hash, - Eq, - Clone, - Copy, - Default, + AsRefStr, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum UpdateStatus { diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 4434f3084b..98641c8f60 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -397,10 +397,12 @@ export type ExecuteResponses = { // ==== PROCEDURE ==== RunProcedure: Types.Update; BatchRunProcedure: Types.BatchExecutionResponse; + CancelProcedure: Types.Update; // ==== ACTION ==== RunAction: Types.Update; BatchRunAction: Types.BatchExecutionResponse; + CancelAction: Types.Update; // ==== SYNC ==== RunSync: Types.Update; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 462c35ac6a..1dd355527b 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -471,11 +471,13 @@ export enum Operation { RenameProcedure = "RenameProcedure", DeleteProcedure = "DeleteProcedure", RunProcedure = "RunProcedure", + CancelProcedure = "CancelProcedure", CreateAction = "CreateAction", UpdateAction = "UpdateAction", RenameAction = "RenameAction", DeleteAction = "DeleteAction", RunAction = "RunAction", + CancelAction = "CancelAction", CreateResourceSync = "CreateResourceSync", UpdateResourceSync = "UpdateResourceSync", RenameResourceSync = "RenameResourceSync", @@ -898,9 +900,11 @@ export type Execution = /** Run the target procedure. (alias: `procedure`, `pr`) */ | { type: "RunProcedure", params: RunProcedure } | { type: "BatchRunProcedure", params: BatchRunProcedure } + | { type: "CancelProcedure", params: CancelProcedure } /** Run the target action. (alias: `action`, `ac`) */ | { type: "RunAction", params: RunAction } | { type: "BatchRunAction", params: BatchRunAction } + | { type: "CancelAction", params: CancelAction } /** Execute a Resource Sync. (alias: `sync`) */ | { type: "RunSync", params: RunSync } /** Commit a Resource Sync. (alias: `commit`) */ @@ -6250,6 +6254,19 @@ export interface BuildStatsDay { ts: number; } +/** Cancels the target action run. Response: [Update] */ +export interface CancelAction { + /** Id or name */ + action: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} + /** * Cancels the target build. * Only does anything if the build is `building` when called. @@ -6260,6 +6277,19 @@ export interface CancelBuild { build: string; } +/** Cancels the target procedure run. Response: [Update] */ +export interface CancelProcedure { + /** Id or name */ + procedure: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} + /** * Cancels the target repo build. * Only does anything if the repo build is `building` when called. @@ -10831,8 +10861,10 @@ export type ExecuteRequest = | { type: "CancelRepoBuild", params: CancelRepoBuild } | { type: "RunProcedure", params: RunProcedure } | { type: "BatchRunProcedure", params: BatchRunProcedure } + | { type: "CancelProcedure", params: CancelProcedure } | { type: "RunAction", params: RunAction } | { type: "BatchRunAction", params: BatchRunAction } + | { type: "CancelAction", params: CancelAction } | { type: "RunSync", params: RunSync } | { type: "TestAlerter", params: TestAlerter } | { type: "SendAlert", params: SendAlert } diff --git a/ui/public/client/responses.d.ts b/ui/public/client/responses.d.ts index a10ea90866..0c2e987529 100644 --- a/ui/public/client/responses.d.ts +++ b/ui/public/client/responses.d.ts @@ -294,8 +294,10 @@ export type ExecuteResponses = { CancelRepoBuild: Types.Update; RunProcedure: Types.Update; BatchRunProcedure: Types.BatchExecutionResponse; + CancelProcedure: Types.Update; RunAction: Types.Update; BatchRunAction: Types.BatchExecutionResponse; + CancelAction: Types.Update; RunSync: Types.Update; TestAlerter: Types.Update; SendAlert: Types.Update; diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index e30fe3dee0..dd2111a4f5 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -473,11 +473,13 @@ export declare enum Operation { RenameProcedure = "RenameProcedure", DeleteProcedure = "DeleteProcedure", RunProcedure = "RunProcedure", + CancelProcedure = "CancelProcedure", CreateAction = "CreateAction", UpdateAction = "UpdateAction", RenameAction = "RenameAction", DeleteAction = "DeleteAction", RunAction = "RunAction", + CancelAction = "CancelAction", CreateResourceSync = "CreateResourceSync", UpdateResourceSync = "UpdateResourceSync", RenameResourceSync = "RenameResourceSync", @@ -966,6 +968,9 @@ export type Execution = } | { type: "BatchRunProcedure"; params: BatchRunProcedure; +} | { + type: "CancelProcedure"; + params: CancelProcedure; } /** Run the target action. (alias: `action`, `ac`) */ | { @@ -974,6 +979,9 @@ export type Execution = } | { type: "BatchRunAction"; params: BatchRunAction; +} | { + type: "CancelAction"; + params: CancelAction; } /** Execute a Resource Sync. (alias: `sync`) */ | { @@ -6034,6 +6042,18 @@ export interface BuildStatsDay { count: number; ts: number; } +/** Cancels the target action run. Response: [Update] */ +export interface CancelAction { + /** Id or name */ + action: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} /** * Cancels the target build. * Only does anything if the build is `building` when called. @@ -6043,6 +6063,18 @@ export interface CancelBuild { /** Can be id or name */ build: string; } +/** Cancels the target procedure run. Response: [Update] */ +export interface CancelProcedure { + /** Id or name */ + procedure: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} /** * Cancels the target repo build. * Only does anything if the repo build is `building` when called. @@ -10306,12 +10338,18 @@ export type ExecuteRequest = { } | { type: "BatchRunProcedure"; params: BatchRunProcedure; +} | { + type: "CancelProcedure"; + params: CancelProcedure; } | { type: "RunAction"; params: RunAction; } | { type: "BatchRunAction"; params: BatchRunAction; +} | { + type: "CancelAction"; + params: CancelAction; } | { type: "RunSync"; params: RunSync; diff --git a/ui/public/client/types.js b/ui/public/client/types.js index cd205174e7..70868097ce 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -163,11 +163,13 @@ export var Operation; Operation["RenameProcedure"] = "RenameProcedure"; Operation["DeleteProcedure"] = "DeleteProcedure"; Operation["RunProcedure"] = "RunProcedure"; + Operation["CancelProcedure"] = "CancelProcedure"; Operation["CreateAction"] = "CreateAction"; Operation["UpdateAction"] = "UpdateAction"; Operation["RenameAction"] = "RenameAction"; Operation["DeleteAction"] = "DeleteAction"; Operation["RunAction"] = "RunAction"; + Operation["CancelAction"] = "CancelAction"; Operation["CreateResourceSync"] = "CreateResourceSync"; Operation["UpdateResourceSync"] = "UpdateResourceSync"; Operation["RenameResourceSync"] = "RenameResourceSync"; diff --git a/ui/src/resources/procedure/config/executions.tsx b/ui/src/resources/procedure/config/executions.tsx index 4161be4810..c780806145 100644 --- a/ui/src/resources/procedure/config/executions.tsx +++ b/ui/src/resources/procedure/config/executions.tsx @@ -52,6 +52,8 @@ export type ProcedureMinExecutionType = Exclude< | "CreateSwarmSecret" | "RotateSwarmSecret" | "RemoveSwarmSecrets" + | "CancelProcedure" + | "CancelAction" >; export type ProcedureExecutionParams = From 1e74c96138444179f06b3365d9361d1201ecb2cc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 10:57:14 -0700 Subject: [PATCH 077/203] add procedure / action cancel buttons --- bin/core/src/api/execute/action.rs | 2 +- bin/core/src/api/execute/procedure.rs | 2 +- ui/src/resources/action/executions.tsx | 44 +++++++++++++++++++++ ui/src/resources/action/index.tsx | 32 ++------------- ui/src/resources/procedure/executions.tsx | 47 ++++++++++++++++------- 5 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 ui/src/resources/action/executions.tsx diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 5cc1c20ed6..9b8ffdcca9 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -525,7 +525,7 @@ impl Resolve for CancelAction { let mut update = update.clone(); update.push_simple_log( "Cancel Triggered", - "The action cancel has been triggered", + "The action cancel has been triggered.", ); update.finalize(); update_update(update.clone()).await?; diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index 1178d436ec..bbb9132bb4 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -231,7 +231,7 @@ impl Resolve for CancelProcedure { let mut update = update.clone(); update.push_simple_log( "Cancel Triggered", - "The procedure cancel has been triggered", + "Procedure cancel has been triggered. The procedure will exit after the currently running stage is complete.", ); update.finalize(); update_update(update.clone()).await?; diff --git a/ui/src/resources/action/executions.tsx b/ui/src/resources/action/executions.tsx new file mode 100644 index 0000000000..4c1a1cb243 --- /dev/null +++ b/ui/src/resources/action/executions.tsx @@ -0,0 +1,44 @@ +import { useExecute, useRead } from "@/lib/hooks"; +import { useAction } from "."; +import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; +import { ICONS } from "@/lib/icons"; +import { ConfirmButton } from "mogh_ui"; + +export function RunAction({ id }: { id: string }) { + const running = + (useRead("GetActionActionState", { action: id }, { refetchInterval: 5000 }) + .data?.running ?? 0) > 0; + const { mutateAsync: run, isPending: runPending } = useExecute("RunAction"); + const { mutateAsync: cancel, isPending: cancelPending } = + useExecute("CancelAction"); + const action = useAction(id); + + if (!action) { + return null; + } + + if (running) { + return ( + } + onClick={() => cancel({ action: id })} + loading={cancelPending} + > + Cancel Run + + ); + } else { + return ( + } + confirmText={action.name} + onConfirm={() => run({ action: id, args: {} })} + loading={runPending} + > + Run Action + + ); + } +} diff --git a/ui/src/resources/action/index.tsx b/ui/src/resources/action/index.tsx index 584725277b..9420864e11 100644 --- a/ui/src/resources/action/index.tsx +++ b/ui/src/resources/action/index.tsx @@ -1,5 +1,5 @@ import { actionStateIntention } from "@/lib/color"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -8,12 +8,12 @@ import { Badge, Group, Popover, Text } from "@mantine/core"; import { Clock } from "lucide-react"; import { useDisclosure } from "@mantine/hooks"; import { updateLogToHtml } from "@/lib/utils"; -import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; import NewResource from "@/resources/new"; import ActionConfig from "./config"; import ActionTable from "./table"; import ResourceHeader from "../header"; import BatchExecutions from "@/components/batch-executions"; +import { RunAction } from "./executions"; export function useAction(id: string | undefined, useName?: boolean) { return useRead("ListActions", {}).data?.find((r) => @@ -148,33 +148,7 @@ export const ActionComponents: RequiredResourceComponents< }, Executions: { - RunAction: ({ id }) => { - const running = - (useRead( - "GetActionActionState", - { action: id }, - { refetchInterval: 5000 }, - ).data?.running ?? 0) > 0; - const { mutateAsync, isPending } = useExecute("RunAction"); - const action = useAction(id); - - if (!action) { - return null; - } - - return ( - } - confirmText={action.name} - onConfirm={async () => { - await mutateAsync({ action: id, args: {} }); - }} - loading={running || isPending} - > - {running ? "Running" : "Run Action"} - - ); - }, + RunAction, }, Config: ActionConfig, diff --git a/ui/src/resources/procedure/executions.tsx b/ui/src/resources/procedure/executions.tsx index a5deb8212c..ede63a52e3 100644 --- a/ui/src/resources/procedure/executions.tsx +++ b/ui/src/resources/procedure/executions.tsx @@ -2,6 +2,7 @@ import { useExecute, useRead } from "@/lib/hooks"; import { useProcedure } from "."; import { ICONS } from "@/lib/icons"; import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; +import { ConfirmButton } from "mogh_ui"; export function RunProcedure({ id }: { id: string }) { const running = useRead( @@ -9,18 +10,38 @@ export function RunProcedure({ id }: { id: string }) { { procedure: id }, { refetchInterval: 5000 }, ).data?.running; - const { mutateAsync: run, isPending } = useExecute("RunProcedure"); + const { mutateAsync: run, isPending: runPending } = + useExecute("RunProcedure"); + const { mutateAsync: cancel, isPending: cancelPending } = + useExecute("CancelProcedure"); const procedure = useProcedure(id); - if (!procedure) return null; - return ( - } - onConfirm={() => run({ procedure: id })} - disabled={running || isPending} - loading={running || isPending} - > - {running ? "Running" : "Run Procedure"} - - ); + + if (!procedure) { + return null; + } + + if (running) { + return ( + } + onClick={() => cancel({ procedure: id })} + loading={cancelPending} + > + Cancel Run + + ); + } else { + return ( + } + confirmText={procedure.name} + onConfirm={() => run({ procedure: id })} + loading={runPending} + > + Run Procedure + + ); + } } From a4ba2b4404a8f40ce03f6a0f25514fcd1b47a3d1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 10:57:45 -0700 Subject: [PATCH 078/203] deploy 2.3.0-dev-23 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77d23ff956..b35bf55362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 68d28d3666..5021297cc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-22" +version = "2.3.0-dev-23" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 13ae29eda4de0e36024f4d07dfa09b4f710e13fa Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 11:12:52 -0700 Subject: [PATCH 079/203] Execution error --- bin/core/src/api/execute/procedure.rs | 2 +- bin/core/src/helpers/procedure.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index bbb9132bb4..ab66bdbf6c 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -151,7 +151,7 @@ fn resolve_inner( ); } Err(e) => update - .push_error_log("execution error", format_serror(&e.into())), + .push_error_log("Execution error", format_serror(&e.into())), } update.finalize(); diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 74721d2f87..1e5389cb3d 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -521,14 +521,13 @@ async fn handle_resolve_result( match res { Ok(res) => Ok(res), Err(e) => { - let log = - Log::error("execution error", format_serror(&e.into())); let mut update = find_one_by_id(&db_client().updates, update_id) .await .context("Failed to query to db")? .context("no update exists with given id")?; - update.logs.push(log); + update + .push_error_log("Execution error", format_serror(&e.into())); update.finalize(); update_update(update.clone()).await?; Ok(update) From d76fa44ff9a7e6b3435f28fe2f02e98442c9d6e8 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 11:34:57 -0700 Subject: [PATCH 080/203] useIsCancelling hook for better state conveyance --- bin/core/src/helpers/procedure.rs | 2 +- ui/src/lib/hooks.ts | 35 +++++++++++++++++++++++ ui/src/resources/action/executions.tsx | 10 +++++-- ui/src/resources/procedure/executions.tsx | 10 +++++-- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 1e5389cb3d..13e894d346 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -14,7 +14,7 @@ use komodo_client::{ procedure::{Procedure, ProcedureStage}, repo::Repo, stack::Stack, - update::{Log, Update}, + update::Update, user::procedure_user, }, }; diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 621bf241f5..035eb0fb8e 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -571,3 +571,38 @@ function addUserTargetPermissions( } }); } + +/** + * Checks if target operation is cancelling + * by querying relevant Updates on the target. + * If a 'Cancel' operation is more recent than + * an InProgress 'Run' operation, the target + * is cancelling. + */ +export function useIsCancelling( + target: Types.ResourceTarget, + runOp: Types.Operation, + cancelOp: Types.Operation, +) { + const updates = useRead("ListUpdates", { + query: { + "target.type": target.type, + "target.id": target.id, + }, + }).data?.updates; + if (!updates) { + return true; + } + let hasCancel = false; + for (const update of updates) { + if (update.operation === cancelOp) { + hasCancel = true; + } else if ( + update.operation === runOp && + update.status === Types.UpdateStatus.InProgress + ) { + return hasCancel; + } + } + return false; +} diff --git a/ui/src/resources/action/executions.tsx b/ui/src/resources/action/executions.tsx index 4c1a1cb243..90759e5dc5 100644 --- a/ui/src/resources/action/executions.tsx +++ b/ui/src/resources/action/executions.tsx @@ -1,8 +1,9 @@ -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useIsCancelling, useRead } from "@/lib/hooks"; import { useAction } from "."; import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; import { ICONS } from "@/lib/icons"; import { ConfirmButton } from "mogh_ui"; +import { Types } from "komodo_client"; export function RunAction({ id }: { id: string }) { const running = @@ -12,6 +13,11 @@ export function RunAction({ id }: { id: string }) { const { mutateAsync: cancel, isPending: cancelPending } = useExecute("CancelAction"); const action = useAction(id); + const cancelling = useIsCancelling( + { type: "Action", id }, + Types.Operation.RunAction, + Types.Operation.CancelAction, + ); if (!action) { return null; @@ -24,7 +30,7 @@ export function RunAction({ id }: { id: string }) { color="red" icon={} onClick={() => cancel({ action: id })} - loading={cancelPending} + loading={cancelPending || cancelling} > Cancel Run diff --git a/ui/src/resources/procedure/executions.tsx b/ui/src/resources/procedure/executions.tsx index ede63a52e3..be63f4e179 100644 --- a/ui/src/resources/procedure/executions.tsx +++ b/ui/src/resources/procedure/executions.tsx @@ -1,8 +1,9 @@ -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useIsCancelling, useRead } from "@/lib/hooks"; import { useProcedure } from "."; import { ICONS } from "@/lib/icons"; import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; import { ConfirmButton } from "mogh_ui"; +import { Types } from "komodo_client"; export function RunProcedure({ id }: { id: string }) { const running = useRead( @@ -15,6 +16,11 @@ export function RunProcedure({ id }: { id: string }) { const { mutateAsync: cancel, isPending: cancelPending } = useExecute("CancelProcedure"); const procedure = useProcedure(id); + const cancelling = useIsCancelling( + { type: "Procedure", id }, + Types.Operation.RunProcedure, + Types.Operation.CancelProcedure, + ); if (!procedure) { return null; @@ -27,7 +33,7 @@ export function RunProcedure({ id }: { id: string }) { color="red" icon={} onClick={() => cancel({ procedure: id })} - loading={cancelPending} + loading={cancelPending || cancelling} > Cancel Run From dded910ea3320a807a8cbe4152a6118ae69f6d54 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 12:18:16 -0700 Subject: [PATCH 081/203] fmt mem module --- bin/periphery/src/stats/mem.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/periphery/src/stats/mem.rs b/bin/periphery/src/stats/mem.rs index a351c42425..123f74e729 100644 --- a/bin/periphery/src/stats/mem.rs +++ b/bin/periphery/src/stats/mem.rs @@ -49,10 +49,11 @@ fn read_linux() -> Option { let meminfo = parse_meminfo(&std::fs::read_to_string("/proc/meminfo").ok()?)?; // Absent when ZFS isn't loaded => arc stays zero. - let zfs_arc = std::fs::read_to_string("/proc/spl/kstat/zfs/arcstats") - .ok() - .map(|c| parse_zfs_arc_size(&c)) - .unwrap_or(0); + let zfs_arc = + std::fs::read_to_string("/proc/spl/kstat/zfs/arcstats") + .ok() + .map(|c| parse_zfs_arc_size(&c)) + .unwrap_or(0); // Cached + SReclaimable + Buffers - Shmem let buff_cache = meminfo From 43ca25e55b5fdcf57ca47560bfd060eba49efacf Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 12:18:40 -0700 Subject: [PATCH 082/203] allow all builders build to cancel during build --- bin/core/src/api/execute/build.rs | 30 +++---- bin/periphery/src/api/build/mod.rs | 121 +++++++++++++++++++------- bin/periphery/src/api/mod.rs | 1 + bin/periphery/src/state.rs | 8 ++ client/periphery/rs/src/api/build.rs | 13 ++- ui/src/resources/build/executions.tsx | 55 +++++------- 6 files changed, 144 insertions(+), 84 deletions(-) diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index be539d39f7..bde3bee557 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -28,7 +28,7 @@ use komodo_client::{ alert::{Alert, AlertData, SeverityLevel}, all_logs_success, build::{Build, BuildConfig}, - builder::{Builder, BuilderConfig}, + builder::Builder, deployment::DeploymentState, komodo_timestamp, optional_string, permission::PermissionLevel, @@ -175,9 +175,6 @@ impl Resolve for RunBuild { let builder = resource::get::(&build.config.builder_id).await?; - let is_server_builder = - matches!(&builder.config, BuilderConfig::Server(_)); - tokio::spawn(async move { let poll = async { loop { @@ -186,19 +183,13 @@ impl Resolve for RunBuild { id = cancel_recv.recv() => id? }; if incoming_build_id == build_id { - if is_server_builder { - update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature."); - } else { - update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time."); - } + update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time."); update.finalize(); let id = update.id.clone(); if let Err(e) = update_update(update).await { warn!("Failed to modify Update {id} on db | {e:#}"); } - if !is_server_builder { - cancel_clone.cancel(); - } + cancel_clone.cancel(); return Ok(()); } } @@ -273,11 +264,11 @@ impl Resolve for RunBuild { replacers: Default::default(), }) => res, _ = cancel.cancelled() => { - debug!("Build cancelled during clone, cleaning up builder"); - update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone")); + debug!("Build cancelled during repo clone, cleaning up builder"); + update.push_error_log("Build cancelled", String::from("Build cancelled during repo clone")); cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; - info!("Builder cleaned up"); + debug!("Builder cleaned up"); return handle_early_return(update, build.id, build.name, true).await }, }; @@ -323,7 +314,14 @@ impl Resolve for RunBuild { }) => res.context("Failed at call to Periphery to build"), _ = cancel.cancelled() => { info!("Build cancelled during build, cleaning up builder"); - update.push_error_log("Build cancelled", String::from("User cancelled build during docker build")); + if let Err(e) = periphery.request(api::build::CancelBuild { + id: build.id.clone() + }) + .await + .context("Failed to cancel build execution on Server") { + update.push_error_log("Cancel Build", format_serror(&e.into())); + } + update.push_error_log("Build Cancelled", String::from("User cancelled build during image build step")); cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; return handle_early_return(update, build.id, build.name, true).await diff --git a/bin/periphery/src/api/build/mod.rs b/bin/periphery/src/api/build/mod.rs index 21368818d9..6bba92b612 100644 --- a/bin/periphery/src/api/build/mod.rs +++ b/bin/periphery/src/api/build/mod.rs @@ -11,7 +11,7 @@ use command::{ use formatting::format_serror; use interpolate::Interpolator; use komodo_client::entities::{ - EnvironmentVar, all_logs_success, + EnvironmentVar, NoData, all_logs_success, build::{Build, BuildConfig}, environment_vars_from_str, optional_string, to_path_compatible_name, @@ -19,17 +19,19 @@ use komodo_client::entities::{ }; use mogh_resolver::Resolve; use periphery_client::api::build::{ - self, GetDockerfileContentsOnHost, + self, CancelBuild, GetDockerfileContentsOnHost, GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx, WriteDockerfileContentsToHost, }; use tokio::fs; +use tokio_util::sync::CancellationToken; use tracing::Instrument; use crate::{ config::periphery_config, docker::docker_login, helpers::{format_extra_args, format_labels}, + state::build_cancel_cache, }; mod helpers; @@ -133,7 +135,8 @@ impl Resolve for build::Build { fields( id = args.id.to_string(), core = args.core, - build = self.build.name, + build_id = self.build.id, + build_name = self.build.name, repo = self.repo.as_ref().map(|repo| &repo.name), ) )] @@ -263,6 +266,11 @@ impl Resolve for build::Build { } }; + let cancel = CancellationToken::new(); + build_cancel_cache() + .insert(build.id.clone(), cancel.clone()) + .await; + // Pre Build if !pre_build.is_none() { let pre_build_path = build_path.join(&pre_build.path); @@ -270,7 +278,9 @@ impl Resolve for build::Build { if let Some(log) = run_komodo_command_with_sanitization( "Pre Build", &pre_build.command, - CommandOptions::default().path(pre_build_path.as_path()), + CommandOptions::default() + .path(pre_build_path.as_path()) + .cancel(cancel.clone()), if pre_build.shell_mode { KomodoCommandMode::Shell } else { @@ -284,53 +294,73 @@ impl Resolve for build::Build { let success = log.success; logs.push(log); if !success { + build_cancel_cache().remove(&build.id).await; return Ok(logs); } } } // Get command parts + // Do this in fallible block to ensure + // cancel token removed from cache on failure. + let command = async { + // Add VERSION to build args (if not already there) + let mut build_args = environment_vars_from_str(build_args) + .context("Invalid build_args")?; + if !build_args.iter().any(|a| a.variable == "VERSION") { + build_args.push(EnvironmentVar { + variable: String::from("VERSION"), + value: build.config.version.to_string(), + }); + } + let build_args = parse_build_args(&build_args); - // Add VERSION to build args (if not already there) - let mut build_args = environment_vars_from_str(build_args) - .context("Invalid build_args")?; - if !build_args.iter().any(|a| a.variable == "VERSION") { - build_args.push(EnvironmentVar { - variable: String::from("VERSION"), - value: build.config.version.to_string(), - }); - } - let build_args = parse_build_args(&build_args); + let secret_args = environment_vars_from_str(secret_args) + .context("Invalid secret_args")?; + let command_secret_args = + parse_secret_args(&secret_args, &build_path).await?; - let secret_args = environment_vars_from_str(secret_args) - .context("Invalid secret_args")?; - let command_secret_args = - parse_secret_args(&secret_args, &build_path).await?; + let labels = format_labels( + &environment_vars_from_str(labels) + .context("Invalid labels")?, + ); - let labels = format_labels( - &environment_vars_from_str(labels).context("Invalid labels")?, - ); + let extra_args = format_extra_args(extra_args); - let extra_args = format_extra_args(extra_args); + let buildx = if *use_buildx { " buildx" } else { "" }; - let buildx = if *use_buildx { " buildx" } else { "" }; + let image_tags = build + .get_image_tags_as_arg( + commit_hash.as_deref(), + &additional_tags, + ) + .context("Failed to parse image tags into command")?; - let image_tags = build - .get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags) - .context("Failed to parse image tags into command")?; + let maybe_push = if should_push { " --push" } else { "" }; - let maybe_push = if should_push { " --push" } else { "" }; + // Construct command + let command = format!( + "docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .", + ); - // Construct command - let command = format!( - "docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .", - ); + anyhow::Ok(command) + }.await; + + let command = match command { + Ok(command) => command, + Err(e) => { + build_cancel_cache().remove(&build.id).await; + return Err(e); + } + }; let span = info_span!("RunDockerBuild"); if let Some(build_log) = run_komodo_command_with_sanitization( "Docker Build", command, - CommandOptions::default().path(build_path.as_path()), + CommandOptions::default() + .path(build_path.as_path()) + .cancel(cancel), KomodoCommandMode::Shell, &replacers, ) @@ -340,12 +370,39 @@ impl Resolve for build::Build { logs.push(build_log); }; + build_cancel_cache().remove(&build.id).await; + Ok(logs) } } // +impl Resolve for CancelBuild { + #[instrument( + "CancelBuild", + skip_all, + fields( + build_id = self.id, + id = args.id.to_string(), + core = args.core, + ) + )] + async fn resolve( + self, + args: &crate::api::Args, + ) -> anyhow::Result { + build_cancel_cache() + .get(&self.id) + .await + .context("No running build found")? + .cancel(); + Ok(NoData {}) + } +} + +// + impl Resolve for PruneBuilders { #[instrument( "PruneBuilders", diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index 8aa54c4d49..bf4382aabd 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -68,6 +68,7 @@ pub enum PeripheryRequest { GetDockerfileContentsOnHost(GetDockerfileContentsOnHost), WriteDockerfileContentsToHost(WriteDockerfileContentsToHost), Build(Build), + CancelBuild(CancelBuild), PruneBuilders(PruneBuilders), PruneBuildx(PruneBuildx), diff --git a/bin/periphery/src/state.rs b/bin/periphery/src/state.rs index 08da16458d..c2457a900f 100644 --- a/bin/periphery/src/state.rs +++ b/bin/periphery/src/state.rs @@ -341,3 +341,11 @@ pub async fn host_public_ip() -> Option<&'static String> { .await .as_ref() } + +type CancelCache = CloneCache; + +/// Maps build id => CancellationToken +pub fn build_cancel_cache() -> &'static CancelCache { + static BUILD_CANCEL_CACHE: OnceLock = OnceLock::new(); + BUILD_CANCEL_CACHE.get_or_init(Default::default) +} diff --git a/client/periphery/rs/src/api/build.rs b/client/periphery/rs/src/api/build.rs index d4e8fb55e1..f42257d8e0 100644 --- a/client/periphery/rs/src/api/build.rs +++ b/client/periphery/rs/src/api/build.rs @@ -1,9 +1,11 @@ use komodo_client::entities::{ - FileContents, repo::Repo, update::Log, + FileContents, NoData, repo::Repo, update::Log, }; use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; +// + #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[response(BuildResponse)] #[error(anyhow::Error)] @@ -29,6 +31,15 @@ pub type BuildResponse = Vec; // +#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[response(NoData)] +#[error(anyhow::Error)] +pub struct CancelBuild { + pub id: String, +} + +// + /// Get the dockerfile contents on the host, for builds using /// `files_on_host`. #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] diff --git a/ui/src/resources/build/executions.tsx b/ui/src/resources/build/executions.tsx index 6894b0c714..ae62513937 100644 --- a/ui/src/resources/build/executions.tsx +++ b/ui/src/resources/build/executions.tsx @@ -1,7 +1,10 @@ import { Types } from "komodo_client"; -import { useExecute, usePermissions, useRead } from "@/lib/hooks"; -import { useBuilder } from "../builder"; -import { useBuild } from "."; +import { + useExecute, + useIsCancelling, + usePermissions, + useRead, +} from "@/lib/hooks"; import { ConfirmButton } from "mogh_ui"; import { ICONS } from "@/lib/icons"; @@ -12,45 +15,28 @@ export function RunBuild({ id }: { id: string }) { { build: id }, { refetchInterval: 5_000 }, ).data?.building; - const updates = useRead("ListUpdates", { - query: { - "target.type": "Build", - "target.id": id, - }, - }).data; - const { mutate: runBuild, isPending: runPending } = useExecute("RunBuild"); - const { mutate: cancelBuild, isPending: cancelPending } = + const { mutate: run, isPending: runPending } = useExecute("RunBuild"); + const { mutate: cancel, isPending: cancelPending } = useExecute("CancelBuild"); - const build = useBuild(id); - const builder = useBuilder(build?.info.builder_id); - const canCancel = builder?.info.builder_type === "Aws"; + const cancelling = useIsCancelling( + { type: "Build", id }, + Types.Operation.RunBuild, + Types.Operation.CancelBuild, + ); // make sure hidden without perms. - // not usually necessary, but this button also used in deployment actions. + // not usually necessary as execution area hidden without execute + // on the resource, but this button is also used in deployment executions. if (!canExecute) return null; - // updates come in in descending order, so 'find' will find latest update matching operation - const latestBuild = updates?.updates.find( - (u) => u.operation === Types.Operation.RunBuild, - ); - const latestCancel = updates?.updates.find( - (u) => u.operation === Types.Operation.CancelBuild, - ); - const cancelDisabled = - !canCancel || - cancelPending || - (latestCancel && latestBuild - ? latestCancel!.start_ts > latestBuild!.start_ts - : false); - - if (building && canCancel) { + if (building) { return ( } - onClick={() => cancelBuild({ build: id })} - disabled={cancelDisabled} + onClick={() => cancel({ build: id })} + loading={cancelPending || cancelling} > Cancel Build @@ -59,9 +45,8 @@ export function RunBuild({ id }: { id: string }) { return ( } - loading={runPending || building} - onClick={() => runBuild({ build: id })} - disabled={runPending || building} + onClick={() => run({ build: id })} + loading={runPending} > Build From 0acbd76b6b758bd2a44afbc2d5021388b9557af9 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 12:19:14 -0700 Subject: [PATCH 083/203] deploy 2.3.0-dev-24 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b35bf55362..02959ee994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5021297cc3..9d626e1aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-23" +version = "2.3.0-dev-24" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 1dc4602f7432b6fc815853e1f597e4147d867c74 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 12:41:25 -0700 Subject: [PATCH 084/203] bump rust to 1.96.1 --- bin/binaries.Dockerfile | 2 +- bin/chef.binaries.Dockerfile | 2 +- bin/cli/aio.Dockerfile | 2 +- bin/core/aio.Dockerfile | 2 +- bin/periphery/aio.Dockerfile | 2 +- example/alerter/Dockerfile | 2 +- example/update_logger/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/binaries.Dockerfile b/bin/binaries.Dockerfile index a3f0190075..b4633243f0 100644 --- a/bin/binaries.Dockerfile +++ b/bin/binaries.Dockerfile @@ -1,7 +1,7 @@ ## Builds the Komodo Core, Periphery, and Util binaries ## for a specific architecture. Requires OpenSSL 3 or later. -FROM rust:1.96.0-bookworm AS builder +FROM rust:1.96.1-bookworm AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/chef.binaries.Dockerfile b/bin/chef.binaries.Dockerfile index 77e699bff8..7c79f58ddf 100644 --- a/bin/chef.binaries.Dockerfile +++ b/bin/chef.binaries.Dockerfile @@ -3,7 +3,7 @@ ## Uses chef for dependency caching to help speed up back-to-back builds. -FROM lukemathwalker/cargo-chef:latest-rust-1.96.0-bookworm AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.96.1-bookworm AS chef WORKDIR /builder # Plan just the RECIPE to see if things have changed diff --git a/bin/cli/aio.Dockerfile b/bin/cli/aio.Dockerfile index 723cf58df7..5c3f5544bb 100644 --- a/bin/cli/aio.Dockerfile +++ b/bin/cli/aio.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.0-trixie AS builder +FROM rust:1.96.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index 1ba1869ed1..6c326cf689 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -1,7 +1,7 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. # Build Core -FROM rust:1.96.0-trixie AS core-builder +FROM rust:1.96.1-trixie AS core-builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/periphery/aio.Dockerfile b/bin/periphery/aio.Dockerfile index d4edd37c65..fa57040f66 100644 --- a/bin/periphery/aio.Dockerfile +++ b/bin/periphery/aio.Dockerfile @@ -1,6 +1,6 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. -FROM rust:1.96.0-trixie AS builder +FROM rust:1.96.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/example/alerter/Dockerfile b/example/alerter/Dockerfile index 1ef087454f..3a35e4c41c 100644 --- a/example/alerter/Dockerfile +++ b/example/alerter/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.0-trixie as builder +FROM rust:1.96.1-trixie as builder WORKDIR /builder COPY . . diff --git a/example/update_logger/Dockerfile b/example/update_logger/Dockerfile index 1e00187f62..b2073aedf1 100644 --- a/example/update_logger/Dockerfile +++ b/example/update_logger/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.0-trixie as builder +FROM rust:1.96.1-trixie as builder WORKDIR /builder COPY . . From 2c6d9f4d37536fa77e791a631dccd0733712a836 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 15:44:35 -0700 Subject: [PATCH 085/203] tweak ui index.html --- ui/index.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/index.html b/ui/index.html index 580ff00550..14427e763d 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,8 +2,7 @@ - - + @@ -13,7 +12,7 @@ Komodo - +
From ec65db12f37d5829be61d237f2e7d8d21dfd5d92 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 16:05:18 -0700 Subject: [PATCH 086/203] build webhook cancel build before triggering another - better behavior for back-to-back pushes --- bin/core/src/api/listener/resources.rs | 77 ++++++++++++++++++++------ 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/bin/core/src/api/listener/resources.rs b/bin/core/src/api/listener/resources.rs index 2f1be49641..c6be345cc4 100644 --- a/bin/core/src/api/listener/resources.rs +++ b/bin/core/src/api/listener/resources.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, sync::OnceLock}; +use std::{str::FromStr, sync::OnceLock, time::Duration}; use anyhow::{Context, anyhow}; use komodo_client::{ @@ -23,6 +23,7 @@ use crate::{ }, helpers::update::init_execution_update, resource, + state::action_states, }; use super::{ANY_BRANCH, ListenerLockCache}; @@ -37,11 +38,6 @@ impl super::CustomSecret for Build { } } -fn build_locks() -> &'static ListenerLockCache { - static BUILD_LOCKS: OnceLock = OnceLock::new(); - BUILD_LOCKS.get_or_init(Default::default) -} - pub async fn handle_build_webhook( build: Build, body: String, @@ -50,12 +46,6 @@ pub async fn handle_build_webhook( return Ok(()); } - // Acquire and hold lock to make a task queue for - // subsequent listener calls on same resource. - // It would fail if we let it go through from action state busy. - let lock = build_locks().get_or_insert_default(&build.id).await; - let _lock = lock.lock().await; - // Use the correct target branch when using linked repo. let branch = if build.config.linked_repo.is_empty() { build.config.branch @@ -69,13 +59,45 @@ pub async fn handle_build_webhook( B::verify_branch(&body, &branch)?; + // Cancel if currently building + if action_states() + .build + .get(&build.id) + .await + .and_then(|states| { + states.get().ok().map(|states| states.building) + }) + .unwrap_or_default() + { + let user = git_webhook_user().to_owned(); + let cancel = ExecuteRequest::CancelBuild(CancelBuild { + build: build.id.clone(), + }); + let update = init_execution_update(&cancel, &user).await?; + let ExecuteRequest::CancelBuild(cancel) = cancel else { + unreachable!() + }; + + cancel + .resolve(&ExecuteArgs { + user, + update, + task_id: Uuid::new_v4(), + }) + .await + .ok(); + + poll_build_until_cancelled(&build.id).await?; + } + let user = git_webhook_user().to_owned(); - let req = ExecuteRequest::RunBuild(RunBuild { build: build.id }); - let update = init_execution_update(&req, &user).await?; - let ExecuteRequest::RunBuild(req) = req else { + let run = ExecuteRequest::RunBuild(RunBuild { build: build.id }); + let update = init_execution_update(&run, &user).await?; + let ExecuteRequest::RunBuild(run) = run else { unreachable!() }; - req + + run .resolve(&ExecuteArgs { user, update, @@ -83,9 +105,32 @@ pub async fn handle_build_webhook( }) .await .map_err(|e| e.error)?; + Ok(()) } +async fn poll_build_until_cancelled( + build_id: &String, +) -> anyhow::Result<()> { + let action_states = action_states(); + // Poll to ensure cancelled + for _ in 0..10 { + if !action_states + .build + .get(build_id) + .await + .and_then(|states| { + states.get().ok().map(|states| states.building) + }) + .unwrap_or_default() + { + return Ok(()); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + Err(anyhow!("Build still running after cancel")) +} + // ====== // REPO // ====== From 199e352add4a3221902cb0a1a52d3485a6736783 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 5 Jul 2026 16:05:47 -0700 Subject: [PATCH 087/203] deploy 2.3.0-dev-25 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02959ee994..d4fc922c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 9d626e1aa9..2e3428b8f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-24" +version = "2.3.0-dev-25" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 6ca10c9e52c567807e830b2ac26c0a128126d8c7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 6 Jul 2026 11:37:04 -0700 Subject: [PATCH 088/203] De-vendor container apis from Docker --- bin/cli/src/command/container.rs | 10 +- bin/cli/src/command/terminal.rs | 4 +- bin/core/src/api/read/docker.rs | 56 +- bin/core/src/api/read/mod.rs | 74 +- bin/core/src/api/read/provider.rs | 8 +- bin/core/src/api/write/mod.rs | 9 +- bin/core/src/api/write/provider.rs | 16 +- bin/core/src/cloud/aws/ec2.rs | 2 +- bin/core/src/config.rs | 2 +- bin/core/src/helpers/mod.rs | 2 +- bin/periphery/src/api/mod.rs | 10 +- bin/periphery/src/config.rs | 2 +- bin/periphery/src/helpers.rs | 2 +- client/core/rs/src/api/read/docker.rs | 231 +++-- client/core/rs/src/api/read/mod.rs | 24 +- client/core/rs/src/api/read/openapi.rs | 30 +- client/core/rs/src/api/read/provider.rs | 48 +- client/core/rs/src/api/write/openapi.rs | 6 +- client/core/rs/src/api/write/provider.rs | 76 +- client/core/rs/src/entities/builder.rs | 16 +- client/core/rs/src/entities/config/core.rs | 17 +- client/core/rs/src/entities/config/mod.rs | 8 +- .../core/rs/src/entities/config/periphery.rs | 24 +- client/core/rs/src/entities/provider.rs | 12 +- client/core/ts/src/responses.ts | 34 +- client/core/ts/src/types.ts | 928 +++++++++--------- client/periphery/rs/src/api/mod.rs | 8 +- config/core.config.toml | 6 +- config/periphery.config.toml | 6 +- docsite/docs/configuration/providers.md | 6 +- lib/database/src/lib.rs | 4 +- ui/public/client/responses.d.ts | 34 +- ui/public/client/types.d.ts | 860 ++++++++-------- ui/public/client/types.js | 70 +- ui/public/schema/resources.json | 82 +- ui/src/app/topbar/omni-search/hooks.tsx | 4 +- ui/src/components/config/account-selector.tsx | 4 +- .../config/image-registry-config.tsx | 2 +- .../components/config/provider-selector.tsx | 4 +- .../components/docker/container-selector.tsx | 2 +- .../components/docker/containers-section.tsx | 4 +- ui/src/components/docker/link.tsx | 8 +- ui/src/lib/socket.tsx | 28 +- ui/src/pages/containers.tsx | 2 +- ui/src/pages/docker/container/executions.tsx | 2 +- ui/src/pages/docker/container/index.tsx | 4 +- ui/src/pages/docker/image.tsx | 6 +- ui/src/pages/docker/network.tsx | 2 +- ui/src/pages/docker/volume.tsx | 4 +- ui/src/pages/settings/providers/delete.tsx | 2 +- .../pages/settings/providers/from-config.tsx | 4 +- ui/src/pages/settings/providers/index.tsx | 8 +- ui/src/pages/settings/providers/new.tsx | 4 +- .../resources/deployment/config/network.tsx | 2 +- ui/src/resources/deployment/index.tsx | 2 +- ui/src/resources/server/docker/containers.tsx | 2 +- ui/src/resources/server/docker/images.tsx | 4 +- ui/src/resources/server/docker/networks.tsx | 2 +- ui/src/resources/server/docker/volumes.tsx | 2 +- ui/src/resources/server/index.tsx | 6 +- ui/src/resources/server/stats/containers.tsx | 2 +- 61 files changed, 1535 insertions(+), 1308 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index 35e7236a6a..8c714cf7fa 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -7,9 +7,7 @@ use futures_util::{ FutureExt, TryStreamExt, stream::FuturesUnordered, }; use komodo_client::{ - api::read::{ - InspectDockerContainer, ListAllDockerContainers, ListServers, - }, + api::read::{self, ListAllContainers, ListServers}, entities::{ config::cli::args::container::{ Container, ContainerCommand, InspectContainer, @@ -61,7 +59,7 @@ async fn list_containers( .into_iter() .map(|s| (s.id.clone(), s)) .collect::>())), - client.read(ListAllDockerContainers { + client.read(ListAllContainers { servers: Default::default(), tags: Default::default(), terms: terms.clone(), @@ -149,7 +147,7 @@ pub async fn inspect_container( .into_iter() .map(|s| (s.id.clone(), s)) .collect::>())), - client.read(ListAllDockerContainers { + client.read(ListAllContainers { servers: inspect.servers.clone(), tags: Default::default(), terms: vec![inspect.container.clone()], @@ -174,7 +172,7 @@ pub async fn inspect_container( .into_iter() .map(|c| async move { client - .read(InspectDockerContainer { + .read(read::InspectContainer { container: c.name, server: c.server_id.context("No server...")?, }) diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index 86420c63f6..7ce5f9cf42 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -2,7 +2,7 @@ use anyhow::{Context, anyhow}; use colored::Colorize; use komodo_client::{ api::{ - read::{GetServer, ListAllDockerContainers, ListServers}, + read::{GetServer, ListAllContainers, ListServers}, terminal::InitTerminal, }, entities::{ @@ -127,7 +127,7 @@ async fn get_server( let client = super::komodo_client().await?; let mut containers = client - .read(ListAllDockerContainers { + .read(ListAllContainers { servers: Default::default(), tags: Default::default(), terms: vec![container.to_string()], diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index f5702ad728..4c04045bbf 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -22,13 +22,7 @@ use komodo_client::{ }, }; use mogh_resolver::Resolve; -use periphery_client::api::{ - self as periphery, - container::InspectContainer, - docker::{ - ImageHistory, InspectImage, InspectNetwork, InspectVolume, - }, -}; +use periphery_client::api as periphery; use crate::{ api::read::ReadArgs, @@ -39,11 +33,11 @@ use crate::{ state::server_status_cache, }; -impl Resolve for GetDockerContainersSummary { +impl Resolve for GetContainersSummary { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let servers = resource::list_full_for_user::( Default::default(), user, @@ -53,7 +47,7 @@ impl Resolve for GetDockerContainersSummary { .await .context("failed to get servers from db")?; - let mut res = GetDockerContainersSummaryResponse::default(); + let mut res = GetContainersSummaryResponse::default(); for server in servers { let cache = server_status_cache() @@ -79,11 +73,11 @@ impl Resolve for GetDockerContainersSummary { } } -impl Resolve for ListAllDockerContainers { +impl Resolve for ListAllContainers { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let servers = resource::list_for_user::( ServerQuery::builder() .names(self.servers.clone()) @@ -135,11 +129,11 @@ impl Resolve for ListAllDockerContainers { } } -impl Resolve for ListDockerContainers { +impl Resolve for ListContainers { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let server = get_check_permissions::( &self.server, user, @@ -157,7 +151,7 @@ impl Resolve for ListDockerContainers { } } -impl Resolve for InspectDockerContainer { +impl Resolve for InspectContainer { async fn resolve( self, ReadArgs { user }: &ReadArgs, @@ -182,7 +176,7 @@ impl Resolve for InspectDockerContainer { } let res = periphery_client(&server) .await? - .request(InspectContainer { + .request(periphery::container::InspectContainer { name: self.container, }) .await?; @@ -339,11 +333,11 @@ impl Resolve for ListComposeProjects { } } -impl Resolve for ListDockerNetworks { +impl Resolve for ListNetworks { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let server = get_check_permissions::( &self.server, user, @@ -361,7 +355,7 @@ impl Resolve for ListDockerNetworks { } } -impl Resolve for InspectDockerNetwork { +impl Resolve for InspectNetwork { async fn resolve( self, ReadArgs { user }: &ReadArgs, @@ -386,17 +380,19 @@ impl Resolve for InspectDockerNetwork { } let res = periphery_client(&server) .await? - .request(InspectNetwork { name: self.network }) + .request(periphery::docker::InspectNetwork { + name: self.network, + }) .await?; Ok(res) } } -impl Resolve for ListDockerImages { +impl Resolve for ListImages { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let server = get_check_permissions::( &self.server, user, @@ -414,7 +410,7 @@ impl Resolve for ListDockerImages { } } -impl Resolve for InspectDockerImage { +impl Resolve for InspectImage { async fn resolve( self, ReadArgs { user }: &ReadArgs, @@ -436,13 +432,13 @@ impl Resolve for InspectDockerImage { } let res = periphery_client(&server) .await? - .request(InspectImage { name: self.image }) + .request(periphery::docker::InspectImage { name: self.image }) .await?; Ok(res) } } -impl Resolve for ListDockerImageHistory { +impl Resolve for ListImageHistory { async fn resolve( self, ReadArgs { user }: &ReadArgs, @@ -467,17 +463,17 @@ impl Resolve for ListDockerImageHistory { } let res = periphery_client(&server) .await? - .request(ImageHistory { name: self.image }) + .request(periphery::docker::ImageHistory { name: self.image }) .await?; Ok(res) } } -impl Resolve for ListDockerVolumes { +impl Resolve for ListVolumes { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { let server = get_check_permissions::( &self.server, user, @@ -495,7 +491,7 @@ impl Resolve for ListDockerVolumes { } } -impl Resolve for InspectDockerVolume { +impl Resolve for InspectVolume { async fn resolve( self, ReadArgs { user }: &ReadArgs, @@ -517,7 +513,7 @@ impl Resolve for InspectDockerVolume { } let res = periphery_client(&server) .await? - .request(InspectVolume { name: self.volume }) + .request(periphery::docker::InspectVolume { name: self.volume }) .await?; Ok(res) } diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index be680bb646..4b84dbd501 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -10,7 +10,7 @@ use komodo_client::{ ResourceTarget, build::Build, builder::{Builder, BuilderConfig}, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, permission::PermissionLevel, repo::Repo, server::Server, @@ -81,7 +81,8 @@ enum ReadRequest { GetCoreInfo(GetCoreInfo), ListSecrets(ListSecrets), ListGitProvidersFromConfig(ListGitProvidersFromConfig), - ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig), + #[serde(alias = "ListDockerRegistriesFromConfig")] + ListImageRegistriesFromConfig(ListImageRegistriesFromConfig), // ==== SWARM ==== GetSwarmsSummary(GetSwarmsSummary), @@ -118,22 +119,33 @@ enum ReadRequest { // ==== TERMINAL ==== ListTerminals(ListTerminals), - // ==== DOCKER ==== - GetDockerContainersSummary(GetDockerContainersSummary), - ListAllDockerContainers(ListAllDockerContainers), - ListDockerContainers(ListDockerContainers), - InspectDockerContainer(InspectDockerContainer), + // ==== CONTAINER ==== + #[serde(alias = "GetDockerContainersSummary")] + GetContainersSummary(GetContainersSummary), + #[serde(alias = "ListAllDockerContainers")] + ListAllContainers(ListAllContainers), + #[serde(alias = "ListDockerContainers")] + ListContainers(ListContainers), + #[serde(alias = "InspectDockerContainer")] + InspectContainer(InspectContainer), GetResourceMatchingContainer(GetResourceMatchingContainer), GetContainerLog(GetContainerLog), SearchContainerLog(SearchContainerLog), ListComposeProjects(ListComposeProjects), - ListDockerNetworks(ListDockerNetworks), - InspectDockerNetwork(InspectDockerNetwork), - ListDockerImages(ListDockerImages), - InspectDockerImage(InspectDockerImage), - ListDockerImageHistory(ListDockerImageHistory), - ListDockerVolumes(ListDockerVolumes), - InspectDockerVolume(InspectDockerVolume), + #[serde(alias = "ListDockerNetworks")] + ListNetworks(ListNetworks), + #[serde(alias = "InspectDockerNetwork")] + InspectNetwork(InspectNetwork), + #[serde(alias = "ListDockerImages")] + ListImages(ListImages), + #[serde(alias = "InspectDockerImage")] + InspectImage(InspectImage), + #[serde(alias = "ListDockerImageHistory")] + ListImageHistory(ListImageHistory), + #[serde(alias = "ListDockerVolumes")] + ListVolumes(ListVolumes), + #[serde(alias = "InspectDockerVolume")] + InspectVolume(InspectVolume), // ==== SERVER STATS ==== GetSystemInformation(GetSystemInformation), @@ -260,8 +272,10 @@ enum ReadRequest { // ==== PROVIDER ==== GetGitProviderAccount(GetGitProviderAccount), ListGitProviderAccounts(ListGitProviderAccounts), - GetDockerRegistryAccount(GetDockerRegistryAccount), - ListDockerRegistryAccounts(ListDockerRegistryAccounts), + #[serde(alias = "GetDockerRegistryAccount")] + GetImageRegistryAccount(GetImageRegistryAccount), + #[serde(alias = "ListDockerRegistryAccounts")] + ListImageRegistryAccounts(ListImageRegistryAccounts), // ==== ONBOARDING KEY ==== ListOnboardingKeys(ListOnboardingKeys), @@ -529,17 +543,17 @@ impl Resolve for ListGitProvidersFromConfig { // -impl Resolve for ListDockerRegistriesFromConfig { +impl Resolve for ListImageRegistriesFromConfig { async fn resolve( self, _: &ReadArgs, - ) -> mogh_error::Result { - let mut registries = core_config().docker_registries.clone(); + ) -> mogh_error::Result { + let mut registries = core_config().image_registries.clone(); if let Some(target) = self.target { match target { ResourceTarget::Server(id) => { - merge_docker_registries_for_server(&mut registries, &id) + merge_image_registries_for_server(&mut registries, &id) .await?; } ResourceTarget::Builder(id) => { @@ -547,7 +561,7 @@ impl Resolve for ListDockerRegistriesFromConfig { BuilderConfig::Url(_) => {} BuilderConfig::Server(config) => { if let Some(server_id) = config.server_ids.first() { - merge_docker_registries_for_server( + merge_image_registries_for_server( &mut registries, server_id, ) @@ -555,9 +569,9 @@ impl Resolve for ListDockerRegistriesFromConfig { } } BuilderConfig::Aws(config) => { - merge_docker_registries( + merge_image_registries( &mut registries, - config.docker_registries, + config.image_registries, ); } } @@ -615,14 +629,14 @@ fn merge_git_providers( } } -async fn merge_docker_registries_for_server( - registries: &mut Vec, +async fn merge_image_registries_for_server( + registries: &mut Vec, server_id: &str, ) -> mogh_error::Result<()> { let server = resource::get::(server_id).await?; let more = periphery_client(&server) .await? - .request(periphery_client::api::ListDockerRegistries {}) + .request(periphery_client::api::ListImageRegistries {}) .await .with_context(|| { format!( @@ -630,13 +644,13 @@ async fn merge_docker_registries_for_server( server.name ) })?; - merge_docker_registries(registries, more); + merge_image_registries(registries, more); Ok(()) } -fn merge_docker_registries( - registries: &mut Vec, - more: Vec, +fn merge_image_registries( + registries: &mut Vec, + more: Vec, ) { for incoming_registry in more { if let Some(registry) = registries diff --git a/bin/core/src/api/read/provider.rs b/bin/core/src/api/read/provider.rs index b915ab6246..94c44b4466 100644 --- a/bin/core/src/api/read/provider.rs +++ b/bin/core/src/api/read/provider.rs @@ -61,11 +61,11 @@ impl Resolve for ListGitProviderAccounts { } } -impl Resolve for GetDockerRegistryAccount { +impl Resolve for GetImageRegistryAccount { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can read docker registry accounts") @@ -83,11 +83,11 @@ impl Resolve for GetDockerRegistryAccount { } } -impl Resolve for ListDockerRegistryAccounts { +impl Resolve for ListImageRegistryAccounts { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can read docker registry accounts") diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 7eb880cdd1..d9533a9089 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -211,9 +211,12 @@ pub enum WriteRequest { CreateGitProviderAccount(CreateGitProviderAccount), UpdateGitProviderAccount(UpdateGitProviderAccount), DeleteGitProviderAccount(DeleteGitProviderAccount), - CreateDockerRegistryAccount(CreateDockerRegistryAccount), - UpdateDockerRegistryAccount(UpdateDockerRegistryAccount), - DeleteDockerRegistryAccount(DeleteDockerRegistryAccount), + #[serde(alias = "CreateDockerRegistryAccount")] + CreateImageRegistryAccount(CreateImageRegistryAccount), + #[serde(alias = "UpdateDockerRegistryAccount")] + UpdateImageRegistryAccount(UpdateImageRegistryAccount), + #[serde(alias = "DeleteDockerRegistryAccount")] + DeleteImageRegistryAccount(DeleteImageRegistryAccount), // ==== ALERT ==== CloseAlert(CloseAlert), diff --git a/bin/core/src/api/write/provider.rs b/bin/core/src/api/write/provider.rs index 0548216b06..398b38c2d4 100644 --- a/bin/core/src/api/write/provider.rs +++ b/bin/core/src/api/write/provider.rs @@ -7,7 +7,7 @@ use komodo_client::{ api::write::*, entities::{ Operation, ResourceTarget, - provider::{DockerRegistryAccount, GitProviderAccount}, + provider::{GitProviderAccount, ImageRegistryAccount}, }, }; use mogh_error::AddStatusCodeError; @@ -248,7 +248,7 @@ impl Resolve for DeleteGitProviderAccount { } } -impl Resolve for CreateDockerRegistryAccount { +impl Resolve for CreateImageRegistryAccount { #[instrument( "CreateDockerRegistryAccount", skip_all, @@ -261,7 +261,7 @@ impl Resolve for CreateDockerRegistryAccount { async fn resolve( self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!( @@ -271,7 +271,7 @@ impl Resolve for CreateDockerRegistryAccount { ); } - let mut account: DockerRegistryAccount = self.account.into(); + let mut account: ImageRegistryAccount = self.account.into(); if account.domain.is_empty() { return Err( @@ -326,7 +326,7 @@ impl Resolve for CreateDockerRegistryAccount { } } -impl Resolve for UpdateDockerRegistryAccount { +impl Resolve for UpdateImageRegistryAccount { #[instrument( "UpdateDockerRegistryAccount", skip_all, @@ -340,7 +340,7 @@ impl Resolve for UpdateDockerRegistryAccount { async fn resolve( mut self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can update docker registry accounts") @@ -423,7 +423,7 @@ impl Resolve for UpdateDockerRegistryAccount { } } -impl Resolve for DeleteDockerRegistryAccount { +impl Resolve for DeleteImageRegistryAccount { #[instrument( "DeleteDockerRegistryAccount", skip_all, @@ -435,7 +435,7 @@ impl Resolve for DeleteDockerRegistryAccount { async fn resolve( self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can delete docker registry accounts") diff --git a/bin/core/src/cloud/aws/ec2.rs b/bin/core/src/cloud/aws/ec2.rs index 854969e536..563f2e98e1 100644 --- a/bin/core/src/cloud/aws/ec2.rs +++ b/bin/core/src/cloud/aws/ec2.rs @@ -88,7 +88,7 @@ pub async fn launch_ec2_instance( port: _, use_https: _, git_providers: _, - docker_registries: _, + image_registries: _, secrets: _, } = config; let instance_type = handle_unknown_instance_type( diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 8459a70189..c08d79894d 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -447,7 +447,7 @@ pub fn core_config() -> &'static CoreConfig { // These can't be overridden on env secrets: config.secrets, git_providers: config.git_providers, - docker_registries: config.docker_registries, + image_registries: config.image_registries, } }) } diff --git a/bin/core/src/helpers/mod.rs b/bin/core/src/helpers/mod.rs index 1fe282fb2a..d327dbac98 100644 --- a/bin/core/src/helpers/mod.rs +++ b/bin/core/src/helpers/mod.rs @@ -169,7 +169,7 @@ pub async fn registry_token( } Ok( core_config() - .docker_registries + .image_registries .iter() .find(|provider| provider.domain == provider_domain) .and_then(|provider| { diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index bf4382aabd..d9da3a164e 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -1,7 +1,7 @@ use command::{CommandOptions, run_komodo_standard_command}; use encoding::{EncodedJsonMessage, EncodedResponse}; use komodo_client::entities::{ - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, stats::SystemProcess, update::Log, }; @@ -54,7 +54,7 @@ pub enum PeripheryRequest { // Config (Read) ListGitProviders(ListGitProviders), - ListDockerRegistries(ListDockerRegistries), + ListImageRegistries(ListImageRegistries), ListSecrets(ListSecrets), // Repo (Write) @@ -226,12 +226,12 @@ impl Resolve for ListGitProviders { } } -impl Resolve for ListDockerRegistries { +impl Resolve for ListImageRegistries { async fn resolve( self, _: &Args, - ) -> anyhow::Result> { - Ok(periphery_config().docker_registries.0.clone()) + ) -> anyhow::Result> { + Ok(periphery_config().image_registries.0.clone()) } } diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs index d67623fa3f..6e043caad1 100644 --- a/bin/periphery/src/config.rs +++ b/bin/periphery/src/config.rs @@ -180,7 +180,7 @@ pub fn periphery_config() -> &'static PeripheryConfig { .or(config.ssl_cert_file), secrets: config.secrets, git_providers: config.git_providers, - docker_registries: config.docker_registries, + image_registries: config.image_registries, } }) } diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index c2ecd3bb47..d95cf35a2e 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -271,7 +271,7 @@ pub fn registry_token( account_username: &str, ) -> anyhow::Result<&'static str> { periphery_config() - .docker_registries + .image_registries .iter() .find(|registry| registry.domain == domain) .and_then(|registry| { diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 3e7d7a1f60..675aa0577a 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -23,30 +23,32 @@ use super::KomodoReadRequest; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/GetDockerContainersSummary", + path = "/GetContainersSummary", description = "Gets a summary of data relating to all containers.", - request_body(content = GetDockerContainersSummary), + request_body(content = GetContainersSummary), responses( - (status = 200, description = "The docker containers summary", body = GetDockerContainersSummaryResponse), + (status = 200, description = "The containers summary", body = GetContainersSummaryResponse), ), )] -pub fn get_docker_containers_summary() {} +pub fn get_containers_summary() {} /// Gets a summary of data relating to all containers. -/// Response: [GetDockerContainersSummaryResponse]. +/// Response: [GetContainersSummaryResponse]. +/// +/// Pre v2.3.0, called `GetDockerContainersSummary` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(GetDockerContainersSummaryResponse)] +#[response(GetContainersSummaryResponse)] #[error(mogh_error::Error)] -pub struct GetDockerContainersSummary {} +pub struct GetContainersSummary {} -/// Response for [GetDockerContainersSummary] +/// Response for [GetContainersSummary] #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct GetDockerContainersSummaryResponse { +pub struct GetContainersSummaryResponse { /// The total number of Containers pub total: u32, /// The number of Containers with Running state @@ -64,24 +66,26 @@ pub struct GetDockerContainersSummaryResponse { #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListAllDockerContainers", - description = "List all docker containers on the target servers.", - request_body(content = ListAllDockerContainers), + path = "/ListAllContainers", + description = "List all containers on the target servers.", + request_body(content = ListAllContainers), responses( - (status = 200, description = "The list of containers", body = ListAllDockerContainersResponse), + (status = 200, description = "The list of containers", body = ListAllContainersResponse), ), )] -pub fn list_all_docker_containers() {} +pub fn list_all_containers() {} -/// List all docker containers on the target servers. -/// Response: [ListDockerContainersResponse]. +/// List all containers on the target servers. +/// Response: [ListAllContainersResponse]. +/// +/// Pre v2.3.0, called `ListAllDockerContainers` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListAllDockerContainersResponse)] +#[response(ListAllContainersResponse)] #[error(mogh_error::Error)] -pub struct ListAllDockerContainers { +pub struct ListAllContainers { /// Filter by server id or name. #[serde(default)] pub servers: Vec, @@ -118,61 +122,65 @@ fn default_limit() -> u64 { } #[typeshare] -pub type ListAllDockerContainersResponse = Vec; +pub type ListAllContainersResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerContainers", - description = "List all docker containers on the target server.", - request_body(content = ListDockerContainers), + path = "/ListContainers", + description = "List all containers on the target server.", + request_body(content = ListContainers), responses( - (status = 200, description = "The list of containers", body = ListDockerContainersResponse), + (status = 200, description = "The list of containers", body = ListContainersResponse), ), )] -pub fn list_docker_containers() {} +pub fn list_containers() {} -/// List all docker containers on the target server. -/// Response: [ListDockerContainersResponse]. +/// List all containers on the target server. +/// Response: [ListContainersResponse]. +/// +/// Pre v2.3.0, called `ListDockerContainers` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerContainersResponse)] +#[response(ListContainersResponse)] #[error(mogh_error::Error)] -pub struct ListDockerContainers { +pub struct ListContainers { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerContainersResponse = Vec; +pub type ListContainersResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerContainer", - description = "Inspect a docker container on the server.", - request_body(content = InspectDockerContainer), + path = "/InspectContainer", + description = "Inspect a container on the server.", + request_body(content = InspectContainer), responses( - (status = 200, description = "The container", body = InspectDockerContainerResponse), + (status = 200, description = "The container", body = InspectContainerResponse), ), )] -pub fn inspect_docker_container() {} +pub fn inspect_container() {} -/// Inspect a docker container on the server. Response: [Container]. +/// Inspect a container on the server. Response: [Container]. +/// +/// Pre v2.3.0, called `InspectDockerContainer` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerContainerResponse)] +#[response(InspectContainerResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerContainer { +pub struct InspectContainer { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -181,7 +189,7 @@ pub struct InspectDockerContainer { } #[typeshare] -pub type InspectDockerContainerResponse = Container; +pub type InspectContainerResponse = Container; // @@ -322,7 +330,7 @@ pub type SearchContainerLogResponse = Log; #[utoipa::path( post, path = "/ListComposeProjects", - description = "List all docker compose projects on the target server.", + description = "List all compose projects on the target server.", request_body(content = ListComposeProjects), responses( (status = 200, description = "The list of compose projects", body = ListComposeProjectsResponse), @@ -330,7 +338,7 @@ pub type SearchContainerLogResponse = Log; )] pub fn list_compose_projects() {} -/// List all docker compose projects on the target server. +/// List all compose projects on the target server. /// Response: [ListComposeProjectsResponse]. #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] @@ -352,53 +360,57 @@ pub type ListComposeProjectsResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerNetworks", - description = "List the docker networks on the server.", - request_body(content = ListDockerNetworks), + path = "/ListNetworks", + description = "List the container networks on the server.", + request_body(content = ListNetworks), responses( - (status = 200, description = "The list of networks", body = ListDockerNetworksResponse), + (status = 200, description = "The list of networks", body = ListNetworksResponse), ), )] -pub fn list_docker_networks() {} +pub fn list_networks() {} -/// List the docker networks on the server. Response: [ListDockerNetworksResponse]. +/// List the container networks on the server. Response: [ListNetworksResponse]. +/// +/// Pre v2.3.0, called `ListDockerNetworks` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerNetworksResponse)] +#[response(ListNetworksResponse)] #[error(mogh_error::Error)] -pub struct ListDockerNetworks { +pub struct ListNetworks { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerNetworksResponse = Vec; +pub type ListNetworksResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerNetwork", - description = "Inspect a docker network on the server.", - request_body(content = InspectDockerNetwork), + path = "/InspectNetwork", + description = "Inspect a container network on the server.", + request_body(content = InspectNetwork), responses( - (status = 200, description = "The network", body = InspectDockerNetworkResponse), + (status = 200, description = "The network", body = InspectNetworkResponse), ), )] -pub fn inspect_docker_network() {} +pub fn inspect_network() {} -/// Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. +/// Inspect a container network on the server. Response: [InspectNetworkResponse]. +/// +/// Pre v2.3.0, called `InspectDockerNetwork` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerNetworkResponse)] +#[response(InspectNetworkResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerNetwork { +pub struct InspectNetwork { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -407,61 +419,65 @@ pub struct InspectDockerNetwork { } #[typeshare] -pub type InspectDockerNetworkResponse = Network; +pub type InspectNetworkResponse = Network; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerImages", - description = "List the docker images locally cached on the target server.", - request_body(content = ListDockerImages), + path = "/ListImages", + description = "List the container images locally cached on the target server.", + request_body(content = ListImages), responses( - (status = 200, description = "The list of images", body = ListDockerImagesResponse), + (status = 200, description = "The list of images", body = ListImagesResponse), ), )] -pub fn list_docker_images() {} +pub fn list_images() {} -/// List the docker images locally cached on the target server. -/// Response: [ListDockerImagesResponse]. +/// List the container images locally cached on the target server. +/// Response: [ListImagesResponse]. +/// +/// Pre v2.3.0, called `ListDockerImages` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerImagesResponse)] +#[response(ListImagesResponse)] #[error(mogh_error::Error)] -pub struct ListDockerImages { +pub struct ListImages { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerImagesResponse = Vec; +pub type ListImagesResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerImage", - description = "Inspect a docker image on the server.", - request_body(content = InspectDockerImage), + path = "/InspectImage", + description = "Inspect a container image on the server.", + request_body(content = InspectImage), responses( - (status = 200, description = "The image", body = InspectDockerImageResponse), + (status = 200, description = "The image", body = InspectImageResponse), ), )] -pub fn inspect_docker_image() {} +pub fn inspect_image() {} -/// Inspect a docker image on the server. Response: [Image]. +/// Inspect a container image on the server. Response: [Image]. +/// +/// Pre v2.3.0, called `InspectDockerImage` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerImageResponse)] +#[response(InspectImageResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerImage { +pub struct InspectImage { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -470,30 +486,32 @@ pub struct InspectDockerImage { } #[typeshare] -pub type InspectDockerImageResponse = Image; +pub type InspectImageResponse = Image; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerImageHistory", + path = "/ListImageHistory", description = "Get image history from the server.", - request_body(content = ListDockerImageHistory), + request_body(content = ListImageHistory), responses( - (status = 200, description = "The image history", body = ListDockerImageHistoryResponse), + (status = 200, description = "The image history", body = ListImageHistoryResponse), ), )] -pub fn list_docker_image_history() {} +pub fn list_image_history() {} -/// Get image history from the server. Response: [ListDockerImageHistoryResponse]. +/// Get image history from the server. Response: [ListImageHistoryResponse]. +/// +/// Pre v2.3.0, called `ListDockerImageHistory` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerImageHistoryResponse)] +#[response(ListImageHistoryResponse)] #[error(mogh_error::Error)] -pub struct ListDockerImageHistory { +pub struct ListImageHistory { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -502,62 +520,65 @@ pub struct ListDockerImageHistory { } #[typeshare] -pub type ListDockerImageHistoryResponse = - Vec; +pub type ListImageHistoryResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerVolumes", - description = "List all docker volumes on the target server.", - request_body(content = ListDockerVolumes), + path = "/ListVolumes", + description = "List all container volumes on the target server.", + request_body(content = ListVolumes), responses( - (status = 200, description = "The list of volumes", body = ListDockerVolumesResponse), + (status = 200, description = "The list of volumes", body = ListVolumesResponse), ), )] -pub fn list_docker_volumes() {} +pub fn list_volumes() {} -/// List all docker volumes on the target server. -/// Response: [ListDockerVolumesResponse]. +/// List all container volumes on the target server. +/// Response: [ListVolumesResponse]. +/// +/// Pre v2.3.0, called `ListDockerVolumes` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerVolumesResponse)] +#[response(ListVolumesResponse)] #[error(mogh_error::Error)] -pub struct ListDockerVolumes { +pub struct ListVolumes { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerVolumesResponse = Vec; +pub type ListVolumesResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerVolume", - description = "Inspect a docker volume on the server.", - request_body(content = InspectDockerVolume), + path = "/InspectVolume", + description = "Inspect a container volume on the server.", + request_body(content = InspectVolume), responses( - (status = 200, description = "The volume", body = InspectDockerVolumeResponse), + (status = 200, description = "The volume", body = InspectVolumeResponse), ), )] -pub fn inspect_docker_volume() {} +pub fn inspect_volume() {} -/// Inspect a docker volume on the server. Response: [Volume]. +/// Inspect a container volume on the server. Response: [Volume]. +/// +/// Pre v2.3.0, called `InspectDockerVolume` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerVolumeResponse)] +#[response(InspectVolumeResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerVolume { +pub struct InspectVolume { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -566,4 +587,4 @@ pub struct InspectDockerVolume { } #[typeshare] -pub type InspectDockerVolumeResponse = Volume; +pub type InspectVolumeResponse = Volume; diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 888e7ce426..64ed527a4e 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -54,7 +54,7 @@ pub use variable::*; use crate::entities::{ ResourceTarget, Timelength, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, }; #[cfg(feature = "utoipa")] @@ -191,38 +191,40 @@ pub type ListGitProvidersFromConfigResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerRegistriesFromConfig", - description = "List the docker registry providers available in Core / Periphery config files.", - request_body(content = ListDockerRegistriesFromConfig), + path = "/ListImageRegistriesFromConfig", + description = "List the image registry providers available in Core / Periphery config files.", + request_body(content = ListImageRegistriesFromConfig), responses( - (status = 200, description = "The available docker registries", body = ListDockerRegistriesFromConfigResponse), + (status = 200, description = "The available image registries", body = ListImageRegistriesFromConfigResponse), (status = 400, description = "Target must be `Server` or `Builder`", body = mogh_error::Serror), (status = 500, description = "Failed", body = mogh_error::Serror), ), )] -pub fn list_docker_registries_from_config() {} +pub fn list_image_registries_from_config() {} -/// List the docker registry providers available in Core / Periphery config files. -/// Response: [ListDockerRegistriesFromConfigResponse]. +/// List the image registry providers available in Core / Periphery config files. +/// Response: [ListImageRegistriesFromConfigResponse]. /// /// Includes: /// - registries in core config /// - registries configured on builds, deployments /// - registries on the optional Server or Builder +/// +/// Pre v2.3.0, called `ListDockerRegistriesFromConfig` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerRegistriesFromConfigResponse)] +#[response(ListImageRegistriesFromConfigResponse)] #[error(mogh_error::Error)] -pub struct ListDockerRegistriesFromConfig { +pub struct ListImageRegistriesFromConfig { /// Accepts an optional Server or Builder target to expand the core list with /// providers available on that specific resource. pub target: Option, } #[typeshare] -pub type ListDockerRegistriesFromConfigResponse = Vec; +pub type ListImageRegistriesFromConfigResponse = Vec; // diff --git a/client/core/rs/src/api/read/openapi.rs b/client/core/rs/src/api/read/openapi.rs index b83ece3da5..5f469aacc8 100644 --- a/client/core/rs/src/api/read/openapi.rs +++ b/client/core/rs/src/api/read/openapi.rs @@ -15,9 +15,9 @@ use crate::api::read; read::get_git_provider_account, read::list_git_provider_accounts, read::list_git_providers_from_config, - read::get_docker_registry_account, - read::list_docker_registry_accounts, - read::list_docker_registries_from_config, + read::get_image_registry_account, + read::list_image_registry_accounts, + read::list_image_registries_from_config, // swarm read::list_swarms, read::list_full_swarms, @@ -126,22 +126,22 @@ use crate::api::read; // variable read::list_variables, read::get_variable, - // docker - read::get_docker_containers_summary, - read::list_all_docker_containers, - read::list_docker_containers, - read::inspect_docker_container, + // container + read::get_containers_summary, + read::list_all_containers, + read::list_containers, + read::inspect_container, read::get_resource_matching_container, read::get_container_log, read::search_container_log, read::list_compose_projects, - read::list_docker_networks, - read::inspect_docker_network, - read::list_docker_images, - read::inspect_docker_image, - read::list_docker_image_history, - read::list_docker_volumes, - read::inspect_docker_volume, + read::list_networks, + read::inspect_network, + read::list_images, + read::inspect_image, + read::list_image_history, + read::list_volumes, + read::inspect_volume, // toml read::export_all_resources_to_toml, read::export_resources_to_toml, diff --git a/client/core/rs/src/api/read/provider.rs b/client/core/rs/src/api/read/provider.rs index e84df7db68..d45d876a84 100644 --- a/client/core/rs/src/api/read/provider.rs +++ b/client/core/rs/src/api/read/provider.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::provider::{ - DockerRegistryAccount, GitProviderAccount, + GitProviderAccount, ImageRegistryAccount, }; use super::KomodoReadRequest; @@ -74,53 +74,57 @@ pub type ListGitProviderAccountsResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/GetDockerRegistryAccount", - description = "Get a specific docker registry account.", - request_body(content = GetDockerRegistryAccount), + path = "/GetImageRegistryAccount", + description = "Get a specific image registry account.", + request_body(content = GetImageRegistryAccount), responses( - (status = 200, description = "The docker registry account", body = GetDockerRegistryAccountResponse), + (status = 200, description = "The image registry account", body = GetImageRegistryAccountResponse), ), )] -pub fn get_docker_registry_account() {} +pub fn get_image_registry_account() {} -/// Get a specific docker registry account. -/// Response: [GetDockerRegistryAccountResponse]. +/// Get a specific image registry account. +/// Response: [GetImageRegistryAccountResponse]. +/// +/// Pre v2.3.0, called `GetDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(GetDockerRegistryAccountResponse)] +#[response(GetImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct GetDockerRegistryAccount { +pub struct GetImageRegistryAccount { pub id: String, } #[typeshare] -pub type GetDockerRegistryAccountResponse = DockerRegistryAccount; +pub type GetImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerRegistryAccounts", - description = "List docker registry accounts matching optional query.", - request_body(content = ListDockerRegistryAccounts), + path = "/ListImageRegistryAccounts", + description = "List image registry accounts matching optional query.", + request_body(content = ListImageRegistryAccounts), responses( - (status = 200, description = "The list of docker registry accounts", body = ListDockerRegistryAccountsResponse), + (status = 200, description = "The list of image registry accounts", body = ListImageRegistryAccountsResponse), ), )] -pub fn list_docker_registry_accounts() {} +pub fn list_image_registry_accounts() {} -/// List docker registry accounts matching optional query. -/// Response: [ListDockerRegistryAccountsResponse]. +/// List image registry accounts matching optional query. +/// Response: [ListImageRegistryAccountsResponse]. +/// +/// Pre v2.3.0, called `ListDockerRegistryAccounts` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerRegistryAccountsResponse)] +#[response(ListImageRegistryAccountsResponse)] #[error(mogh_error::Error)] -pub struct ListDockerRegistryAccounts { +pub struct ListImageRegistryAccounts { /// Optionally filter by accounts with a specific domain. pub domain: Option, /// Optionally filter by accounts with a specific username. @@ -128,5 +132,5 @@ pub struct ListDockerRegistryAccounts { } #[typeshare] -pub type ListDockerRegistryAccountsResponse = - Vec; +pub type ListImageRegistryAccountsResponse = + Vec; diff --git a/client/core/rs/src/api/write/openapi.rs b/client/core/rs/src/api/write/openapi.rs index 542a80aecb..c6861669d7 100644 --- a/client/core/rs/src/api/write/openapi.rs +++ b/client/core/rs/src/api/write/openapi.rs @@ -136,9 +136,9 @@ use crate::api::write; write::create_git_provider_account, write::update_git_provider_account, write::delete_git_provider_account, - write::create_docker_registry_account, - write::update_docker_registry_account, - write::delete_docker_registry_account, + write::create_image_registry_account, + write::update_image_registry_account, + write::delete_image_registry_account, ), )] pub struct KomodoWriteApi; diff --git a/client/core/rs/src/api/write/provider.rs b/client/core/rs/src/api/write/provider.rs index 7b661929ad..96a9fab1f2 100644 --- a/client/core/rs/src/api/write/provider.rs +++ b/client/core/rs/src/api/write/provider.rs @@ -104,88 +104,94 @@ pub type DeleteGitProviderAccountResponse = GitProviderAccount; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/CreateDockerRegistryAccount", - description = "**Admin only.** Create a docker registry account.", - request_body(content = CreateDockerRegistryAccount), + path = "/CreateImageRegistryAccount", + description = "**Admin only.** Create an image registry account.", + request_body(content = CreateImageRegistryAccount), responses( - (status = 200, description = "The created account", body = CreateDockerRegistryAccountResponse), + (status = 200, description = "The created account", body = CreateImageRegistryAccountResponse), ), )] -pub fn create_docker_registry_account() {} +pub fn create_image_registry_account() {} -/// **Admin only.** Create a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Create an image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `CreateDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(CreateDockerRegistryAccountResponse)] +#[response(CreateImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct CreateDockerRegistryAccount { - pub account: _PartialDockerRegistryAccount, +pub struct CreateImageRegistryAccount { + pub account: _PartialImageRegistryAccount, } #[typeshare] -pub type CreateDockerRegistryAccountResponse = DockerRegistryAccount; +pub type CreateImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/UpdateDockerRegistryAccount", - description = "**Admin only.** Update a docker registry account.", - request_body(content = UpdateDockerRegistryAccount), + path = "/UpdateImageRegistryAccount", + description = "**Admin only.** Update an image registry account.", + request_body(content = UpdateImageRegistryAccount), responses( - (status = 200, description = "The updated account", body = UpdateDockerRegistryAccountResponse), + (status = 200, description = "The updated account", body = UpdateImageRegistryAccountResponse), ), )] -pub fn update_docker_registry_account() {} +pub fn update_image_registry_account() {} -/// **Admin only.** Update a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Update a image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `UpdateDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(UpdateDockerRegistryAccountResponse)] +#[response(UpdateImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct UpdateDockerRegistryAccount { - /// The id of the docker registry to update +pub struct UpdateImageRegistryAccount { + /// The id of the image registry to update pub id: String, - /// The partial docker registry account. - pub account: _PartialDockerRegistryAccount, + /// The partial image registry account. + pub account: _PartialImageRegistryAccount, } #[typeshare] -pub type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; +pub type UpdateImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/DeleteDockerRegistryAccount", - description = "**Admin only.** Delete a docker registry account.", - request_body(content = DeleteDockerRegistryAccount), + path = "/DeleteImageRegistryAccount", + description = "**Admin only.** Delete an image registry account.", + request_body(content = DeleteImageRegistryAccount), responses( - (status = 200, description = "The deleted account", body = DeleteDockerRegistryAccountResponse), + (status = 200, description = "The deleted account", body = DeleteImageRegistryAccountResponse), ), )] -pub fn delete_docker_registry_account() {} +pub fn delete_image_registry_account() {} -/// **Admin only.** Delete a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Delete an image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `DeleteDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(DeleteDockerRegistryAccountResponse)] +#[response(DeleteImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct DeleteDockerRegistryAccount { - /// The id of the docker registry account to delete +pub struct DeleteImageRegistryAccount { + /// The id of the image registry account to delete pub id: String, } #[typeshare] -pub type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; +pub type DeleteImageRegistryAccountResponse = ImageRegistryAccount; diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index 78d7296bc2..f048b98e00 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -10,7 +10,7 @@ use crate::deserializers::{ use super::{ MergePartial, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, }; @@ -353,9 +353,9 @@ impl MergePartial for BuilderConfig { git_providers: partial .git_providers .unwrap_or(config.git_providers), - docker_registries: partial - .docker_registries - .unwrap_or(config.docker_registries), + image_registries: partial + .image_registries + .unwrap_or(config.image_registries), secrets: partial.secrets.unwrap_or(config.secrets), }; BuilderConfig::Aws(config) @@ -596,10 +596,12 @@ pub struct AwsBuilderConfig { #[serde(default)] #[builder(default)] pub git_providers: Vec, - /// Which docker registries are available on the AMI. + /// Which image registries are available on the AMI. + /// + /// Pre v2.3.0, called `docker_registries` #[serde(default)] #[builder(default)] - pub docker_registries: Vec, + pub image_registries: Vec, /// Which secrets are available on the AMI. #[serde(default, deserialize_with = "string_list_deserializer")] #[partial_attr(serde( @@ -628,7 +630,7 @@ impl Default for AwsBuilderConfig { periphery_public_key: Default::default(), insecure_tls: default_insecure_tls(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), secrets: Default::default(), } } diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 1a1692a35c..8a9678514a 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -23,7 +23,7 @@ use crate::{ }, }; -use super::{DockerRegistry, GitProvider, empty_or_redacted}; +use super::{GitProvider, ImageRegistry, empty_or_redacted}; /// # Komodo Core Environment Variables /// @@ -726,14 +726,17 @@ pub struct CoreConfig { // ====================== // = Registry Providers = // ====================== - /// Configure docker credentials used to push / pull images. - /// Supports any docker image repository. + /// Configure image registry credentials used to push / pull images. + /// + /// Pre v2.3.0, called `docker_registries` #[serde( default, + alias = "image_registry", alias = "docker_registry", + alias = "docker_registries", skip_serializing_if = "Vec::is_empty" )] - pub docker_registries: Vec, + pub image_registries: Vec, // =========== // = Secrets = @@ -950,7 +953,7 @@ impl Default for CoreConfig { monitoring_interval: default_monitoring_interval(), aws: Default::default(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), secrets: Default::default(), ssl_enabled: Default::default(), ssl_key_file: default_ssl_key_file(), @@ -1080,8 +1083,8 @@ impl CoreConfig { provider }) .collect(), - docker_registries: config - .docker_registries + image_registries: config + .image_registries .into_iter() .map(|mut provider| { provider.accounts.iter_mut().for_each(|account| { diff --git a/client/core/rs/src/entities/config/mod.rs b/client/core/rs/src/entities/config/mod.rs index 6ec7babed8..e74b3c482c 100644 --- a/client/core/rs/src/entities/config/mod.rs +++ b/client/core/rs/src/entities/config/mod.rs @@ -154,9 +154,9 @@ fn default_git_https() -> bool { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct DockerRegistry { - /// The docker provider domain. Default: `docker.io`. - #[serde(default = "default_docker_provider")] +pub struct ImageRegistry { + /// The image provider domain. Default: `docker.io`. + #[serde(default = "default_image_provider")] pub domain: String, /// The accounts on the registry. Required. #[serde(alias = "account")] @@ -167,7 +167,7 @@ pub struct DockerRegistry { pub organizations: Vec, } -fn default_docker_provider() -> String { +fn default_image_provider() -> String { String::from("docker.io") } diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs index 3ecca5ea23..76d022ea3b 100644 --- a/client/core/rs/src/entities/config/periphery.rs +++ b/client/core/rs/src/entities/config/periphery.rs @@ -29,7 +29,7 @@ use crate::{ }; use super::{ - DockerRegistry, GitProvider, ProviderAccount, empty_or_redacted, + GitProvider, ImageRegistry, ProviderAccount, empty_or_redacted, }; /// # Periphery Command Line Arguments. @@ -424,10 +424,16 @@ pub struct PeripheryConfig { #[serde(default, alias = "git_provider")] pub git_providers: ForgivingVec, - /// Configure docker credentials used to push / pull images. - /// Supports any docker image repository. - #[serde(default, alias = "docker_registry")] - pub docker_registries: ForgivingVec, + /// Configure image registry credentials used to push / pull images. + /// + /// Pre v2.3.0, called `docker_registries` + #[serde( + default, + alias = "image_registry", + alias = "docker_registry", + alias = "docker_registries" + )] + pub image_registries: ForgivingVec, } fn default_periphery_port() -> u16 { @@ -489,7 +495,7 @@ impl Default for PeripheryConfig { exclude_disk_mounts: Default::default(), secrets: Default::default(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), ssl_enabled: default_ssl_enabled(), ssl_key_file: None, ssl_cert_file: None, @@ -560,10 +566,10 @@ impl PeripheryConfig { .collect(), }) .collect(), - docker_registries: self - .docker_registries + image_registries: self + .image_registries .iter() - .map(|provider| DockerRegistry { + .map(|provider| ImageRegistry { domain: provider.domain.clone(), organizations: provider.organizations.clone(), accounts: provider diff --git a/client/core/rs/src/entities/provider.rs b/client/core/rs/src/entities/provider.rs index f685b396d2..a4d20e72ca 100644 --- a/client/core/rs/src/entities/provider.rs +++ b/client/core/rs/src/entities/provider.rs @@ -72,8 +72,8 @@ impl utoipa::PartialSchema for PartialGitProviderAccount { #[cfg(feature = "utoipa")] impl utoipa::ToSchema for PartialGitProviderAccount {} -#[typeshare(serialized_as = "Partial")] -pub type _PartialDockerRegistryAccount = PartialDockerRegistryAccount; +#[typeshare(serialized_as = "Partial")] +pub type _PartialImageRegistryAccount = PartialImageRegistryAccount; /// Configuration to access private image repositories on various registries. #[typeshare] @@ -87,10 +87,10 @@ pub type _PartialDockerRegistryAccount = PartialDockerRegistryAccount; derive(mongo_indexed::derive::MongoIndexed) )] #[cfg_attr(feature = "mongo", unique_doc_index({ "domain": 1, "username": 1 }))] -pub struct DockerRegistryAccount { +pub struct ImageRegistryAccount { /// The Mongo ID of the docker registry account. /// This field is de/serialized from/to JSON as - /// `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + /// `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` #[serde( default, rename = "_id", @@ -122,7 +122,7 @@ fn default_registry_domain() -> String { } #[cfg(feature = "utoipa")] -impl utoipa::PartialSchema for PartialDockerRegistryAccount { +impl utoipa::PartialSchema for PartialImageRegistryAccount { fn schema() -> utoipa::openapi::RefOr { utoipa::schema!(#[inline] std::collections::HashMap).into() @@ -130,4 +130,4 @@ impl utoipa::PartialSchema for PartialDockerRegistryAccount { } #[cfg(feature = "utoipa")] -impl utoipa::ToSchema for PartialDockerRegistryAccount {} +impl utoipa::ToSchema for PartialImageRegistryAccount {} diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 98641c8f60..808ac85d8e 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -5,7 +5,7 @@ export type ReadResponses = { GetCoreInfo: Types.GetCoreInfoResponse; ListSecrets: Types.ListSecretsResponse; ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse; - ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse; + ListImageRegistriesFromConfig: Types.ListImageRegistriesFromConfigResponse; // ==== SWARM ==== GetSwarmsSummary: Types.GetSwarmsSummaryResponse; @@ -43,21 +43,21 @@ export type ReadResponses = { ListTerminals: Types.ListTerminalsResponse; // ==== DOCKER ==== - GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse; - ListAllDockerContainers: Types.ListAllDockerContainersResponse; - ListDockerContainers: Types.ListDockerContainersResponse; - InspectDockerContainer: Types.InspectDockerContainerResponse; + GetContainersSummary: Types.GetContainersSummaryResponse; + ListAllContainers: Types.ListAllContainersResponse; + ListContainers: Types.ListContainersResponse; + InspectContainer: Types.InspectContainerResponse; GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse; GetContainerLog: Types.GetContainerLogResponse; SearchContainerLog: Types.SearchContainerLogResponse; ListComposeProjects: Types.ListComposeProjectsResponse; - ListDockerNetworks: Types.ListDockerNetworksResponse; - InspectDockerNetwork: Types.InspectDockerNetworkResponse; - ListDockerImages: Types.ListDockerImagesResponse; - InspectDockerImage: Types.InspectDockerImageResponse; - ListDockerImageHistory: Types.ListDockerImageHistoryResponse; - ListDockerVolumes: Types.ListDockerVolumesResponse; - InspectDockerVolume: Types.InspectDockerVolumeResponse; + ListNetworks: Types.ListNetworksResponse; + InspectNetwork: Types.InspectNetworkResponse; + ListImages: Types.ListImagesResponse; + InspectImage: Types.InspectImageResponse; + ListImageHistory: Types.ListImageHistoryResponse; + ListVolumes: Types.ListVolumesResponse; + InspectVolume: Types.InspectVolumeResponse; // ==== SERVER STATS ==== GetSystemInformation: Types.GetSystemInformationResponse; @@ -184,8 +184,8 @@ export type ReadResponses = { // ==== PROVIDER ==== GetGitProviderAccount: Types.GetGitProviderAccountResponse; ListGitProviderAccounts: Types.ListGitProviderAccountsResponse; - GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse; - ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse; + GetImageRegistryAccount: Types.GetImageRegistryAccountResponse; + ListImageRegistryAccounts: Types.ListImageRegistryAccountsResponse; // ==== ONBOARDING KEY ==== ListOnboardingKeys: Types.ListOnboardingKeysResponse; @@ -343,9 +343,9 @@ export type WriteResponses = { CreateGitProviderAccount: Types.CreateGitProviderAccountResponse; UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse; DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse; - CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse; - UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse; - DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse; + CreateImageRegistryAccount: Types.CreateImageRegistryAccountResponse; + UpdateImageRegistryAccount: Types.UpdateImageRegistryAccountResponse; + DeleteImageRegistryAccount: Types.DeleteImageRegistryAccountResponse; // ==== ALERT ==== CloseAlert: Types.NoData; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 1dd355527b..21225a68b3 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1035,22 +1035,26 @@ export interface CreateApiKeyResponse { export type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; -/** Configuration to access private image repositories on various registries. */ -export interface DockerRegistryAccount { +/** + * Configuration to access private git repos from various git providers. + * Note. Cannot create two accounts with the same domain and username. + */ +export interface GitProviderAccount { /** - * The Mongo ID of the docker registry account. + * The Mongo ID of the git provider account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` */ _id?: MongoId; /** * The domain of the provider. * - * For docker registry, this can include 'http://...', - * however this is not recommended and won't work unless "insecure registries" are enabled - * on your hosts. See . + * For git, this cannot include the protocol eg 'http://', + * which is controlled with 'https' field. */ domain: string; + /** Whether git provider is accessed over http or https. */ + https: boolean; /** The account username */ username?: string; /** @@ -1060,28 +1064,24 @@ export interface DockerRegistryAccount { token?: string; } -export type CreateDockerRegistryAccountResponse = DockerRegistryAccount; +export type CreateGitProviderAccountResponse = GitProviderAccount; -/** - * Configuration to access private git repos from various git providers. - * Note. Cannot create two accounts with the same domain and username. - */ -export interface GitProviderAccount { +/** Configuration to access private image repositories on various registries. */ +export interface ImageRegistryAccount { /** - * The Mongo ID of the git provider account. + * The Mongo ID of the docker registry account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + * `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` */ _id?: MongoId; /** * The domain of the provider. * - * For git, this cannot include the protocol eg 'http://', - * which is controlled with 'https' field. + * For docker registry, this can include 'http://...', + * however this is not recommended and won't work unless "insecure registries" are enabled + * on your hosts. See . */ domain: string; - /** Whether git provider is accessed over http or https. */ - https: boolean; /** The account username */ username?: string; /** @@ -1091,7 +1091,7 @@ export interface GitProviderAccount { token?: string; } -export type CreateGitProviderAccountResponse = GitProviderAccount; +export type CreateImageRegistryAccountResponse = ImageRegistryAccount; export type UserConfig = /** User that logs in with username / password */ @@ -1214,10 +1214,10 @@ export type CreateVariableResponse = Variable; export type DeleteApiKeyForServiceUserResponse = NoData; -export type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; - export type DeleteGitProviderAccountResponse = GitProviderAccount; +export type DeleteImageRegistryAccountResponse = ImageRegistryAccount; + /** * An public key used to authenticate new Periphery -> Core connections * to join Komodo as a newly created Server. @@ -1879,10 +1879,10 @@ export interface ContainerStats { export type GetDeploymentStatsResponse = ContainerStats; -export type GetDockerRegistryAccountResponse = DockerRegistryAccount; - export type GetGitProviderAccountResponse = GitProviderAccount; +export type GetImageRegistryAccountResponse = ImageRegistryAccount; + export enum Timelength { /** `1-sec` */ OneSecond = "1-sec", @@ -3536,6 +3536,8 @@ export interface Container { NetworkSettings?: NetworkSettings; } +export type InspectContainerResponse = Container; + export type InspectDeploymentContainerResponse = Container; /** The service mode. */ @@ -4096,8 +4098,6 @@ export interface SwarmService { export type InspectDeploymentSwarmServiceResponse = SwarmService; -export type InspectDockerContainerResponse = Container; - /** Describes the platform which the image in the manifest runs on, as defined in the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). */ export interface OciPlatform { /** The CPU architecture, for example `amd64` or `ppc64`. */ @@ -4254,7 +4254,7 @@ export interface Image { Metadata?: ImageInspectMetadata; } -export type InspectDockerImageResponse = Image; +export type InspectImageResponse = Image; export interface IpamConfig { Subnet?: string; @@ -4299,157 +4299,7 @@ export interface Network { Labels?: Record; } -export type InspectDockerNetworkResponse = Network; - -export enum VolumeScopeEnum { - Empty = "", - Local = "local", - Global = "global", -} - -export enum ClusterVolumeSpecAccessModeScopeEnum { - Empty = "", - Single = "single", - Multi = "multi", -} - -export enum ClusterVolumeSpecAccessModeSharingEnum { - Empty = "", - None = "none", - Readonly = "readonly", - Onewriter = "onewriter", - All = "all", -} - -/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ -export interface ClusterVolumeSpecAccessModeSecrets { - /** Key is the name of the key of the key-value pair passed to the plugin. */ - Key?: string; - /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ - Secret?: string; -} - -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} - -/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ -export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { - /** A list of required topologies, at least one of which the volume must be accessible from. */ - Requisite?: Topology[]; - /** A list of topologies that the volume should attempt to be provisioned in. */ - Preferred?: Topology[]; -} - -/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ -export interface ClusterVolumeSpecAccessModeCapacityRange { - /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ - RequiredBytes?: I64; - /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ - LimitBytes?: I64; -} - -export enum ClusterVolumeSpecAccessModeAvailabilityEnum { - Empty = "", - Active = "active", - Pause = "pause", - Drain = "drain", -} - -/** Defines how the volume is used by tasks. */ -export interface ClusterVolumeSpecAccessMode { - /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ - Scope?: ClusterVolumeSpecAccessModeScopeEnum; - /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ - Sharing?: ClusterVolumeSpecAccessModeSharingEnum; - /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ - Secrets?: ClusterVolumeSpecAccessModeSecrets[]; - AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; - CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; - /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ - Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; -} - -/** Cluster-specific options used to create the volume. */ -export interface ClusterVolumeSpec { - /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ - Group?: string; - AccessMode?: ClusterVolumeSpecAccessMode; -} - -/** Information about the global status of the volume. */ -export interface ClusterVolumeInfo { - /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ - CapacityBytes?: I64; - /** A map of strings to strings returned from the storage plugin when the volume is created. */ - VolumeContext?: Record; - /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ - VolumeID?: string; - /** The topology this volume is actually accessible from. */ - AccessibleTopology?: Topology[]; -} - -export enum ClusterVolumePublishStatusStateEnum { - Empty = "", - PendingPublish = "pending-publish", - Published = "published", - PendingNodeUnpublish = "pending-node-unpublish", - PendingControllerUnpublish = "pending-controller-unpublish", -} - -export interface ClusterVolumePublishStatus { - /** The ID of the Swarm node the volume is published on. */ - NodeID?: string; - /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ - State?: ClusterVolumePublishStatusStateEnum; - /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ - PublishContext?: Record; -} - -/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ -export interface ClusterVolume { - /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ - ID?: string; - Version?: ObjectVersion; - CreatedAt?: string; - UpdatedAt?: string; - Spec?: ClusterVolumeSpec; - Info?: ClusterVolumeInfo; - /** The status of the volume as it pertains to its publishing and use on specific nodes */ - PublishStatus?: ClusterVolumePublishStatus[]; -} - -/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ -export interface VolumeUsageData { - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - Size: I64; - /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ - RefCount: I64; -} - -export interface Volume { - /** Name of the volume. */ - Name: string; - /** Name of the volume driver used by the volume. */ - Driver: string; - /** Mount path of the volume on the host. */ - Mountpoint: string; - /** Date/Time the volume was created. */ - CreatedAt?: string; - /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ - Status?: string[]; - /** User-defined key/value metadata. */ - Labels?: Record; - /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ - Scope?: VolumeScopeEnum; - ClusterVolume?: ClusterVolume; - /** The driver specific options used when creating the volume. */ - Options?: Record; - UsageData?: VolumeUsageData; -} - -export type InspectDockerVolumeResponse = Volume; +export type InspectNetworkResponse = Network; export type InspectStackContainerResponse = Container; @@ -4924,6 +4774,156 @@ export interface SwarmTask { export type InspectSwarmTaskResponse = SwarmTask; +export enum VolumeScopeEnum { + Empty = "", + Local = "local", + Global = "global", +} + +export enum ClusterVolumeSpecAccessModeScopeEnum { + Empty = "", + Single = "single", + Multi = "multi", +} + +export enum ClusterVolumeSpecAccessModeSharingEnum { + Empty = "", + None = "none", + Readonly = "readonly", + Onewriter = "onewriter", + All = "all", +} + +/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ +export interface ClusterVolumeSpecAccessModeSecrets { + /** Key is the name of the key of the key-value pair passed to the plugin. */ + Key?: string; + /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ + Secret?: string; +} + +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} + +/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ +export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { + /** A list of required topologies, at least one of which the volume must be accessible from. */ + Requisite?: Topology[]; + /** A list of topologies that the volume should attempt to be provisioned in. */ + Preferred?: Topology[]; +} + +/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ +export interface ClusterVolumeSpecAccessModeCapacityRange { + /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ + RequiredBytes?: I64; + /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ + LimitBytes?: I64; +} + +export enum ClusterVolumeSpecAccessModeAvailabilityEnum { + Empty = "", + Active = "active", + Pause = "pause", + Drain = "drain", +} + +/** Defines how the volume is used by tasks. */ +export interface ClusterVolumeSpecAccessMode { + /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ + Scope?: ClusterVolumeSpecAccessModeScopeEnum; + /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ + Sharing?: ClusterVolumeSpecAccessModeSharingEnum; + /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ + Secrets?: ClusterVolumeSpecAccessModeSecrets[]; + AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; + CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; + /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ + Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; +} + +/** Cluster-specific options used to create the volume. */ +export interface ClusterVolumeSpec { + /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ + Group?: string; + AccessMode?: ClusterVolumeSpecAccessMode; +} + +/** Information about the global status of the volume. */ +export interface ClusterVolumeInfo { + /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ + CapacityBytes?: I64; + /** A map of strings to strings returned from the storage plugin when the volume is created. */ + VolumeContext?: Record; + /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ + VolumeID?: string; + /** The topology this volume is actually accessible from. */ + AccessibleTopology?: Topology[]; +} + +export enum ClusterVolumePublishStatusStateEnum { + Empty = "", + PendingPublish = "pending-publish", + Published = "published", + PendingNodeUnpublish = "pending-node-unpublish", + PendingControllerUnpublish = "pending-controller-unpublish", +} + +export interface ClusterVolumePublishStatus { + /** The ID of the Swarm node the volume is published on. */ + NodeID?: string; + /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ + State?: ClusterVolumePublishStatusStateEnum; + /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ + PublishContext?: Record; +} + +/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ +export interface ClusterVolume { + /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ + ID?: string; + Version?: ObjectVersion; + CreatedAt?: string; + UpdatedAt?: string; + Spec?: ClusterVolumeSpec; + Info?: ClusterVolumeInfo; + /** The status of the volume as it pertains to its publishing and use on specific nodes */ + PublishStatus?: ClusterVolumePublishStatus[]; +} + +/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ +export interface VolumeUsageData { + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + Size: I64; + /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ + RefCount: I64; +} + +export interface Volume { + /** Name of the volume. */ + Name: string; + /** Name of the volume driver used by the volume. */ + Driver: string; + /** Mount path of the volume on the host. */ + Mountpoint: string; + /** Date/Time the volume was created. */ + CreatedAt?: string; + /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ + Status?: string[]; + /** User-defined key/value metadata. */ + Labels?: Record; + /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ + Scope?: VolumeScopeEnum; + ClusterVolume?: ClusterVolume; + /** The driver specific options used when creating the volume. */ + Options?: Record; + UsageData?: VolumeUsageData; +} + +export type InspectVolumeResponse = Volume; + export type JsonObject = any; export type ListActionsResponse = ActionListItem[]; @@ -4988,7 +4988,7 @@ export interface ContainerListItem { labels?: Record; } -export type ListAllDockerContainersResponse = ContainerListItem[]; +export type ListAllContainersResponse = ContainerListItem[]; /** * Combined state options for @@ -5091,9 +5091,51 @@ export interface ComposeProject { export type ListComposeProjectsResponse = ComposeProject[]; +export type ListContainersResponse = ContainerListItem[]; + export type ListDeploymentsResponse = DeploymentListItem[]; -export type ListDockerContainersResponse = ContainerListItem[]; +export type ListFullActionsResponse = Action[]; + +export type ListFullAlertersResponse = Alerter[]; + +export type ListFullBuildersResponse = Builder[]; + +export type ListFullBuildsResponse = Build[]; + +export type ListFullDeploymentsResponse = Deployment[]; + +export type ListFullProceduresResponse = Procedure[]; + +export type ListFullReposResponse = Repo[]; + +export type ListFullResourceSyncsResponse = ResourceSync[]; + +export type ListFullServersResponse = Server[]; + +export type ListFullStacksResponse = Stack[]; + +export type ListFullSwarmsResponse = Swarm[]; + +export type ListGitProviderAccountsResponse = GitProviderAccount[]; + +export interface ProviderAccount { + /** The account username. Required. */ + username: string; + /** The account access token. Required. */ + token?: string; +} + +export interface GitProvider { + /** The git provider domain. Default: `github.com`. */ + domain: string; + /** Whether to use https. Default: true. */ + https: boolean; + /** The accounts on the git provider. Required. */ + accounts: ProviderAccount[]; +} + +export type ListGitProvidersFromConfigResponse = GitProvider[]; /** individual image layer information in response to ImageHistory operation */ export interface ImageHistoryResponseItem { @@ -5105,7 +5147,23 @@ export interface ImageHistoryResponseItem { Comment: string; } -export type ListDockerImageHistoryResponse = ImageHistoryResponseItem[]; +export type ListImageHistoryResponse = ImageHistoryResponseItem[]; + +export interface ImageRegistry { + /** The image provider domain. Default: `docker.io`. */ + domain: string; + /** The accounts on the registry. Required. */ + accounts: ProviderAccount[]; + /** + * Available organizations on the registry provider. + * Used to push an image under an organization's repo rather than an account's repo. + */ + organizations?: string[]; +} + +export type ListImageRegistriesFromConfigResponse = ImageRegistry[]; + +export type ListImageRegistryAccountsResponse = ImageRegistryAccount[]; export interface ImageListItem { /** @@ -5134,99 +5192,26 @@ export interface ImageListItem { in_use: boolean; } -export type ListDockerImagesResponse = ImageListItem[]; +export type ListImagesResponse = ImageListItem[]; export interface NetworkListItem { name?: string; id?: string; created?: string; - scope?: string; - driver?: string; - enable_ipv6?: boolean; - ipam_driver?: string; - ipam_subnet?: string; - ipam_gateway?: string; - internal?: boolean; - attachable?: boolean; - ingress?: boolean; - /** Whether the network is attached to one or more containers */ - in_use: boolean; -} - -export type ListDockerNetworksResponse = NetworkListItem[]; - -export interface ProviderAccount { - /** The account username. Required. */ - username: string; - /** The account access token. Required. */ - token?: string; -} - -export interface DockerRegistry { - /** The docker provider domain. Default: `docker.io`. */ - domain: string; - /** The accounts on the registry. Required. */ - accounts: ProviderAccount[]; - /** - * Available organizations on the registry provider. - * Used to push an image under an organization's repo rather than an account's repo. - */ - organizations?: string[]; -} - -export type ListDockerRegistriesFromConfigResponse = DockerRegistry[]; - -export type ListDockerRegistryAccountsResponse = DockerRegistryAccount[]; - -export interface VolumeListItem { - /** The name of the volume */ - name: string; - driver: string; - mountpoint: string; - created?: string; - scope: VolumeScopeEnum; - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - size?: I64; - /** Whether the volume is currently attached to any container */ - in_use: boolean; -} - -export type ListDockerVolumesResponse = VolumeListItem[]; - -export type ListFullActionsResponse = Action[]; - -export type ListFullAlertersResponse = Alerter[]; - -export type ListFullBuildersResponse = Builder[]; - -export type ListFullBuildsResponse = Build[]; - -export type ListFullDeploymentsResponse = Deployment[]; - -export type ListFullProceduresResponse = Procedure[]; - -export type ListFullReposResponse = Repo[]; - -export type ListFullResourceSyncsResponse = ResourceSync[]; - -export type ListFullServersResponse = Server[]; - -export type ListFullStacksResponse = Stack[]; - -export type ListFullSwarmsResponse = Swarm[]; - -export type ListGitProviderAccountsResponse = GitProviderAccount[]; - -export interface GitProvider { - /** The git provider domain. Default: `github.com`. */ - domain: string; - /** Whether to use https. Default: true. */ - https: boolean; - /** The accounts on the git provider. Required. */ - accounts: ProviderAccount[]; + scope?: string; + driver?: string; + enable_ipv6?: boolean; + ipam_driver?: string; + ipam_subnet?: string; + ipam_gateway?: string; + internal?: boolean; + attachable?: boolean; + ingress?: boolean; + /** Whether the network is attached to one or more containers */ + in_use: boolean; } -export type ListGitProvidersFromConfigResponse = GitProvider[]; +export type ListNetworksResponse = NetworkListItem[]; export type ListOnboardingKeysResponse = OnboardingKey[]; @@ -5746,6 +5731,21 @@ export type ListUsersResponse = User[]; export type ListVariablesResponse = Variable[]; +export interface VolumeListItem { + /** The name of the volume */ + name: string; + driver: string; + mountpoint: string; + created?: string; + scope: VolumeScopeEnum; + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + size?: I64; + /** Whether the volume is currently attached to any container */ + in_use: boolean; +} + +export type ListVolumesResponse = VolumeListItem[]; + export type MongoDocument = any; export interface ProcedureQuerySpecifics { @@ -5812,10 +5812,10 @@ export interface SwarmQuerySpecifics { export type SwarmQuery = ResourceQuery; -export type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; - export type UpdateGitProviderAccountResponse = GitProviderAccount; +export type UpdateImageRegistryAccountResponse = ImageRegistryAccount; + export type UpdateOnboardingKeyResponse = OnboardingKey; export type UpdatePermissionOnResourceTypeResponse = NoData; @@ -5850,10 +5850,10 @@ export type _PartialBuilderConfig = Partial; export type _PartialDeploymentConfig = Partial; -export type _PartialDockerRegistryAccount = Partial; - export type _PartialGitProviderAccount = Partial; +export type _PartialImageRegistryAccount = Partial; + export type _PartialProcedureConfig = Partial; export type _PartialRepoConfig = Partial; @@ -5930,8 +5930,12 @@ export interface AwsBuilderConfig { insecure_tls: boolean; /** Which git providers are available on the AMI */ git_providers?: GitProvider[]; - /** Which docker registries are available on the AMI. */ - docker_registries?: DockerRegistry[]; + /** + * Which image registries are available on the AMI. + * + * Pre v2.3.0, called `docker_registries` + */ + image_registries?: ImageRegistry[]; /** Which secrets are available on the AMI. */ secrets?: string[]; } @@ -6853,14 +6857,6 @@ export interface CreateDeploymentFromContainer { server: string; } -/** - * **Admin only.** Create a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface CreateDockerRegistryAccount { - account: _PartialDockerRegistryAccount; -} - /** * **Admin only.** Create a git provider account. * Response: [GitProviderAccount]. @@ -6873,6 +6869,16 @@ export interface CreateGitProviderAccount { account: _PartialGitProviderAccount; } +/** + * **Admin only.** Create an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `CreateDockerRegistryAccount` + */ +export interface CreateImageRegistryAccount { + account: _PartialImageRegistryAccount; +} + /** * **Admin only.** Create a local user. * Response: [User]. @@ -7177,15 +7183,6 @@ export interface DeleteDeployment { id: string; } -/** - * **Admin only.** Delete a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface DeleteDockerRegistryAccount { - /** The id of the docker registry account to delete */ - id: string; -} - /** * **Admin only.** Delete a git provider account. * Response: [DeleteGitProviderAccountResponse]. @@ -7206,6 +7203,17 @@ export interface DeleteImage { name: string; } +/** + * **Admin only.** Delete an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `DeleteDockerRegistryAccount` + */ +export interface DeleteImageRegistryAccount { + /** The id of the image registry account to delete */ + id: string; +} + /** * Delete a docker network. * Response: [Update] @@ -7826,6 +7834,29 @@ export interface GetContainerLog { timestamps?: boolean; } +/** + * Gets a summary of data relating to all containers. + * Response: [GetContainersSummaryResponse]. + * + * Pre v2.3.0, called `GetDockerContainersSummary` + */ +export interface GetContainersSummary { +} + +/** Response for [GetContainersSummary] */ +export interface GetContainersSummaryResponse { + /** The total number of Containers */ + total: number; + /** The number of Containers with Running state */ + running: number; + /** The number of Containers with Stopped or Paused or Created state */ + stopped: number; + /** The number of Containers with Restarting or Dead state */ + unhealthy: number; + /** The number of Containers with Unknown state */ + unknown: number; +} + /** * Get information about the Komodo Core API configuration. * Response: [GetCoreInfoResponse]. @@ -7946,35 +7977,6 @@ export interface GetDeploymentsSummaryResponse { unknown: I64; } -/** - * Gets a summary of data relating to all containers. - * Response: [GetDockerContainersSummaryResponse]. - */ -export interface GetDockerContainersSummary { -} - -/** Response for [GetDockerContainersSummary] */ -export interface GetDockerContainersSummaryResponse { - /** The total number of Containers */ - total: number; - /** The number of Containers with Running state */ - running: number; - /** The number of Containers with Stopped or Paused or Created state */ - stopped: number; - /** The number of Containers with Restarting or Dead state */ - unhealthy: number; - /** The number of Containers with Unknown state */ - unknown: number; -} - -/** - * Get a specific docker registry account. - * Response: [GetDockerRegistryAccountResponse]. - */ -export interface GetDockerRegistryAccount { - id: string; -} - /** * Get a specific git provider account. * Response: [GetGitProviderAccountResponse]. @@ -8053,6 +8055,16 @@ export interface GetHistoricalServerStatsResponse { next_page?: number; } +/** + * Get a specific image registry account. + * Response: [GetImageRegistryAccountResponse]. + * + * Pre v2.3.0, called `GetDockerRegistryAccount` + */ +export interface GetImageRegistryAccount { + id: string; +} + /** * Get the Periphery information of the target server, * including the Periphery version and public key. @@ -8459,6 +8471,18 @@ export interface GlobalAutoUpdate { skip_auto_update?: boolean; } +/** + * Inspect a container on the server. Response: [Container]. + * + * Pre v2.3.0, called `InspectDockerContainer` + */ +export interface InspectContainer { + /** Id or name */ + server: string; + /** The container name */ + container: string; +} + /** * Inspect the docker container associated with the Deployment. * Response: [Container]. @@ -8477,38 +8501,30 @@ export interface InspectDeploymentSwarmService { deployment: string; } -/** Inspect a docker container on the server. Response: [Container]. */ -export interface InspectDockerContainer { - /** Id or name */ - server: string; - /** The container name */ - container: string; -} - -/** Inspect a docker image on the server. Response: [Image]. */ -export interface InspectDockerImage { +/** + * Inspect a container image on the server. Response: [Image]. + * + * Pre v2.3.0, called `InspectDockerImage` + */ +export interface InspectImage { /** Id or name */ server: string; /** The image name */ image: string; } -/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */ -export interface InspectDockerNetwork { +/** + * Inspect a container network on the server. Response: [InspectNetworkResponse]. + * + * Pre v2.3.0, called `InspectDockerNetwork` + */ +export interface InspectNetwork { /** Id or name */ server: string; /** The network name */ network: string; } -/** Inspect a docker volume on the server. Response: [Volume]. */ -export interface InspectDockerVolume { - /** Id or name */ - server: string; - /** The volume name */ - volume: string; -} - /** * Inspect a docker container associated with a Stack. * Response: [Container]. @@ -8615,6 +8631,18 @@ export interface InspectSwarmTask { task: string; } +/** + * Inspect a container volume on the server. Response: [Volume]. + * + * Pre v2.3.0, called `InspectDockerVolume` + */ +export interface InspectVolume { + /** Id or name */ + server: string; + /** The volume name */ + volume: string; +} + export interface LatestCommit { hash: string; message: string; @@ -8682,10 +8710,12 @@ export interface ListAlertsResponse { } /** - * List all docker containers on the target servers. - * Response: [ListDockerContainersResponse]. + * List all containers on the target servers. + * Response: [ListAllContainersResponse]. + * + * Pre v2.3.0, called `ListAllDockerContainers` */ -export interface ListAllDockerContainers { +export interface ListAllContainers { /** Filter by server id or name. */ servers?: string[]; /** Filter servers by tag. */ @@ -8827,7 +8857,7 @@ export interface ListCommonStackExtraArgs { } /** - * List all docker compose projects on the target server. + * List all compose projects on the target server. * Response: [ListComposeProjectsResponse]. */ export interface ListComposeProjects { @@ -8836,81 +8866,23 @@ export interface ListComposeProjects { } /** - * List deployments matching optional query. - * Response: [ListDeploymentsResponse]. - */ -export interface ListDeployments { - /** optional structured query to filter deployments. */ - query?: DeploymentQuery; -} - -/** - * List all docker containers on the target server. - * Response: [ListDockerContainersResponse]. - */ -export interface ListDockerContainers { - /** Id or name */ - server: string; -} - -/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */ -export interface ListDockerImageHistory { - /** Id or name */ - server: string; - /** The image name */ - image: string; -} - -/** - * List the docker images locally cached on the target server. - * Response: [ListDockerImagesResponse]. + * List all containers on the target server. + * Response: [ListContainersResponse]. + * + * Pre v2.3.0, called `ListDockerContainers` */ -export interface ListDockerImages { - /** Id or name */ - server: string; -} - -/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */ -export interface ListDockerNetworks { +export interface ListContainers { /** Id or name */ server: string; } /** - * List the docker registry providers available in Core / Periphery config files. - * Response: [ListDockerRegistriesFromConfigResponse]. - * - * Includes: - * - registries in core config - * - registries configured on builds, deployments - * - registries on the optional Server or Builder - */ -export interface ListDockerRegistriesFromConfig { - /** - * Accepts an optional Server or Builder target to expand the core list with - * providers available on that specific resource. - */ - target?: ResourceTarget; -} - -/** - * List docker registry accounts matching optional query. - * Response: [ListDockerRegistryAccountsResponse]. - */ -export interface ListDockerRegistryAccounts { - /** Optionally filter by accounts with a specific domain. */ - domain?: string; - /** Optionally filter by accounts with a specific username. */ - username?: string; -} - -/** - * List all docker volumes on the target server. - * Response: [ListDockerVolumesResponse]. + * List deployments matching optional query. + * Response: [ListDeploymentsResponse]. */ -export interface ListDockerVolumes { - /** Id or name */ - server: string; +export interface ListDeployments { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ @@ -9009,6 +8981,71 @@ export interface ListGitProvidersFromConfig { target?: ResourceTarget; } +/** + * Get image history from the server. Response: [ListImageHistoryResponse]. + * + * Pre v2.3.0, called `ListDockerImageHistory` + */ +export interface ListImageHistory { + /** Id or name */ + server: string; + /** The image name */ + image: string; +} + +/** + * List the image registry providers available in Core / Periphery config files. + * Response: [ListImageRegistriesFromConfigResponse]. + * + * Includes: + * - registries in core config + * - registries configured on builds, deployments + * - registries on the optional Server or Builder + * + * Pre v2.3.0, called `ListDockerRegistriesFromConfig` + */ +export interface ListImageRegistriesFromConfig { + /** + * Accepts an optional Server or Builder target to expand the core list with + * providers available on that specific resource. + */ + target?: ResourceTarget; +} + +/** + * List image registry accounts matching optional query. + * Response: [ListImageRegistryAccountsResponse]. + * + * Pre v2.3.0, called `ListDockerRegistryAccounts` + */ +export interface ListImageRegistryAccounts { + /** Optionally filter by accounts with a specific domain. */ + domain?: string; + /** Optionally filter by accounts with a specific username. */ + username?: string; +} + +/** + * List the container images locally cached on the target server. + * Response: [ListImagesResponse]. + * + * Pre v2.3.0, called `ListDockerImages` + */ +export interface ListImages { + /** Id or name */ + server: string; +} + +/** + * List the container networks on the server. Response: [ListNetworksResponse]. + * + * Pre v2.3.0, called `ListDockerNetworks` + */ +export interface ListNetworks { + /** Id or name */ + server: string; +} + /** * **Admin only.** Gets list of onboarding keys. * Response: [ListOnboardingKeysResponse] @@ -9296,6 +9333,17 @@ export interface ListUsers { export interface ListVariables { } +/** + * List all container volumes on the target server. + * Response: [ListVolumesResponse]. + * + * Pre v2.3.0, called `ListDockerVolumes` + */ +export interface ListVolumes { + /** Id or name */ + server: string; +} + export interface NameAndId { name: string; id: string; @@ -10442,17 +10490,6 @@ export interface UpdateDeployment { config: _PartialDeploymentConfig; } -/** - * **Admin only.** Update a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface UpdateDockerRegistryAccount { - /** The id of the docker registry to update */ - id: string; - /** The partial docker registry account. */ - account: _PartialDockerRegistryAccount; -} - /** * **Admin only.** Update a git provider account. * Response: [GitProviderAccount]. @@ -10464,6 +10501,19 @@ export interface UpdateGitProviderAccount { account: _PartialGitProviderAccount; } +/** + * **Admin only.** Update a image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `UpdateDockerRegistryAccount` + */ +export interface UpdateImageRegistryAccount { + /** The id of the image registry to update */ + id: string; + /** The partial image registry account. */ + account: _PartialImageRegistryAccount; +} + /** * **Admin only.** Update an onboarding key. * Response: The updated [OnboardingKey]. @@ -10996,7 +11046,7 @@ export type ReadRequest = | { type: "GetCoreInfo", params: GetCoreInfo } | { type: "ListSecrets", params: ListSecrets } | { type: "ListGitProvidersFromConfig", params: ListGitProvidersFromConfig } - | { type: "ListDockerRegistriesFromConfig", params: ListDockerRegistriesFromConfig } + | { type: "ListImageRegistriesFromConfig", params: ListImageRegistriesFromConfig } | { type: "GetSwarmsSummary", params: GetSwarmsSummary } | { type: "GetSwarm", params: GetSwarm } | { type: "GetSwarmActionState", params: GetSwarmActionState } @@ -11026,21 +11076,21 @@ export type ReadRequest = | { type: "ListServers", params: ListServers } | { type: "ListFullServers", params: ListFullServers } | { type: "ListTerminals", params: ListTerminals } - | { type: "GetDockerContainersSummary", params: GetDockerContainersSummary } - | { type: "ListAllDockerContainers", params: ListAllDockerContainers } - | { type: "ListDockerContainers", params: ListDockerContainers } - | { type: "InspectDockerContainer", params: InspectDockerContainer } + | { type: "GetContainersSummary", params: GetContainersSummary } + | { type: "ListAllContainers", params: ListAllContainers } + | { type: "ListContainers", params: ListContainers } + | { type: "InspectContainer", params: InspectContainer } | { type: "GetResourceMatchingContainer", params: GetResourceMatchingContainer } | { type: "GetContainerLog", params: GetContainerLog } | { type: "SearchContainerLog", params: SearchContainerLog } | { type: "ListComposeProjects", params: ListComposeProjects } - | { type: "ListDockerNetworks", params: ListDockerNetworks } - | { type: "InspectDockerNetwork", params: InspectDockerNetwork } - | { type: "ListDockerImages", params: ListDockerImages } - | { type: "InspectDockerImage", params: InspectDockerImage } - | { type: "ListDockerImageHistory", params: ListDockerImageHistory } - | { type: "ListDockerVolumes", params: ListDockerVolumes } - | { type: "InspectDockerVolume", params: InspectDockerVolume } + | { type: "ListNetworks", params: ListNetworks } + | { type: "InspectNetwork", params: InspectNetwork } + | { type: "ListImages", params: ListImages } + | { type: "InspectImage", params: InspectImage } + | { type: "ListImageHistory", params: ListImageHistory } + | { type: "ListVolumes", params: ListVolumes } + | { type: "InspectVolume", params: InspectVolume } | { type: "GetSystemInformation", params: GetSystemInformation } | { type: "GetSystemStats", params: GetSystemStats } | { type: "GetHistoricalServerStats", params: GetHistoricalServerStats } @@ -11129,8 +11179,8 @@ export type ReadRequest = | { type: "ListVariables", params: ListVariables } | { type: "GetGitProviderAccount", params: GetGitProviderAccount } | { type: "ListGitProviderAccounts", params: ListGitProviderAccounts } - | { type: "GetDockerRegistryAccount", params: GetDockerRegistryAccount } - | { type: "ListDockerRegistryAccounts", params: ListDockerRegistryAccounts } + | { type: "GetImageRegistryAccount", params: GetImageRegistryAccount } + | { type: "ListImageRegistryAccounts", params: ListImageRegistryAccounts } | { type: "ListOnboardingKeys", params: ListOnboardingKeys }; export enum RepoWebhookAction { @@ -11299,9 +11349,9 @@ export type WriteRequest = | { type: "CreateGitProviderAccount", params: CreateGitProviderAccount } | { type: "UpdateGitProviderAccount", params: UpdateGitProviderAccount } | { type: "DeleteGitProviderAccount", params: DeleteGitProviderAccount } - | { type: "CreateDockerRegistryAccount", params: CreateDockerRegistryAccount } - | { type: "UpdateDockerRegistryAccount", params: UpdateDockerRegistryAccount } - | { type: "DeleteDockerRegistryAccount", params: DeleteDockerRegistryAccount } + | { type: "CreateImageRegistryAccount", params: CreateImageRegistryAccount } + | { type: "UpdateImageRegistryAccount", params: UpdateImageRegistryAccount } + | { type: "DeleteImageRegistryAccount", params: DeleteImageRegistryAccount } | { type: "CloseAlert", params: CloseAlert }; export type WsLoginMessage = diff --git a/client/periphery/rs/src/api/mod.rs b/client/periphery/rs/src/api/mod.rs index 76f4d13bd0..ef5c1d0219 100644 --- a/client/periphery/rs/src/api/mod.rs +++ b/client/periphery/rs/src/api/mod.rs @@ -1,6 +1,6 @@ use komodo_client::entities::{ FileContents, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, stack::{StackRemoteFileContents, StackServiceNames}, update::Log, }; @@ -66,11 +66,11 @@ pub type ListGitProvidersResponse = Vec; // #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] -#[response(ListDockerRegistriesResponse)] +#[response(ListImageRegistriesResponse)] #[error(anyhow::Error)] -pub struct ListDockerRegistries {} +pub struct ListImageRegistries {} -pub type ListDockerRegistriesResponse = Vec; +pub type ListImageRegistriesResponse = Vec; // diff --git a/config/core.config.toml b/config/core.config.toml index a662275249..26dadfe06e 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -623,15 +623,15 @@ aws.secret_access_key = "" ## They allow these Resources to pull private images. ## They cannot be configured on the environment. -## configure docker registries -# [[docker_registry]] +## configure image registries +# [[image_registry]] # domain = "docker.io" # accounts = [ # { username = "mbecker2020", token = "access_token_for_account" } # ] # organizations = ["DockerhubOrganization"] -# [[docker_registry]] +# [[image_registry]] # domain = "git.mogh.tech" # use a custom provider, like self-hosted gitea # accounts = [ # { username = "mbecker20", token = "access_token_for_account" }, diff --git a/config/periphery.config.toml b/config/periphery.config.toml index 0da92f59bc..838e4ba948 100644 --- a/config/periphery.config.toml +++ b/config/periphery.config.toml @@ -270,15 +270,15 @@ pretty_startup_config = false # REGISTRY PROVIDERS # ###################### -## Configure Periphery based docker registries -# [[docker_registry]] +## Configure Periphery based image registries +# [[image_registry]] # domain = "docker.io" # accounts = [ # { username = "mbecker2020", token = "access_token_for_account" } # ] # organizations = ["DockerhubOrganization"] -# [[docker_registry]] +# [[image_registry]] # domain = "git.mogh.tech" # use a custom provider, like self-hosted gitea # accounts = [ # { username = "mbecker20", token = "access_token_for_account" }, diff --git a/docsite/docs/configuration/providers.md b/docsite/docs/configuration/providers.md index c32fec6197..41c78a32a3 100644 --- a/docsite/docs/configuration/providers.md +++ b/docsite/docs/configuration/providers.md @@ -91,20 +91,20 @@ Komodo supports pushing and pulling images from any Docker-compatible registry, ```toml # in core.config.toml or periphery.config.toml -[[docker_registry]] +[[image_registry]] domain = "docker.io" accounts = [ { username = "my-user", token = "dckr_pat_xxxxxxxxxxxx" }, ] organizations = ["MyOrg"] -[[docker_registry]] +[[image_registry]] domain = "ghcr.io" accounts = [ { username = "my-user", token = "ghp_xxxxxxxxxxxx" }, ] -[[docker_registry]] +[[image_registry]] domain = "registry.example.com" # self-hosted registry accounts = [ { username = "my-user", token = "access_token" }, diff --git a/lib/database/src/lib.rs b/lib/database/src/lib.rs index bf46f611ea..d0e50e1d2c 100644 --- a/lib/database/src/lib.rs +++ b/lib/database/src/lib.rs @@ -13,7 +13,7 @@ use komodo_client::entities::{ onboarding_key::OnboardingKey, permission::Permission, procedure::Procedure, - provider::{DockerRegistryAccount, GitProviderAccount}, + provider::{GitProviderAccount, ImageRegistryAccount}, repo::Repo, server::Server, stack::Stack, @@ -52,7 +52,7 @@ pub struct Client { pub tags: Collection, pub variables: Collection, pub git_accounts: Collection, - pub registry_accounts: Collection, + pub registry_accounts: Collection, pub updates: Collection, pub alerts: Collection, pub stats: Collection, diff --git a/ui/public/client/responses.d.ts b/ui/public/client/responses.d.ts index 0c2e987529..7319afd1a7 100644 --- a/ui/public/client/responses.d.ts +++ b/ui/public/client/responses.d.ts @@ -4,7 +4,7 @@ export type ReadResponses = { GetCoreInfo: Types.GetCoreInfoResponse; ListSecrets: Types.ListSecretsResponse; ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse; - ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse; + ListImageRegistriesFromConfig: Types.ListImageRegistriesFromConfigResponse; GetSwarmsSummary: Types.GetSwarmsSummaryResponse; GetSwarm: Types.GetSwarmResponse; GetSwarmActionState: Types.GetSwarmActionStateResponse; @@ -34,21 +34,21 @@ export type ReadResponses = { ListServers: Types.ListServersResponse; ListFullServers: Types.ListFullServersResponse; ListTerminals: Types.ListTerminalsResponse; - GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse; - ListAllDockerContainers: Types.ListAllDockerContainersResponse; - ListDockerContainers: Types.ListDockerContainersResponse; - InspectDockerContainer: Types.InspectDockerContainerResponse; + GetContainersSummary: Types.GetContainersSummaryResponse; + ListAllContainers: Types.ListAllContainersResponse; + ListContainers: Types.ListContainersResponse; + InspectContainer: Types.InspectContainerResponse; GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse; GetContainerLog: Types.GetContainerLogResponse; SearchContainerLog: Types.SearchContainerLogResponse; ListComposeProjects: Types.ListComposeProjectsResponse; - ListDockerNetworks: Types.ListDockerNetworksResponse; - InspectDockerNetwork: Types.InspectDockerNetworkResponse; - ListDockerImages: Types.ListDockerImagesResponse; - InspectDockerImage: Types.InspectDockerImageResponse; - ListDockerImageHistory: Types.ListDockerImageHistoryResponse; - ListDockerVolumes: Types.ListDockerVolumesResponse; - InspectDockerVolume: Types.InspectDockerVolumeResponse; + ListNetworks: Types.ListNetworksResponse; + InspectNetwork: Types.InspectNetworkResponse; + ListImages: Types.ListImagesResponse; + InspectImage: Types.InspectImageResponse; + ListImageHistory: Types.ListImageHistoryResponse; + ListVolumes: Types.ListVolumesResponse; + InspectVolume: Types.InspectVolumeResponse; GetSystemInformation: Types.GetSystemInformationResponse; GetSystemStats: Types.GetSystemStatsResponse; GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse; @@ -137,8 +137,8 @@ export type ReadResponses = { ListVariables: Types.ListVariablesResponse; GetGitProviderAccount: Types.GetGitProviderAccountResponse; ListGitProviderAccounts: Types.ListGitProviderAccountsResponse; - GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse; - ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse; + GetImageRegistryAccount: Types.GetImageRegistryAccountResponse; + ListImageRegistryAccounts: Types.ListImageRegistryAccountsResponse; ListOnboardingKeys: Types.ListOnboardingKeysResponse; }; export type WriteResponses = { @@ -252,9 +252,9 @@ export type WriteResponses = { CreateGitProviderAccount: Types.CreateGitProviderAccountResponse; UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse; DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse; - CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse; - UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse; - DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse; + CreateImageRegistryAccount: Types.CreateImageRegistryAccountResponse; + UpdateImageRegistryAccount: Types.UpdateImageRegistryAccountResponse; + DeleteImageRegistryAccount: Types.DeleteImageRegistryAccountResponse; CloseAlert: Types.NoData; }; export type ExecuteResponses = { diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index dd2111a4f5..4c1c588274 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -1189,22 +1189,26 @@ export interface CreateApiKeyResponse { secret: string; } export type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; -/** Configuration to access private image repositories on various registries. */ -export interface DockerRegistryAccount { +/** + * Configuration to access private git repos from various git providers. + * Note. Cannot create two accounts with the same domain and username. + */ +export interface GitProviderAccount { /** - * The Mongo ID of the docker registry account. + * The Mongo ID of the git provider account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` */ _id?: MongoId; /** * The domain of the provider. * - * For docker registry, this can include 'http://...', - * however this is not recommended and won't work unless "insecure registries" are enabled - * on your hosts. See . + * For git, this cannot include the protocol eg 'http://', + * which is controlled with 'https' field. */ domain: string; + /** Whether git provider is accessed over http or https. */ + https: boolean; /** The account username */ username?: string; /** @@ -1213,27 +1217,23 @@ export interface DockerRegistryAccount { */ token?: string; } -export type CreateDockerRegistryAccountResponse = DockerRegistryAccount; -/** - * Configuration to access private git repos from various git providers. - * Note. Cannot create two accounts with the same domain and username. - */ -export interface GitProviderAccount { +export type CreateGitProviderAccountResponse = GitProviderAccount; +/** Configuration to access private image repositories on various registries. */ +export interface ImageRegistryAccount { /** - * The Mongo ID of the git provider account. + * The Mongo ID of the docker registry account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + * `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` */ _id?: MongoId; /** * The domain of the provider. * - * For git, this cannot include the protocol eg 'http://', - * which is controlled with 'https' field. + * For docker registry, this can include 'http://...', + * however this is not recommended and won't work unless "insecure registries" are enabled + * on your hosts. See . */ domain: string; - /** Whether git provider is accessed over http or https. */ - https: boolean; /** The account username */ username?: string; /** @@ -1242,7 +1242,7 @@ export interface GitProviderAccount { */ token?: string; } -export type CreateGitProviderAccountResponse = GitProviderAccount; +export type CreateImageRegistryAccountResponse = ImageRegistryAccount; export type UserConfig = /** User that logs in with username / password */ { @@ -1367,8 +1367,8 @@ export interface Variable { } export type CreateVariableResponse = Variable; export type DeleteApiKeyForServiceUserResponse = NoData; -export type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; export type DeleteGitProviderAccountResponse = GitProviderAccount; +export type DeleteImageRegistryAccountResponse = ImageRegistryAccount; /** * An public key used to authenticate new Periphery -> Core connections * to join Komodo as a newly created Server. @@ -2057,8 +2057,8 @@ export interface ContainerStats { pids: string; } export type GetDeploymentStatsResponse = ContainerStats; -export type GetDockerRegistryAccountResponse = DockerRegistryAccount; export type GetGitProviderAccountResponse = GitProviderAccount; +export type GetImageRegistryAccountResponse = ImageRegistryAccount; export declare enum Timelength { /** `1-sec` */ OneSecond = "1-sec", @@ -3624,6 +3624,7 @@ export interface Container { Config?: ContainerConfig; NetworkSettings?: NetworkSettings; } +export type InspectContainerResponse = Container; export type InspectDeploymentContainerResponse = Container; /** The service mode. */ export declare enum SwarmServiceMode { @@ -4128,7 +4129,6 @@ export interface SwarmService { JobStatus?: ServiceJobStatus; } export type InspectDeploymentSwarmServiceResponse = SwarmService; -export type InspectDockerContainerResponse = Container; /** Describes the platform which the image in the manifest runs on, as defined in the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). */ export interface OciPlatform { /** The CPU architecture, for example `amd64` or `ppc64`. */ @@ -4273,7 +4273,7 @@ export interface Image { RootFS?: ImageInspectRootFs; Metadata?: ImageInspectMetadata; } -export type InspectDockerImageResponse = Image; +export type InspectImageResponse = Image; export interface IpamConfig { Subnet?: string; IPRange?: string; @@ -4313,140 +4313,7 @@ export interface Network { Options?: Record; Labels?: Record; } -export type InspectDockerNetworkResponse = Network; -export declare enum VolumeScopeEnum { - Empty = "", - Local = "local", - Global = "global" -} -export declare enum ClusterVolumeSpecAccessModeScopeEnum { - Empty = "", - Single = "single", - Multi = "multi" -} -export declare enum ClusterVolumeSpecAccessModeSharingEnum { - Empty = "", - None = "none", - Readonly = "readonly", - Onewriter = "onewriter", - All = "all" -} -/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ -export interface ClusterVolumeSpecAccessModeSecrets { - /** Key is the name of the key of the key-value pair passed to the plugin. */ - Key?: string; - /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ - Secret?: string; -} -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} -/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ -export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { - /** A list of required topologies, at least one of which the volume must be accessible from. */ - Requisite?: Topology[]; - /** A list of topologies that the volume should attempt to be provisioned in. */ - Preferred?: Topology[]; -} -/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ -export interface ClusterVolumeSpecAccessModeCapacityRange { - /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ - RequiredBytes?: I64; - /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ - LimitBytes?: I64; -} -export declare enum ClusterVolumeSpecAccessModeAvailabilityEnum { - Empty = "", - Active = "active", - Pause = "pause", - Drain = "drain" -} -/** Defines how the volume is used by tasks. */ -export interface ClusterVolumeSpecAccessMode { - /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ - Scope?: ClusterVolumeSpecAccessModeScopeEnum; - /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ - Sharing?: ClusterVolumeSpecAccessModeSharingEnum; - /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ - Secrets?: ClusterVolumeSpecAccessModeSecrets[]; - AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; - CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; - /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ - Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; -} -/** Cluster-specific options used to create the volume. */ -export interface ClusterVolumeSpec { - /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ - Group?: string; - AccessMode?: ClusterVolumeSpecAccessMode; -} -/** Information about the global status of the volume. */ -export interface ClusterVolumeInfo { - /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ - CapacityBytes?: I64; - /** A map of strings to strings returned from the storage plugin when the volume is created. */ - VolumeContext?: Record; - /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ - VolumeID?: string; - /** The topology this volume is actually accessible from. */ - AccessibleTopology?: Topology[]; -} -export declare enum ClusterVolumePublishStatusStateEnum { - Empty = "", - PendingPublish = "pending-publish", - Published = "published", - PendingNodeUnpublish = "pending-node-unpublish", - PendingControllerUnpublish = "pending-controller-unpublish" -} -export interface ClusterVolumePublishStatus { - /** The ID of the Swarm node the volume is published on. */ - NodeID?: string; - /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ - State?: ClusterVolumePublishStatusStateEnum; - /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ - PublishContext?: Record; -} -/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ -export interface ClusterVolume { - /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ - ID?: string; - Version?: ObjectVersion; - CreatedAt?: string; - UpdatedAt?: string; - Spec?: ClusterVolumeSpec; - Info?: ClusterVolumeInfo; - /** The status of the volume as it pertains to its publishing and use on specific nodes */ - PublishStatus?: ClusterVolumePublishStatus[]; -} -/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ -export interface VolumeUsageData { - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - Size: I64; - /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ - RefCount: I64; -} -export interface Volume { - /** Name of the volume. */ - Name: string; - /** Name of the volume driver used by the volume. */ - Driver: string; - /** Mount path of the volume on the host. */ - Mountpoint: string; - /** Date/Time the volume was created. */ - CreatedAt?: string; - /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ - Status?: string[]; - /** User-defined key/value metadata. */ - Labels?: Record; - /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ - Scope?: VolumeScopeEnum; - ClusterVolume?: ClusterVolume; - /** The driver specific options used when creating the volume. */ - Options?: Record; - UsageData?: VolumeUsageData; -} -export type InspectDockerVolumeResponse = Volume; +export type InspectNetworkResponse = Network; export type InspectStackContainerResponse = Container; /** Swarm service list item. */ export interface SwarmServiceListItem { @@ -4873,6 +4740,139 @@ export interface SwarmTask { JobIteration?: ObjectVersion; } export type InspectSwarmTaskResponse = SwarmTask; +export declare enum VolumeScopeEnum { + Empty = "", + Local = "local", + Global = "global" +} +export declare enum ClusterVolumeSpecAccessModeScopeEnum { + Empty = "", + Single = "single", + Multi = "multi" +} +export declare enum ClusterVolumeSpecAccessModeSharingEnum { + Empty = "", + None = "none", + Readonly = "readonly", + Onewriter = "onewriter", + All = "all" +} +/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ +export interface ClusterVolumeSpecAccessModeSecrets { + /** Key is the name of the key of the key-value pair passed to the plugin. */ + Key?: string; + /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ + Secret?: string; +} +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} +/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ +export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { + /** A list of required topologies, at least one of which the volume must be accessible from. */ + Requisite?: Topology[]; + /** A list of topologies that the volume should attempt to be provisioned in. */ + Preferred?: Topology[]; +} +/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ +export interface ClusterVolumeSpecAccessModeCapacityRange { + /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ + RequiredBytes?: I64; + /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ + LimitBytes?: I64; +} +export declare enum ClusterVolumeSpecAccessModeAvailabilityEnum { + Empty = "", + Active = "active", + Pause = "pause", + Drain = "drain" +} +/** Defines how the volume is used by tasks. */ +export interface ClusterVolumeSpecAccessMode { + /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ + Scope?: ClusterVolumeSpecAccessModeScopeEnum; + /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ + Sharing?: ClusterVolumeSpecAccessModeSharingEnum; + /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ + Secrets?: ClusterVolumeSpecAccessModeSecrets[]; + AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; + CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; + /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ + Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; +} +/** Cluster-specific options used to create the volume. */ +export interface ClusterVolumeSpec { + /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ + Group?: string; + AccessMode?: ClusterVolumeSpecAccessMode; +} +/** Information about the global status of the volume. */ +export interface ClusterVolumeInfo { + /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ + CapacityBytes?: I64; + /** A map of strings to strings returned from the storage plugin when the volume is created. */ + VolumeContext?: Record; + /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ + VolumeID?: string; + /** The topology this volume is actually accessible from. */ + AccessibleTopology?: Topology[]; +} +export declare enum ClusterVolumePublishStatusStateEnum { + Empty = "", + PendingPublish = "pending-publish", + Published = "published", + PendingNodeUnpublish = "pending-node-unpublish", + PendingControllerUnpublish = "pending-controller-unpublish" +} +export interface ClusterVolumePublishStatus { + /** The ID of the Swarm node the volume is published on. */ + NodeID?: string; + /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ + State?: ClusterVolumePublishStatusStateEnum; + /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ + PublishContext?: Record; +} +/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ +export interface ClusterVolume { + /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ + ID?: string; + Version?: ObjectVersion; + CreatedAt?: string; + UpdatedAt?: string; + Spec?: ClusterVolumeSpec; + Info?: ClusterVolumeInfo; + /** The status of the volume as it pertains to its publishing and use on specific nodes */ + PublishStatus?: ClusterVolumePublishStatus[]; +} +/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ +export interface VolumeUsageData { + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + Size: I64; + /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ + RefCount: I64; +} +export interface Volume { + /** Name of the volume. */ + Name: string; + /** Name of the volume driver used by the volume. */ + Driver: string; + /** Mount path of the volume on the host. */ + Mountpoint: string; + /** Date/Time the volume was created. */ + CreatedAt?: string; + /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ + Status?: string[]; + /** User-defined key/value metadata. */ + Labels?: Record; + /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ + Scope?: VolumeScopeEnum; + ClusterVolume?: ClusterVolume; + /** The driver specific options used when creating the volume. */ + Options?: Record; + UsageData?: VolumeUsageData; +} +export type InspectVolumeResponse = Volume; export type JsonObject = any; export type ListActionsResponse = ActionListItem[]; export type ListAlertersResponse = AlerterListItem[]; @@ -4931,7 +4931,7 @@ export interface ContainerListItem { */ labels?: Record; } -export type ListAllDockerContainersResponse = ContainerListItem[]; +export type ListAllContainersResponse = ContainerListItem[]; /** * Combined state options for * both Server and Swarm based Stacks. @@ -5017,8 +5017,35 @@ export interface ComposeProject { compose_files: string[]; } export type ListComposeProjectsResponse = ComposeProject[]; +export type ListContainersResponse = ContainerListItem[]; export type ListDeploymentsResponse = DeploymentListItem[]; -export type ListDockerContainersResponse = ContainerListItem[]; +export type ListFullActionsResponse = Action[]; +export type ListFullAlertersResponse = Alerter[]; +export type ListFullBuildersResponse = Builder[]; +export type ListFullBuildsResponse = Build[]; +export type ListFullDeploymentsResponse = Deployment[]; +export type ListFullProceduresResponse = Procedure[]; +export type ListFullReposResponse = Repo[]; +export type ListFullResourceSyncsResponse = ResourceSync[]; +export type ListFullServersResponse = Server[]; +export type ListFullStacksResponse = Stack[]; +export type ListFullSwarmsResponse = Swarm[]; +export type ListGitProviderAccountsResponse = GitProviderAccount[]; +export interface ProviderAccount { + /** The account username. Required. */ + username: string; + /** The account access token. Required. */ + token?: string; +} +export interface GitProvider { + /** The git provider domain. Default: `github.com`. */ + domain: string; + /** Whether to use https. Default: true. */ + https: boolean; + /** The accounts on the git provider. Required. */ + accounts: ProviderAccount[]; +} +export type ListGitProvidersFromConfigResponse = GitProvider[]; /** individual image layer information in response to ImageHistory operation */ export interface ImageHistoryResponseItem { Id: string; @@ -5028,7 +5055,20 @@ export interface ImageHistoryResponseItem { Size: I64; Comment: string; } -export type ListDockerImageHistoryResponse = ImageHistoryResponseItem[]; +export type ListImageHistoryResponse = ImageHistoryResponseItem[]; +export interface ImageRegistry { + /** The image provider domain. Default: `docker.io`. */ + domain: string; + /** The accounts on the registry. Required. */ + accounts: ProviderAccount[]; + /** + * Available organizations on the registry provider. + * Used to push an image under an organization's repo rather than an account's repo. + */ + organizations?: string[]; +} +export type ListImageRegistriesFromConfigResponse = ImageRegistry[]; +export type ListImageRegistryAccountsResponse = ImageRegistryAccount[]; export interface ImageListItem { /** * ID is the content-addressable ID of an image. @@ -5051,81 +5091,28 @@ export interface ImageListItem { /** Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH). */ created: I64; /** Total size of the image including all layers it is composed of. */ - size: I64; - /** Whether the image is in use by any container */ - in_use: boolean; -} -export type ListDockerImagesResponse = ImageListItem[]; -export interface NetworkListItem { - name?: string; - id?: string; - created?: string; - scope?: string; - driver?: string; - enable_ipv6?: boolean; - ipam_driver?: string; - ipam_subnet?: string; - ipam_gateway?: string; - internal?: boolean; - attachable?: boolean; - ingress?: boolean; - /** Whether the network is attached to one or more containers */ - in_use: boolean; -} -export type ListDockerNetworksResponse = NetworkListItem[]; -export interface ProviderAccount { - /** The account username. Required. */ - username: string; - /** The account access token. Required. */ - token?: string; -} -export interface DockerRegistry { - /** The docker provider domain. Default: `docker.io`. */ - domain: string; - /** The accounts on the registry. Required. */ - accounts: ProviderAccount[]; - /** - * Available organizations on the registry provider. - * Used to push an image under an organization's repo rather than an account's repo. - */ - organizations?: string[]; + size: I64; + /** Whether the image is in use by any container */ + in_use: boolean; } -export type ListDockerRegistriesFromConfigResponse = DockerRegistry[]; -export type ListDockerRegistryAccountsResponse = DockerRegistryAccount[]; -export interface VolumeListItem { - /** The name of the volume */ - name: string; - driver: string; - mountpoint: string; +export type ListImagesResponse = ImageListItem[]; +export interface NetworkListItem { + name?: string; + id?: string; created?: string; - scope: VolumeScopeEnum; - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - size?: I64; - /** Whether the volume is currently attached to any container */ + scope?: string; + driver?: string; + enable_ipv6?: boolean; + ipam_driver?: string; + ipam_subnet?: string; + ipam_gateway?: string; + internal?: boolean; + attachable?: boolean; + ingress?: boolean; + /** Whether the network is attached to one or more containers */ in_use: boolean; } -export type ListDockerVolumesResponse = VolumeListItem[]; -export type ListFullActionsResponse = Action[]; -export type ListFullAlertersResponse = Alerter[]; -export type ListFullBuildersResponse = Builder[]; -export type ListFullBuildsResponse = Build[]; -export type ListFullDeploymentsResponse = Deployment[]; -export type ListFullProceduresResponse = Procedure[]; -export type ListFullReposResponse = Repo[]; -export type ListFullResourceSyncsResponse = ResourceSync[]; -export type ListFullServersResponse = Server[]; -export type ListFullStacksResponse = Stack[]; -export type ListFullSwarmsResponse = Swarm[]; -export type ListGitProviderAccountsResponse = GitProviderAccount[]; -export interface GitProvider { - /** The git provider domain. Default: `github.com`. */ - domain: string; - /** Whether to use https. Default: true. */ - https: boolean; - /** The accounts on the git provider. Required. */ - accounts: ProviderAccount[]; -} -export type ListGitProvidersFromConfigResponse = GitProvider[]; +export type ListNetworksResponse = NetworkListItem[]; export type ListOnboardingKeysResponse = OnboardingKey[]; export type UserTarget = /** User Id */ @@ -5604,6 +5591,19 @@ export type ListUserGroupsResponse = UserGroup[]; export type ListUserTargetPermissionsResponse = Permission[]; export type ListUsersResponse = User[]; export type ListVariablesResponse = Variable[]; +export interface VolumeListItem { + /** The name of the volume */ + name: string; + driver: string; + mountpoint: string; + created?: string; + scope: VolumeScopeEnum; + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + size?: I64; + /** Whether the volume is currently attached to any container */ + in_use: boolean; +} +export type ListVolumesResponse = VolumeListItem[]; export type MongoDocument = any; export interface ProcedureQuerySpecifics { } @@ -5651,8 +5651,8 @@ export interface SwarmQuerySpecifics { servers: string[]; } export type SwarmQuery = ResourceQuery; -export type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; export type UpdateGitProviderAccountResponse = GitProviderAccount; +export type UpdateImageRegistryAccountResponse = ImageRegistryAccount; export type UpdateOnboardingKeyResponse = OnboardingKey; export type UpdatePermissionOnResourceTypeResponse = NoData; export type UpdatePermissionOnTargetResponse = NoData; @@ -5670,8 +5670,8 @@ export type _PartialAwsBuilderConfig = Partial; export type _PartialBuildConfig = Partial; export type _PartialBuilderConfig = Partial; export type _PartialDeploymentConfig = Partial; -export type _PartialDockerRegistryAccount = Partial; export type _PartialGitProviderAccount = Partial; +export type _PartialImageRegistryAccount = Partial; export type _PartialProcedureConfig = Partial; export type _PartialRepoConfig = Partial; export type _PartialResourceSyncConfig = Partial; @@ -5738,8 +5738,12 @@ export interface AwsBuilderConfig { insecure_tls: boolean; /** Which git providers are available on the AMI */ git_providers?: GitProvider[]; - /** Which docker registries are available on the AMI. */ - docker_registries?: DockerRegistry[]; + /** + * Which image registries are available on the AMI. + * + * Pre v2.3.0, called `docker_registries` + */ + image_registries?: ImageRegistry[]; /** Which secrets are available on the AMI. */ secrets?: string[]; } @@ -6604,13 +6608,6 @@ export interface CreateDeploymentFromContainer { /** The server id or name on which container exists. */ server: string; } -/** - * **Admin only.** Create a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface CreateDockerRegistryAccount { - account: _PartialDockerRegistryAccount; -} /** * **Admin only.** Create a git provider account. * Response: [GitProviderAccount]. @@ -6622,6 +6619,15 @@ export interface CreateGitProviderAccount { */ account: _PartialGitProviderAccount; } +/** + * **Admin only.** Create an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `CreateDockerRegistryAccount` + */ +export interface CreateImageRegistryAccount { + account: _PartialImageRegistryAccount; +} /** * **Admin only.** Create a local user. * Response: [User]. @@ -6901,14 +6907,6 @@ export interface DeleteDeployment { /** The id or name of the deployment to delete. */ id: string; } -/** - * **Admin only.** Delete a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface DeleteDockerRegistryAccount { - /** The id of the docker registry account to delete */ - id: string; -} /** * **Admin only.** Delete a git provider account. * Response: [DeleteGitProviderAccountResponse]. @@ -6927,6 +6925,16 @@ export interface DeleteImage { /** The name of the image to delete. */ name: string; } +/** + * **Admin only.** Delete an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `DeleteDockerRegistryAccount` + */ +export interface DeleteImageRegistryAccount { + /** The id of the image registry account to delete */ + id: string; +} /** * Delete a docker network. * Response: [Update] @@ -7497,6 +7505,27 @@ export interface GetContainerLog { /** Enable `--timestamps` */ timestamps?: boolean; } +/** + * Gets a summary of data relating to all containers. + * Response: [GetContainersSummaryResponse]. + * + * Pre v2.3.0, called `GetDockerContainersSummary` + */ +export interface GetContainersSummary { +} +/** Response for [GetContainersSummary] */ +export interface GetContainersSummaryResponse { + /** The total number of Containers */ + total: number; + /** The number of Containers with Running state */ + running: number; + /** The number of Containers with Stopped or Paused or Created state */ + stopped: number; + /** The number of Containers with Restarting or Dead state */ + unhealthy: number; + /** The number of Containers with Unknown state */ + unknown: number; +} /** * Get information about the Komodo Core API configuration. * Response: [GetCoreInfoResponse]. @@ -7607,32 +7636,6 @@ export interface GetDeploymentsSummaryResponse { /** The number of Deployments with Unknown state */ unknown: I64; } -/** - * Gets a summary of data relating to all containers. - * Response: [GetDockerContainersSummaryResponse]. - */ -export interface GetDockerContainersSummary { -} -/** Response for [GetDockerContainersSummary] */ -export interface GetDockerContainersSummaryResponse { - /** The total number of Containers */ - total: number; - /** The number of Containers with Running state */ - running: number; - /** The number of Containers with Stopped or Paused or Created state */ - stopped: number; - /** The number of Containers with Restarting or Dead state */ - unhealthy: number; - /** The number of Containers with Unknown state */ - unknown: number; -} -/** - * Get a specific docker registry account. - * Response: [GetDockerRegistryAccountResponse]. - */ -export interface GetDockerRegistryAccount { - id: string; -} /** * Get a specific git provider account. * Response: [GetGitProviderAccountResponse]. @@ -7707,6 +7710,15 @@ export interface GetHistoricalServerStatsResponse { /** If there is a next page of data, pass this to `page` to get it. */ next_page?: number; } +/** + * Get a specific image registry account. + * Response: [GetImageRegistryAccountResponse]. + * + * Pre v2.3.0, called `GetDockerRegistryAccount` + */ +export interface GetImageRegistryAccount { + id: string; +} /** * Get the Periphery information of the target server, * including the Periphery version and public key. @@ -8070,6 +8082,17 @@ export interface GlobalAutoUpdate { */ skip_auto_update?: boolean; } +/** + * Inspect a container on the server. Response: [Container]. + * + * Pre v2.3.0, called `InspectDockerContainer` + */ +export interface InspectContainer { + /** Id or name */ + server: string; + /** The container name */ + container: string; +} /** * Inspect the docker container associated with the Deployment. * Response: [Container]. @@ -8086,34 +8109,28 @@ export interface InspectDeploymentSwarmService { /** Id or name */ deployment: string; } -/** Inspect a docker container on the server. Response: [Container]. */ -export interface InspectDockerContainer { - /** Id or name */ - server: string; - /** The container name */ - container: string; -} -/** Inspect a docker image on the server. Response: [Image]. */ -export interface InspectDockerImage { +/** + * Inspect a container image on the server. Response: [Image]. + * + * Pre v2.3.0, called `InspectDockerImage` + */ +export interface InspectImage { /** Id or name */ server: string; /** The image name */ image: string; } -/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */ -export interface InspectDockerNetwork { +/** + * Inspect a container network on the server. Response: [InspectNetworkResponse]. + * + * Pre v2.3.0, called `InspectDockerNetwork` + */ +export interface InspectNetwork { /** Id or name */ server: string; /** The network name */ network: string; } -/** Inspect a docker volume on the server. Response: [Volume]. */ -export interface InspectDockerVolume { - /** Id or name */ - server: string; - /** The volume name */ - volume: string; -} /** * Inspect a docker container associated with a Stack. * Response: [Container]. @@ -8210,6 +8227,17 @@ export interface InspectSwarmTask { /** Task id */ task: string; } +/** + * Inspect a container volume on the server. Response: [Volume]. + * + * Pre v2.3.0, called `InspectDockerVolume` + */ +export interface InspectVolume { + /** Id or name */ + server: string; + /** The volume name */ + volume: string; +} export interface LatestCommit { hash: string; message: string; @@ -8272,10 +8300,12 @@ export interface ListAlertsResponse { next_page?: I64; } /** - * List all docker containers on the target servers. - * Response: [ListDockerContainersResponse]. + * List all containers on the target servers. + * Response: [ListAllContainersResponse]. + * + * Pre v2.3.0, called `ListAllDockerContainers` */ -export interface ListAllDockerContainers { +export interface ListAllContainers { /** Filter by server id or name. */ servers?: string[]; /** Filter servers by tag. */ @@ -8406,7 +8436,7 @@ export interface ListCommonStackExtraArgs { query?: StackQuery; } /** - * List all docker compose projects on the target server. + * List all compose projects on the target server. * Response: [ListComposeProjectsResponse]. */ export interface ListComposeProjects { @@ -8414,74 +8444,22 @@ export interface ListComposeProjects { server: string; } /** - * List deployments matching optional query. - * Response: [ListDeploymentsResponse]. - */ -export interface ListDeployments { - /** optional structured query to filter deployments. */ - query?: DeploymentQuery; -} -/** - * List all docker containers on the target server. - * Response: [ListDockerContainersResponse]. - */ -export interface ListDockerContainers { - /** Id or name */ - server: string; -} -/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */ -export interface ListDockerImageHistory { - /** Id or name */ - server: string; - /** The image name */ - image: string; -} -/** - * List the docker images locally cached on the target server. - * Response: [ListDockerImagesResponse]. + * List all containers on the target server. + * Response: [ListContainersResponse]. + * + * Pre v2.3.0, called `ListDockerContainers` */ -export interface ListDockerImages { - /** Id or name */ - server: string; -} -/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */ -export interface ListDockerNetworks { +export interface ListContainers { /** Id or name */ server: string; } /** - * List the docker registry providers available in Core / Periphery config files. - * Response: [ListDockerRegistriesFromConfigResponse]. - * - * Includes: - * - registries in core config - * - registries configured on builds, deployments - * - registries on the optional Server or Builder - */ -export interface ListDockerRegistriesFromConfig { - /** - * Accepts an optional Server or Builder target to expand the core list with - * providers available on that specific resource. - */ - target?: ResourceTarget; -} -/** - * List docker registry accounts matching optional query. - * Response: [ListDockerRegistryAccountsResponse]. - */ -export interface ListDockerRegistryAccounts { - /** Optionally filter by accounts with a specific domain. */ - domain?: string; - /** Optionally filter by accounts with a specific username. */ - username?: string; -} -/** - * List all docker volumes on the target server. - * Response: [ListDockerVolumesResponse]. + * List deployments matching optional query. + * Response: [ListDeploymentsResponse]. */ -export interface ListDockerVolumes { - /** Id or name */ - server: string; +export interface ListDeployments { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { @@ -8566,6 +8544,66 @@ export interface ListGitProvidersFromConfig { */ target?: ResourceTarget; } +/** + * Get image history from the server. Response: [ListImageHistoryResponse]. + * + * Pre v2.3.0, called `ListDockerImageHistory` + */ +export interface ListImageHistory { + /** Id or name */ + server: string; + /** The image name */ + image: string; +} +/** + * List the image registry providers available in Core / Periphery config files. + * Response: [ListImageRegistriesFromConfigResponse]. + * + * Includes: + * - registries in core config + * - registries configured on builds, deployments + * - registries on the optional Server or Builder + * + * Pre v2.3.0, called `ListDockerRegistriesFromConfig` + */ +export interface ListImageRegistriesFromConfig { + /** + * Accepts an optional Server or Builder target to expand the core list with + * providers available on that specific resource. + */ + target?: ResourceTarget; +} +/** + * List image registry accounts matching optional query. + * Response: [ListImageRegistryAccountsResponse]. + * + * Pre v2.3.0, called `ListDockerRegistryAccounts` + */ +export interface ListImageRegistryAccounts { + /** Optionally filter by accounts with a specific domain. */ + domain?: string; + /** Optionally filter by accounts with a specific username. */ + username?: string; +} +/** + * List the container images locally cached on the target server. + * Response: [ListImagesResponse]. + * + * Pre v2.3.0, called `ListDockerImages` + */ +export interface ListImages { + /** Id or name */ + server: string; +} +/** + * List the container networks on the server. Response: [ListNetworksResponse]. + * + * Pre v2.3.0, called `ListDockerNetworks` + */ +export interface ListNetworks { + /** Id or name */ + server: string; +} /** * **Admin only.** Gets list of onboarding keys. * Response: [ListOnboardingKeysResponse] @@ -8824,6 +8862,16 @@ export interface ListUsers { */ export interface ListVariables { } +/** + * List all container volumes on the target server. + * Response: [ListVolumesResponse]. + * + * Pre v2.3.0, called `ListDockerVolumes` + */ +export interface ListVolumes { + /** Id or name */ + server: string; +} export interface NameAndId { name: string; id: string; @@ -9874,16 +9922,6 @@ export interface UpdateDeployment { /** The partial config update. */ config: _PartialDeploymentConfig; } -/** - * **Admin only.** Update a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface UpdateDockerRegistryAccount { - /** The id of the docker registry to update */ - id: string; - /** The partial docker registry account. */ - account: _PartialDockerRegistryAccount; -} /** * **Admin only.** Update a git provider account. * Response: [GitProviderAccount]. @@ -9894,6 +9932,18 @@ export interface UpdateGitProviderAccount { /** The partial git provider account. */ account: _PartialGitProviderAccount; } +/** + * **Admin only.** Update a image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `UpdateDockerRegistryAccount` + */ +export interface UpdateImageRegistryAccount { + /** The id of the image registry to update */ + id: string; + /** The partial image registry account. */ + account: _PartialImageRegistryAccount; +} /** * **Admin only.** Update an onboarding key. * Response: The updated [OnboardingKey]. @@ -10566,8 +10616,8 @@ export type ReadRequest = { type: "ListGitProvidersFromConfig"; params: ListGitProvidersFromConfig; } | { - type: "ListDockerRegistriesFromConfig"; - params: ListDockerRegistriesFromConfig; + type: "ListImageRegistriesFromConfig"; + params: ListImageRegistriesFromConfig; } | { type: "GetSwarmsSummary"; params: GetSwarmsSummary; @@ -10656,17 +10706,17 @@ export type ReadRequest = { type: "ListTerminals"; params: ListTerminals; } | { - type: "GetDockerContainersSummary"; - params: GetDockerContainersSummary; + type: "GetContainersSummary"; + params: GetContainersSummary; } | { - type: "ListAllDockerContainers"; - params: ListAllDockerContainers; + type: "ListAllContainers"; + params: ListAllContainers; } | { - type: "ListDockerContainers"; - params: ListDockerContainers; + type: "ListContainers"; + params: ListContainers; } | { - type: "InspectDockerContainer"; - params: InspectDockerContainer; + type: "InspectContainer"; + params: InspectContainer; } | { type: "GetResourceMatchingContainer"; params: GetResourceMatchingContainer; @@ -10680,26 +10730,26 @@ export type ReadRequest = { type: "ListComposeProjects"; params: ListComposeProjects; } | { - type: "ListDockerNetworks"; - params: ListDockerNetworks; + type: "ListNetworks"; + params: ListNetworks; } | { - type: "InspectDockerNetwork"; - params: InspectDockerNetwork; + type: "InspectNetwork"; + params: InspectNetwork; } | { - type: "ListDockerImages"; - params: ListDockerImages; + type: "ListImages"; + params: ListImages; } | { - type: "InspectDockerImage"; - params: InspectDockerImage; + type: "InspectImage"; + params: InspectImage; } | { - type: "ListDockerImageHistory"; - params: ListDockerImageHistory; + type: "ListImageHistory"; + params: ListImageHistory; } | { - type: "ListDockerVolumes"; - params: ListDockerVolumes; + type: "ListVolumes"; + params: ListVolumes; } | { - type: "InspectDockerVolume"; - params: InspectDockerVolume; + type: "InspectVolume"; + params: InspectVolume; } | { type: "GetSystemInformation"; params: GetSystemInformation; @@ -10965,11 +11015,11 @@ export type ReadRequest = { type: "ListGitProviderAccounts"; params: ListGitProviderAccounts; } | { - type: "GetDockerRegistryAccount"; - params: GetDockerRegistryAccount; + type: "GetImageRegistryAccount"; + params: GetImageRegistryAccount; } | { - type: "ListDockerRegistryAccounts"; - params: ListDockerRegistryAccounts; + type: "ListImageRegistryAccounts"; + params: ListImageRegistryAccounts; } | { type: "ListOnboardingKeys"; params: ListOnboardingKeys; @@ -11356,14 +11406,14 @@ export type WriteRequest = { type: "DeleteGitProviderAccount"; params: DeleteGitProviderAccount; } | { - type: "CreateDockerRegistryAccount"; - params: CreateDockerRegistryAccount; + type: "CreateImageRegistryAccount"; + params: CreateImageRegistryAccount; } | { - type: "UpdateDockerRegistryAccount"; - params: UpdateDockerRegistryAccount; + type: "UpdateImageRegistryAccount"; + params: UpdateImageRegistryAccount; } | { - type: "DeleteDockerRegistryAccount"; - params: DeleteDockerRegistryAccount; + type: "DeleteImageRegistryAccount"; + params: DeleteImageRegistryAccount; } | { type: "CloseAlert"; params: CloseAlert; diff --git a/ui/public/client/types.js b/ui/public/client/types.js index 70868097ce..5328a6d206 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -600,41 +600,6 @@ export var ImageManifestSummaryKindEnum; ImageManifestSummaryKindEnum["Attestation"] = "attestation"; ImageManifestSummaryKindEnum["Unknown"] = "unknown"; })(ImageManifestSummaryKindEnum || (ImageManifestSummaryKindEnum = {})); -export var VolumeScopeEnum; -(function (VolumeScopeEnum) { - VolumeScopeEnum["Empty"] = ""; - VolumeScopeEnum["Local"] = "local"; - VolumeScopeEnum["Global"] = "global"; -})(VolumeScopeEnum || (VolumeScopeEnum = {})); -export var ClusterVolumeSpecAccessModeScopeEnum; -(function (ClusterVolumeSpecAccessModeScopeEnum) { - ClusterVolumeSpecAccessModeScopeEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeScopeEnum["Single"] = "single"; - ClusterVolumeSpecAccessModeScopeEnum["Multi"] = "multi"; -})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {})); -export var ClusterVolumeSpecAccessModeSharingEnum; -(function (ClusterVolumeSpecAccessModeSharingEnum) { - ClusterVolumeSpecAccessModeSharingEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeSharingEnum["None"] = "none"; - ClusterVolumeSpecAccessModeSharingEnum["Readonly"] = "readonly"; - ClusterVolumeSpecAccessModeSharingEnum["Onewriter"] = "onewriter"; - ClusterVolumeSpecAccessModeSharingEnum["All"] = "all"; -})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {})); -export var ClusterVolumeSpecAccessModeAvailabilityEnum; -(function (ClusterVolumeSpecAccessModeAvailabilityEnum) { - ClusterVolumeSpecAccessModeAvailabilityEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeAvailabilityEnum["Active"] = "active"; - ClusterVolumeSpecAccessModeAvailabilityEnum["Pause"] = "pause"; - ClusterVolumeSpecAccessModeAvailabilityEnum["Drain"] = "drain"; -})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {})); -export var ClusterVolumePublishStatusStateEnum; -(function (ClusterVolumePublishStatusStateEnum) { - ClusterVolumePublishStatusStateEnum["Empty"] = ""; - ClusterVolumePublishStatusStateEnum["PendingPublish"] = "pending-publish"; - ClusterVolumePublishStatusStateEnum["Published"] = "published"; - ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish"; - ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish"; -})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {})); export var TaskState; (function (TaskState) { TaskState["NEW"] = "new"; @@ -686,6 +651,41 @@ export var SwarmSpecCaConfigExternalCasProtocolEnum; SwarmSpecCaConfigExternalCasProtocolEnum["EMPTY"] = ""; SwarmSpecCaConfigExternalCasProtocolEnum["CFSSL"] = "cfssl"; })(SwarmSpecCaConfigExternalCasProtocolEnum || (SwarmSpecCaConfigExternalCasProtocolEnum = {})); +export var VolumeScopeEnum; +(function (VolumeScopeEnum) { + VolumeScopeEnum["Empty"] = ""; + VolumeScopeEnum["Local"] = "local"; + VolumeScopeEnum["Global"] = "global"; +})(VolumeScopeEnum || (VolumeScopeEnum = {})); +export var ClusterVolumeSpecAccessModeScopeEnum; +(function (ClusterVolumeSpecAccessModeScopeEnum) { + ClusterVolumeSpecAccessModeScopeEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeScopeEnum["Single"] = "single"; + ClusterVolumeSpecAccessModeScopeEnum["Multi"] = "multi"; +})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {})); +export var ClusterVolumeSpecAccessModeSharingEnum; +(function (ClusterVolumeSpecAccessModeSharingEnum) { + ClusterVolumeSpecAccessModeSharingEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeSharingEnum["None"] = "none"; + ClusterVolumeSpecAccessModeSharingEnum["Readonly"] = "readonly"; + ClusterVolumeSpecAccessModeSharingEnum["Onewriter"] = "onewriter"; + ClusterVolumeSpecAccessModeSharingEnum["All"] = "all"; +})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {})); +export var ClusterVolumeSpecAccessModeAvailabilityEnum; +(function (ClusterVolumeSpecAccessModeAvailabilityEnum) { + ClusterVolumeSpecAccessModeAvailabilityEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeAvailabilityEnum["Active"] = "active"; + ClusterVolumeSpecAccessModeAvailabilityEnum["Pause"] = "pause"; + ClusterVolumeSpecAccessModeAvailabilityEnum["Drain"] = "drain"; +})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {})); +export var ClusterVolumePublishStatusStateEnum; +(function (ClusterVolumePublishStatusStateEnum) { + ClusterVolumePublishStatusStateEnum["Empty"] = ""; + ClusterVolumePublishStatusStateEnum["PendingPublish"] = "pending-publish"; + ClusterVolumePublishStatusStateEnum["Published"] = "published"; + ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish"; + ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish"; +})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {})); export var PortTypeEnum; (function (PortTypeEnum) { PortTypeEnum["EMPTY"] = ""; diff --git a/ui/public/schema/resources.json b/ui/public/schema/resources.json index b53179e1e1..81a6d006c6 100644 --- a/ui/public/schema/resources.json +++ b/ui/public/schema/resources.json @@ -2547,6 +2547,22 @@ "params" ] }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "CancelProcedure" + }, + "params": { + "$ref": "#/definitions/CancelProcedure" + } + }, + "required": [ + "type", + "params" + ] + }, { "description": "Run the target action. (alias: `action`, `ac`)", "type": "object", @@ -2580,6 +2596,22 @@ "params" ] }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "CancelAction" + }, + "params": { + "$ref": "#/definitions/CancelAction" + } + }, + "required": [ + "type", + "params" + ] + }, { "description": "Execute a Resource Sync. (alias: `sync`)", "type": "object", @@ -3951,6 +3983,26 @@ "pattern" ] }, + "CancelProcedure": { + "description": "Cancels the target procedure run. Response: [Update]", + "type": "object", + "properties": { + "procedure": { + "description": "Id or name", + "type": "string" + }, + "update_id": { + "description": "The update id associated with the specific\nrun to cancel\nMust provide either `action`\nor `update_id`", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "procedure" + ] + }, "RunAction": { "description": "Runs the target Action. Response: [Update]", "type": "object", @@ -3985,6 +4037,26 @@ "pattern" ] }, + "CancelAction": { + "description": "Cancels the target action run. Response: [Update]", + "type": "object", + "properties": { + "action": { + "description": "Id or name", + "type": "string" + }, + "update_id": { + "description": "The update id associated with the specific\nrun to cancel\nMust provide either `action`\nor `update_id`", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "action" + ] + }, "RunSync": { "description": "Runs the target resource sync. Response: [Update]", "type": "object", @@ -5840,14 +5912,14 @@ "$ref": "#/definitions/GitProvider" } }, - "docker_registries": { - "description": "Which docker registries are available on the AMI.", + "image_registries": { + "description": "Which image registries are available on the AMI.\n\nPre v2.3.0, called `docker_registries`", "type": [ "array", "null" ], "items": { - "$ref": "#/definitions/DockerRegistry" + "$ref": "#/definitions/ImageRegistry" } }, "secrets": { @@ -5904,11 +5976,11 @@ "username" ] }, - "DockerRegistry": { + "ImageRegistry": { "type": "object", "properties": { "domain": { - "description": "The docker provider domain. Default: `docker.io`.", + "description": "The image provider domain. Default: `docker.io`.", "type": "string", "default": "docker.io" }, diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 2d8eb4c366..c1aeeb214d 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -51,7 +51,7 @@ export function useOmniSearch(): { const debouncedTerms = useDebounce(searchTerms, 700); - const containersQuery: Types.ListAllDockerContainers = useMemo( + const containersQuery: Types.ListAllContainers = useMemo( () => ({ terms: debouncedTerms // Allows search like 'cont my name' to return containers matching 'my name' @@ -62,7 +62,7 @@ export function useOmniSearch(): { [debouncedTerms], ); - const containers = useRead("ListAllDockerContainers", containersQuery, { + const containers = useRead("ListAllContainers", containersQuery, { refetchInterval: 15_000, // Only fetch when there is query typed enabled: !!debouncedTerms.length, diff --git a/ui/src/components/config/account-selector.tsx b/ui/src/components/config/account-selector.tsx index cdf74096fe..58c9339aaf 100644 --- a/ui/src/components/config/account-selector.tsx +++ b/ui/src/components/config/account-selector.tsx @@ -28,10 +28,10 @@ export default function AccountSelector({ }: AccountSelectorProps) { const [dbRequest, configRequest]: | ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - | ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] = + | ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"] = accountType === "git" ? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - : ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"]; + : ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"]; const dbAccounts = useRead(dbRequest, {}).data?.filter( (account) => account.domain === provider, ); diff --git a/ui/src/components/config/image-registry-config.tsx b/ui/src/components/config/image-registry-config.tsx index 4451735b26..63dcc43fdb 100644 --- a/ui/src/components/config/image-registry-config.tsx +++ b/ui/src/components/config/image-registry-config.tsx @@ -25,7 +25,7 @@ export default function ImageRegistryConfig({ imageName, }: ImageRegistryConfig) { // This is the only way to get organizations for now - const config_provider = useRead("ListDockerRegistriesFromConfig", { + const config_provider = useRead("ListImageRegistriesFromConfig", { target: builderId ? { type: "Builder", id: builderId } : undefined, }).data?.find((provider) => { return provider.domain === registry?.domain; diff --git a/ui/src/components/config/provider-selector.tsx b/ui/src/components/config/provider-selector.tsx index f35949029e..864ea990c4 100644 --- a/ui/src/components/config/provider-selector.tsx +++ b/ui/src/components/config/provider-selector.tsx @@ -25,10 +25,10 @@ export default function ProviderSelector({ }: ProviderSelectorProps) { const [dbRequest, configRequest]: | ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - | ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] = + | ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"] = accountType === "git" ? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - : ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"]; + : ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"]; const dbProviders = useRead(dbRequest, {}).data; const configProviders = useRead(configRequest, {}).data; const [customMode, setCustomMode] = useState(false); diff --git a/ui/src/components/docker/container-selector.tsx b/ui/src/components/docker/container-selector.tsx index 812e14a50a..387d58b15b 100644 --- a/ui/src/components/docker/container-selector.tsx +++ b/ui/src/components/docker/container-selector.tsx @@ -40,7 +40,7 @@ export default function ContainerSelector({ clearable, ...comboboxProps }: ContainerSelectorProps) { - const containers = useRead("ListDockerContainers", { + const containers = useRead("ListContainers", { server: serverId, }).data?.filter((container) => !state || container.state === state); diff --git a/ui/src/components/docker/containers-section.tsx b/ui/src/components/docker/containers-section.tsx index f63d084c01..1d63f497a6 100644 --- a/ui/src/components/docker/containers-section.tsx +++ b/ui/src/components/docker/containers-section.tsx @@ -16,7 +16,7 @@ import { DividedChildren } from "mogh_ui"; export interface ContainersSectionProps extends SectionProps { serverId: string; - containers: Types.ListDockerContainersResponse; + containers: Types.ListContainersResponse; show?: boolean; setShow?: (show: boolean) => void; pruneButton?: boolean; @@ -35,7 +35,7 @@ export default function ContainersSection({ titleOther, ...sectionProps }: ContainersSectionProps) { - const allRunning = useRead("ListDockerContainers", { + const allRunning = useRead("ListContainers", { server: serverId, }).data?.every( (container) => container.state === Types.ContainerStateStatusEnum.Running, diff --git a/ui/src/components/docker/link.tsx b/ui/src/components/docker/link.tsx index 0b118392ce..21d74616dc 100644 --- a/ui/src/components/docker/link.tsx +++ b/ui/src/components/docker/link.tsx @@ -62,7 +62,7 @@ export const DOCKER_LINK_ICONS: { } = { Container: ({ serverId, name, size = "1rem" }) => { const state = - useRead("ListDockerContainers", { server: serverId }).data?.find( + useRead("ListContainers", { server: serverId }).data?.find( (container) => container.name === name, )?.state ?? Types.ContainerStateStatusEnum.Empty; return ( @@ -74,7 +74,7 @@ export const DOCKER_LINK_ICONS: { }, Network: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => !container.networks?.includes(name)); @@ -89,7 +89,7 @@ export const DOCKER_LINK_ICONS: { }, Image: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => container.image_id !== name); @@ -98,7 +98,7 @@ export const DOCKER_LINK_ICONS: { }, Volume: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => !container.volumes?.includes(name)); diff --git a/ui/src/lib/socket.tsx b/ui/src/lib/socket.tsx index 3e9769093d..2d4ba1bb56 100644 --- a/ui/src/lib/socket.tsx +++ b/ui/src/lib/socket.tsx @@ -200,14 +200,14 @@ function onUpdate( // Invalidate docker infos if (["Server", "Deployment", "Stack"].includes(update.target.type)) { invalidate( - ["ListDockerContainers"], - ["InspectDockerContainer"], - ["ListDockerNetworks"], - ["InspectDockerNetwork"], - ["ListDockerImages"], - ["InspectDockerImage"], - ["ListDockerVolumes"], - ["InspectDockerVolume"], + ["ListContainers"], + ["InspectContainer"], + ["ListNetworks"], + ["InspectNetwork"], + ["ListImages"], + ["InspectImage"], + ["ListVolumes"], + ["InspectVolume"], ["GetResourceMatchingContainer"], ); } @@ -251,9 +251,9 @@ function onUpdate( ["GetStacksSummary"], ["ListCommonStackExtraArgs"], ["ListComposeProjects"], - ["ListDockerContainers"], - ["ListDockerNetworks"], - ["ListDockerImages"], + ["ListContainers"], + ["ListNetworks"], + ["ListImages"], ["GetStackLog", { stack: update.target.id }], ["SearchStackLog", { stack: update.target.id }], ["GetStack"], @@ -266,9 +266,9 @@ function onUpdate( invalidate( ["ListDeployments"], ["GetDeploymentsSummary"], - ["ListDockerContainers"], - ["ListDockerNetworks"], - ["ListDockerImages"], + ["ListContainers"], + ["ListNetworks"], + ["ListImages"], ["GetDeployment"], ["GetDeploymentLog", { deployment: update.target.id }], ["SearchDeploymentLog", { deployment: update.target.id }], diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 705e5bfbe4..cf8574aa13 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -38,7 +38,7 @@ export default function Containers() { const tags = useTagsFilter(); const containers = - useRead("ListAllDockerContainers", { + useRead("ListAllContainers", { terms: searchTerms, servers: selectedServers, tags, diff --git a/ui/src/pages/docker/container/executions.tsx b/ui/src/pages/docker/container/executions.tsx index f30159c33a..5d85d4c37e 100644 --- a/ui/src/pages/docker/container/executions.tsx +++ b/ui/src/pages/docker/container/executions.tsx @@ -6,7 +6,7 @@ import { Pause, Play, RefreshCcw, Square, Trash } from "lucide-react"; import { useNavigate } from "react-router-dom"; const useContainer = (serverId: string, containerName: string) => { - return useRead("ListDockerContainers", { server: serverId }).data?.find( + return useRead("ListContainers", { server: serverId }).data?.find( (container) => container.name === containerName, ); }; diff --git a/ui/src/pages/docker/container/index.tsx b/ui/src/pages/docker/container/index.tsx index 329bf37582..dc1f05bc58 100644 --- a/ui/src/pages/docker/container/index.tsx +++ b/ui/src/pages/docker/container/index.tsx @@ -55,14 +55,14 @@ function ContainerInner({ id: serverId, }); const listContainer = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, { refetchInterval: 10_000 }, ).data?.find((container) => container.name === containerName); const inspect = useRead( - "InspectDockerContainer", + "InspectContainer", { server: serverId, container: containerName, diff --git a/ui/src/pages/docker/image.tsx b/ui/src/pages/docker/image.tsx index 43dc0c21f0..fecb1af702 100644 --- a/ui/src/pages/docker/image.tsx +++ b/ui/src/pages/docker/image.tsx @@ -52,13 +52,13 @@ function ImageInner({ data: image, isPending, isError, - } = useRead("InspectDockerImage", { + } = useRead("InspectImage", { server: serverId, image: imageName, }); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, @@ -67,7 +67,7 @@ function ImageInner({ !image?.Id ? false : container.image_id === image?.Id, ); - const history = useRead("ListDockerImageHistory", { + const history = useRead("ListImageHistory", { server: serverId, image: imageName, }).data; diff --git a/ui/src/pages/docker/network.tsx b/ui/src/pages/docker/network.tsx index 6bf91a3475..2c5e2b732b 100644 --- a/ui/src/pages/docker/network.tsx +++ b/ui/src/pages/docker/network.tsx @@ -50,7 +50,7 @@ function NetworkInner({ data: network, isPending, isError, - } = useRead("InspectDockerNetwork", { + } = useRead("InspectNetwork", { server: serverId, network: networkName, }); diff --git a/ui/src/pages/docker/volume.tsx b/ui/src/pages/docker/volume.tsx index 12b65496fc..9863cab225 100644 --- a/ui/src/pages/docker/volume.tsx +++ b/ui/src/pages/docker/volume.tsx @@ -49,7 +49,7 @@ function VolumeInner({ data: volume, isPending, isError, - } = useRead("InspectDockerVolume", { + } = useRead("InspectVolume", { server: serverId, volume: volumeName, }); @@ -62,7 +62,7 @@ function VolumeInner({ ); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, diff --git a/ui/src/pages/settings/providers/delete.tsx b/ui/src/pages/settings/providers/delete.tsx index aadab9aa85..0e7b4f6ae2 100644 --- a/ui/src/pages/settings/providers/delete.tsx +++ b/ui/src/pages/settings/providers/delete.tsx @@ -7,7 +7,7 @@ export default function DeleteProviderAccount({ type, id, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; id: string; }) { const invalidate = useInvalidate(); diff --git a/ui/src/pages/settings/providers/from-config.tsx b/ui/src/pages/settings/providers/from-config.tsx index 0b95ba244c..55a4fd26d5 100644 --- a/ui/src/pages/settings/providers/from-config.tsx +++ b/ui/src/pages/settings/providers/from-config.tsx @@ -4,12 +4,12 @@ import { Badge, Center, Group, Loader, Stack, Text } from "@mantine/core"; export default function ProvidersFromConfig({ type, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; }) { const accounts = useRead( type === "GitProvider" ? "ListGitProvidersFromConfig" - : "ListDockerRegistriesFromConfig", + : "ListImageRegistriesFromConfig", {}, ) .data?.map((provider) => diff --git a/ui/src/pages/settings/providers/index.tsx b/ui/src/pages/settings/providers/index.tsx index ec28d1f60d..f9ab70ccf1 100644 --- a/ui/src/pages/settings/providers/index.tsx +++ b/ui/src/pages/settings/providers/index.tsx @@ -23,12 +23,12 @@ export default function SettingsProviders() { return ( - + ); } -function Providers({ type }: { type: "GitProvider" | "DockerRegistry" }) { +function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) { const user = useUser().data; const disabled = !user?.admin; useSetTitle("Providers"); @@ -62,9 +62,9 @@ function Providers({ type }: { type: "GitProvider" | "DockerRegistry" }) { return ( <>
) : ( diff --git a/ui/src/pages/settings/providers/new.tsx b/ui/src/pages/settings/providers/new.tsx index d1ee096199..92b0584906 100644 --- a/ui/src/pages/settings/providers/new.tsx +++ b/ui/src/pages/settings/providers/new.tsx @@ -17,7 +17,7 @@ import { ChangeEvent, useState } from "react"; export default function NewProviderAccount({ type, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; }) { const [opened, { open, close }] = useDisclosure(); const [domain, setDomain] = useState(""); @@ -62,7 +62,7 @@ export default function NewProviderAccount({ ], ]; const accountType = - type === "DockerRegistry" ? "Registry Account" : "Git Account"; + type === "ImageRegistry" ? "Registry Account" : "Git Account"; return ( <> diff --git a/ui/src/resources/deployment/config/network.tsx b/ui/src/resources/deployment/config/network.tsx index e7aed6e785..db5d40a7d2 100644 --- a/ui/src/resources/deployment/config/network.tsx +++ b/ui/src/resources/deployment/config/network.tsx @@ -20,7 +20,7 @@ export default function DeploymentNetworkSelector({ }: DeploymentNetworkSelectorProps) { const _networks = useRead( - swarmId ? "ListSwarmNetworks" : "ListDockerNetworks", + swarmId ? "ListSwarmNetworks" : "ListNetworks", { swarm: swarmId, server: serverId! }, { enabled: !!swarmId || !!serverId }, ) diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index a0c7dedc4e..cf5f034680 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -247,7 +247,7 @@ export const DeploymentComponents: RequiredResourceComponents< Ports: ({ id }) => { const deployment = useDeployment(id); const container = useRead( - "ListDockerContainers", + "ListContainers", { server: deployment?.info.server_id!, }, diff --git a/ui/src/resources/server/docker/containers.tsx b/ui/src/resources/server/docker/containers.tsx index 74c57720d7..10ccd13156 100644 --- a/ui/src/resources/server/docker/containers.tsx +++ b/ui/src/resources/server/docker/containers.tsx @@ -12,7 +12,7 @@ export default function ServerContainers({ }) { const _search = useServerDockerSearch(); const containers = - useRead("ListDockerContainers", { server: id }, { refetchInterval: 10_000 }) + useRead("ListContainers", { server: id }, { refetchInterval: 10_000 }) .data ?? []; return ( image.in_use); diff --git a/ui/src/resources/server/docker/networks.tsx b/ui/src/resources/server/docker/networks.tsx index 126958fce3..27545a0d35 100644 --- a/ui/src/resources/server/docker/networks.tsx +++ b/ui/src/resources/server/docker/networks.tsx @@ -18,7 +18,7 @@ export default function ServerNetworks({ }) { const [search, setSearch] = useServerDockerSearch(); const networks = - useRead("ListDockerNetworks", { server: id }, { refetchInterval: 10_000 }) + useRead("ListNetworks", { server: id }, { refetchInterval: 10_000 }) .data ?? []; const allInUse = networks.every((network) => diff --git a/ui/src/resources/server/docker/volumes.tsx b/ui/src/resources/server/docker/volumes.tsx index 8979d25884..f36b12e72d 100644 --- a/ui/src/resources/server/docker/volumes.tsx +++ b/ui/src/resources/server/docker/volumes.tsx @@ -18,7 +18,7 @@ export default function ServerVolumes({ }) { const [search, setSearch] = useServerDockerSearch(); const volumes = - useRead("ListDockerVolumes", { server: id }, { refetchInterval: 10_000 }) + useRead("ListVolumes", { server: id }, { refetchInterval: 10_000 }) .data ?? []; const allInUse = volumes.every((volume) => volume.in_use); diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 45743b1d32..a4e5b74731 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -311,7 +311,7 @@ export const ServerComponents: RequiredResourceComponents< { refetchInterval: 5000 }, ).data?.starting_containers; const dontShow = - useRead("ListDockerContainers", { + useRead("ListContainers", { server: id, }).data?.every( (container) => @@ -369,7 +369,7 @@ export const ServerComponents: RequiredResourceComponents< { refetchInterval: 5000 }, ).data?.pausing_containers; const dontShow = - useRead("ListDockerContainers", { + useRead("ListContainers", { server: id, }).data?.every( (container) => @@ -404,7 +404,7 @@ export const ServerComponents: RequiredResourceComponents< { refetchInterval: 5000 }, ).data?.unpausing_containers; const dontShow = - useRead("ListDockerContainers", { + useRead("ListContainers", { server: id, }).data?.every( (container) => diff --git a/ui/src/resources/server/stats/containers.tsx b/ui/src/resources/server/stats/containers.tsx index 16a777ab30..c6c310b56d 100644 --- a/ui/src/resources/server/stats/containers.tsx +++ b/ui/src/resources/server/stats/containers.tsx @@ -19,7 +19,7 @@ export default function ServerContainerStats({ id }: { id: string }) { }); const isServerAvailable = useIsServerAvailable(id); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: id, }, From 8b44b59fbd93b4fd8bb38504bea137342715a513 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 6 Jul 2026 11:37:38 -0700 Subject: [PATCH 089/203] deploy 2.3.0-dev-26 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4fc922c46..cce2f86b6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "async_timing_util", @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "arc-swap", @@ -3525,7 +3525,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "arc-swap", @@ -4641,7 +4641,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "encoding", @@ -7000,7 +7000,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "axum", @@ -8016,7 +8016,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 2e3428b8f6..dac88b55da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-25" +version = "2.3.0-dev-26" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 47a6dfe95c74b9370fa945643f02cb234882606c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 6 Jul 2026 23:48:05 -0700 Subject: [PATCH 090/203] startup support for pagination --- bin/core/src/api/execute/alerter.rs | 2 + bin/core/src/api/execute/mod.rs | 2 + bin/core/src/api/read/action.rs | 6 +++ bin/core/src/api/read/alerter.rs | 6 +++ bin/core/src/api/read/build.rs | 8 ++++ bin/core/src/api/read/builder.rs | 6 +++ bin/core/src/api/read/deployment.rs | 8 ++++ bin/core/src/api/read/docker.rs | 6 +++ bin/core/src/api/read/mod.rs | 6 +++ bin/core/src/api/read/procedure.rs | 6 +++ bin/core/src/api/read/repo.rs | 6 +++ bin/core/src/api/read/schedule.rs | 4 ++ bin/core/src/api/read/server.rs | 6 +++ bin/core/src/api/read/stack.rs | 12 ++++++ bin/core/src/api/read/swarm.rs | 6 +++ bin/core/src/api/read/sync.rs | 6 +++ bin/core/src/api/read/terminal.rs | 6 +++ bin/core/src/api/read/toml.rs | 2 + bin/core/src/api/write/deployment.rs | 2 + bin/core/src/api/write/stack.rs | 2 + bin/core/src/api/write/terminal.rs | 2 + bin/core/src/helpers/procedure.rs | 2 + bin/core/src/monitor/alert/mod.rs | 4 ++ bin/core/src/permission.rs | 48 +++++++++++++++++----- bin/core/src/resource/mod.rs | 59 +++++++++++++++++++--------- 25 files changed, 195 insertions(+), 28 deletions(-) diff --git a/bin/core/src/api/execute/alerter.rs b/bin/core/src/api/execute/alerter.rs index 8e70da7018..791e1f2a3a 100644 --- a/bin/core/src/api/execute/alerter.rs +++ b/bin/core/src/api/execute/alerter.rs @@ -114,6 +114,8 @@ impl Resolve for SendAlert { ) -> Result { let alerters = list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index e313b0ecd8..fdf8abbb20 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -363,6 +363,8 @@ async fn batch_execute( let resources = list_full_for_user_using_pattern::( pattern, Default::default(), + None, + None, user, PermissionLevel::Execute.into(), &[], diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index d815d9d43b..ac3fc74064 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -48,6 +48,8 @@ impl Resolve for ListActions { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -70,6 +72,8 @@ impl Resolve for ListFullActions { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -107,6 +111,8 @@ impl Resolve for GetActionsSummary { ) -> mogh_error::Result { let actions = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index a6c44a2c19..693062767e 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -48,6 +48,8 @@ impl Resolve for ListAlerters { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -70,6 +72,8 @@ impl Resolve for ListFullAlerters { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -85,6 +89,8 @@ impl Resolve for GetAlertersSummary { ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { let query = match list_resource_ids_for_user::( + Default::default(), + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 011b04d34a..051c70eb9f 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -56,6 +56,8 @@ impl Resolve for ListBuilds { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -78,6 +80,8 @@ impl Resolve for ListFullBuilds { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -115,6 +119,8 @@ impl Resolve for GetBuildsSummary { ) -> mogh_error::Result { let builds = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -281,6 +287,8 @@ impl Resolve for ListCommonBuildExtraArgs { }; let builds = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 6062182777..95ccdc020d 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -48,6 +48,8 @@ impl Resolve for ListBuilders { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -70,6 +72,8 @@ impl Resolve for ListFullBuilders { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -85,6 +89,8 @@ impl Resolve for GetBuildersSummary { ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { let query = match list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index adc0614181..fae40bf57e 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -65,6 +65,8 @@ impl Resolve for ListDeployments { let only_update_available = self.query.specific.update_available; let deployments = resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -95,6 +97,8 @@ impl Resolve for ListFullDeployments { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -374,6 +378,8 @@ impl Resolve for GetDeploymentsSummary { ) -> mogh_error::Result { let deployments = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -422,6 +428,8 @@ impl Resolve for ListCommonDeploymentExtraArgs { }; let deployments = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 4c04045bbf..1b4aff1e4e 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -40,6 +40,8 @@ impl Resolve for GetContainersSummary { ) -> mogh_error::Result { let servers = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -83,6 +85,8 @@ impl Resolve for ListAllContainers { .names(self.servers.clone()) .tags(self.tags) .build(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -207,6 +211,8 @@ impl Resolve for GetResourceMatchingContainer { // then check stacks let stacks = list_resources_for_user::( doc! { "config.server_id": &server.id }, + None, + None, user, PermissionLevel::Read.into(), ) diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 4b84dbd501..6c323e1c8c 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -480,18 +480,24 @@ impl Resolve for ListGitProvidersFromConfig { let (builds, repos, syncs) = tokio::try_join!( resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index b8eac7eb99..7de8a1c876 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -46,6 +46,8 @@ impl Resolve for ListProcedures { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -68,6 +70,8 @@ impl Resolve for ListFullProcedures { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -84,6 +88,8 @@ impl Resolve for GetProceduresSummary { ) -> mogh_error::Result { let procedures = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 15b8e093be..ad6a43b3c3 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -46,6 +46,8 @@ impl Resolve for ListRepos { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -68,6 +70,8 @@ impl Resolve for ListFullRepos { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -105,6 +109,8 @@ impl Resolve for GetReposSummary { ) -> mogh_error::Result { let repos = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index fe4843a0cc..5997dd1f71 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -35,6 +35,8 @@ impl Resolve for ListSchedules { tags: self.tags.clone(), specific: Default::default(), }, + None, + None, &args.user, PermissionLevel::Read.into(), &all_tags, @@ -47,6 +49,8 @@ impl Resolve for ListSchedules { tags: self.tags.clone(), specific: Default::default(), }, + None, + None, &args.user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index a207fa5ac6..58a2a729c5 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -43,6 +43,8 @@ impl Resolve for GetServersSummary { ) -> mogh_error::Result { let servers = resource::list_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -107,6 +109,8 @@ impl Resolve for ListServers { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -129,6 +133,8 @@ impl Resolve for ListFullServers { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 89549d5396..87ff4e1116 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -85,6 +85,8 @@ impl Resolve for ListAllStackServices { .names(self.stacks.clone()) .tags(self.tags) .build(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -393,6 +395,8 @@ impl Resolve for ListCommonStackExtraArgs { }; let stacks = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -427,6 +431,8 @@ impl Resolve for ListCommonStackBuildExtraArgs { }; let stacks = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -462,6 +468,8 @@ impl Resolve for ListStacks { let only_update_available = self.query.specific.update_available; let stacks = resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -498,6 +506,8 @@ impl Resolve for ListFullStacks { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -535,6 +545,8 @@ impl Resolve for GetStacksSummary { ) -> mogh_error::Result { let stacks = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index a24c0402eb..534f3a2416 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -46,6 +46,8 @@ impl Resolve for ListSwarms { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -68,6 +70,8 @@ impl Resolve for ListFullSwarms { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -105,6 +109,8 @@ impl Resolve for GetSwarmsSummary { ) -> mogh_error::Result { let swarms = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index da6de125e3..5dbfd5d5ef 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -46,6 +46,8 @@ impl Resolve for ListResourceSyncs { Ok( resource::list_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -68,6 +70,8 @@ impl Resolve for ListFullResourceSyncs { Ok( resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -106,6 +110,8 @@ impl Resolve for GetResourceSyncsSummary { let resource_syncs = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index a7530617d5..82ce0cb87f 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -92,6 +92,8 @@ async fn list_all_terminals_for_user( let (mut servers, stacks, deployments) = tokio::try_join!( resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] @@ -103,12 +105,16 @@ async fn list_all_terminals_for_user( .collect::>())), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index d12eab179d..2dc3c27aa3 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -55,6 +55,8 @@ async fn get_all_targets( targets.extend( resource::list_full_for_user::<$Type>( ResourceQuery::builder().tags(tags).build(), + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index d48bf5357d..97cae36e23 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -632,6 +632,8 @@ impl Resolve for BatchCheckDeploymentForUpdate { let deployments = list_full_for_user_using_pattern::( &self.pattern, Default::default(), + None, + None, user, PermissionLevel::Execute.into(), &[], diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index ebfa1e6280..02df13c5a2 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -1051,6 +1051,8 @@ impl Resolve for BatchCheckStackForUpdate { let stacks = list_full_for_user_using_pattern::( &self.pattern, Default::default(), + None, + None, user, PermissionLevel::Execute.into(), &[], diff --git a/bin/core/src/api/write/terminal.rs b/bin/core/src/api/write/terminal.rs index a4aad5685a..dc42b2086e 100644 --- a/bin/core/src/api/write/terminal.rs +++ b/bin/core/src/api/write/terminal.rs @@ -293,6 +293,8 @@ impl Resolve for BatchDeleteAllTerminals { resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.terminal(), &all_tags, diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 13e894d346..8f58949f64 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -555,6 +555,8 @@ async fn extend_batch_exection( let more = list_full_for_user_using_pattern::( pattern, Default::default(), + None, + None, procedure_user(), PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/monitor/alert/mod.rs b/bin/core/src/monitor/alert/mod.rs index c213973d84..e00f7065c3 100644 --- a/bin/core/src/monitor/alert/mod.rs +++ b/bin/core/src/monitor/alert/mod.rs @@ -36,6 +36,8 @@ async fn get_all_swarms_map() -> anyhow::Result<(HashMap, HashMap)> { let swarms = resource::list_full_for_user::( ResourceQuery::default(), + None, + None, system_user(), PermissionLevel::Read.into(), &[], @@ -61,6 +63,8 @@ async fn get_all_servers_map() { let servers = resource::list_full_for_user::( ResourceQuery::default(), + None, + None, system_user(), PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index 0351db447e..eda949c9b0 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -178,12 +178,14 @@ pub fn get_user_permission_on_resource<'a, T: KomodoResource>( pub async fn list_resources_for_user( filters: impl Into>, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result>> { // Check admin if user.admin { - return list_all_resources::(filters).await; + return list_all_resources::(filters, limit, skip).await; } let mut base = PermissionLevelAndSpecifics { @@ -197,7 +199,7 @@ pub async fn list_resources_for_user( // 'transparent_mode' early return. if base.fulfills(&permission) { - return list_all_resources::(filters).await; + return list_all_resources::(filters, limit, skip).await; } let resource_type = T::resource_type(); @@ -207,7 +209,7 @@ pub async fn list_resources_for_user( base.elevate(all_permission); // 'user.all' early return. if base.fulfills(&permission) { - return list_all_resources::(filters).await; + return list_all_resources::(filters, limit, skip).await; } } @@ -218,13 +220,13 @@ pub async fn list_resources_for_user( base.elevate(all_permission); // 'group.all' early return. if base.fulfills(&permission) { - return list_all_resources::(filters).await; + return list_all_resources::(filters, limit, skip).await; } } } let (all, permissions) = tokio::try_join!( - list_all_resources::(filters), + list_all_resources::(filters, None, None), // And any ids using the permissions table find_collect( &db_client().permissions, @@ -307,6 +309,8 @@ pub async fn list_resources_for_user( /// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access). pub async fn list_resource_ids_for_user( filters: Option, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result>> { @@ -351,11 +355,13 @@ pub async fn list_resource_ids_for_user( } } - let ids = list_resources_for_user::(filters, user, permission) - .await? - .into_iter() - .map(|resource| resource.id) - .collect(); + let ids = list_resources_for_user::( + filters, limit, skip, user, permission, + ) + .await? + .into_iter() + .map(|resource| resource.id) + .collect(); Ok(Some(ids)) } @@ -369,6 +375,8 @@ pub async fn user_resource_target_query( Ok(incoming_query) } else { let swarm_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -384,6 +392,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Swarm" }); let server_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -397,6 +407,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Server" }); let stack_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -410,6 +422,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Stack" }); let deployment_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -423,6 +437,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Deployment" }); let build_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -436,6 +452,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Build" }); let repo_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -449,6 +467,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Repo" }); let procedure_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -462,6 +482,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Procedure" }); let action_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -475,6 +497,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Action" }); let builder_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -488,6 +512,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Builder" }); let alerter_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -502,6 +528,8 @@ pub async fn user_resource_target_query( let resource_sync_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index a8c44ee6f0..d316ca1057 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -262,20 +262,24 @@ pub async fn get( /// Get full resource list with no permissions check. pub async fn list_all_resources( filters: impl Into>, + limit: impl Into>, + skip: impl Into>, ) -> anyhow::Result>> { - find_collect( - T::coll(), - filters, - FindOptions::builder().sort(doc! { "name": 1 }).build(), - ) - .await - .with_context(|| { - format!("Failed to pull {}s from mongo", T::resource_type()) - }) + let options = FindOptions::builder() + .sort(doc! { "name": 1 }) + .limit(limit) + .skip(skip); + find_collect(T::coll(), filters, options.build()) + .await + .with_context(|| { + format!("Failed to pull {}s from mongo", T::resource_type()) + }) } pub async fn list_for_user( mut query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], @@ -283,7 +287,10 @@ pub async fn list_for_user( validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); query.add_filters(&mut filters); - list_for_user_using_document::(filters, user, permission).await + list_for_user_using_document::( + filters, limit, skip, user, permission, + ) + .await } // // pub async fn list_for_user_using_pattern( @@ -308,13 +315,17 @@ pub async fn list_for_user( pub async fn list_for_user_using_document( filters: Document, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result> { - let list = list_resources_for_user::(filters, user, permission) - .await? - .into_iter() - .map(|resource| T::to_list_item(resource)); + let list = list_resources_for_user::( + filters, limit, skip, user, permission, + ) + .await? + .into_iter() + .map(|resource| T::to_list_item(resource)); Ok(join_all(list).await) } @@ -329,13 +340,16 @@ pub async fn list_for_user_using_document( pub async fn list_full_for_user_using_pattern( pattern: &str, query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result>> { - let resources = - list_full_for_user::(query, user, permission, all_tags) - .await?; + let resources = list_full_for_user::( + query, limit, skip, user, permission, all_tags, + ) + .await?; let patterns = parse_string_list(pattern); let mut names = HashSet::::new(); @@ -370,6 +384,8 @@ pub async fn list_full_for_user_using_pattern( pub async fn list_full_for_user( mut query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, user: &User, permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], @@ -377,7 +393,14 @@ pub async fn list_full_for_user( validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); query.add_filters(&mut filters); - list_resources_for_user::(filters, user, permissions).await + list_resources_for_user::( + filters, + limit, + skip, + user, + permissions, + ) + .await } pub type IdResourceMap = HashMap< From bb55120d94441de3f703b496956718054def2e98 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 15:34:03 -0700 Subject: [PATCH 091/203] implement limit / page on the list routes --- Cargo.lock | 1 + bin/cli/src/command/list.rs | 90 +++-- bin/cli/src/command/terminal.rs | 1 + bin/core/src/api/read/action.rs | 8 +- bin/core/src/api/read/alerter.rs | 8 +- bin/core/src/api/read/build.rs | 8 +- bin/core/src/api/read/builder.rs | 8 +- bin/core/src/api/read/deployment.rs | 8 +- bin/core/src/api/read/procedure.rs | 8 +- bin/core/src/api/read/repo.rs | 8 +- bin/core/src/api/read/schedule.rs | 6 +- bin/core/src/api/read/server.rs | 8 +- bin/core/src/api/read/stack.rs | 8 +- bin/core/src/api/read/sync.rs | 8 +- client/core/rs/Cargo.toml | 1 + client/core/rs/src/api/read/action.rs | 37 +- client/core/rs/src/api/read/alerter.rs | 37 +- client/core/rs/src/api/read/build.rs | 34 +- client/core/rs/src/api/read/builder.rs | 37 +- client/core/rs/src/api/read/deployment.rs | 32 +- client/core/rs/src/api/read/docker.rs | 8 +- client/core/rs/src/api/read/mod.rs | 8 +- client/core/rs/src/api/read/procedure.rs | 40 ++- client/core/rs/src/api/read/repo.rs | 37 +- client/core/rs/src/api/read/server.rs | 34 +- client/core/rs/src/api/read/stack.rs | 32 +- client/core/rs/src/api/read/sync.rs | 41 ++- .../rs/src/entities/config/cli/args/list.rs | 5 +- client/core/rs/src/entities/resource.rs | 39 ++- client/core/ts/src/types.ts | 322 +++++++++++++++++- ui/public/client/types.d.ts | 320 ++++++++++++++++- ui/public/client/types.js | 14 +- 32 files changed, 1130 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cce2f86b6d..9cd8280c32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3434,6 +3434,7 @@ dependencies = [ "mongo_indexed", "partial_derive2", "rand 0.10.1", + "regex", "reqwest 0.13.4", "schemars 1.2.1", "serde", diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index b761fdff1f..7aa6d5f3e0 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -52,28 +52,43 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { match &list.command { None => list_all(list).await, Some(ListCommand::Servers(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Stacks(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Deployments(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Builds(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Repos(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Procedures(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Actions(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Syncs(filters)) => { - list_resources::(filters, false).await + list_resources::( + filters, list.page, false, + ) + .await + } + Some(ListCommand::Builders(filters)) => { + list_resources::(filters, list.page, false) + .await + } + Some(ListCommand::Alerters(filters)) => { + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Terminals(filters)) => { list_terminals(filters).await @@ -81,12 +96,6 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { Some(ListCommand::Schedules(filters)) => { list_schedules(filters).await } - Some(ListCommand::Builders(filters)) => { - list_resources::(filters, false).await - } - Some(ListCommand::Alerters(filters)) => { - list_resources::(filters, false).await - } } } @@ -109,14 +118,14 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { .into_iter() .map(|t| (t.id, t.name)) .collect::>())), - ServerListItem::list(client, &filters, true), - StackListItem::list(client, &filters, true), - DeploymentListItem::list(client, &filters, true), - BuildListItem::list(client, &filters, true), - RepoListItem::list(client, &filters, true), - ProcedureListItem::list(client, &filters, true), - ActionListItem::list(client, &filters, true), - ResourceSyncListItem::list(client, &filters, true), + ServerListItem::list(client, &filters, list.page, true), + StackListItem::list(client, &filters, list.page, true), + DeploymentListItem::list(client, &filters, list.page, true), + BuildListItem::list(client, &filters, list.page, true), + RepoListItem::list(client, &filters, list.page, true), + ProcedureListItem::list(client, &filters, list.page, true), + ActionListItem::list(client, &filters, list.page, true), + ResourceSyncListItem::list(client, &filters, list.page, true), )?; if !servers.is_empty() { @@ -172,6 +181,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { async fn list_resources( filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result<()> where @@ -180,7 +190,7 @@ where { let client = crate::command::komodo_client().await?; let (mut resources, tags) = tokio::try_join!( - T::list(client, filters, minimal), + T::list(client, filters, page, minimal), client.read(ListTags::default()).map(|res| res.map(|res| res .into_iter() .map(|t| (t.id, t.name)) @@ -279,6 +289,7 @@ where async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, // For use with root `km ls` minimal: bool, ) -> anyhow::Result>>; @@ -291,6 +302,7 @@ impl ListResources for ServerListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let servers = client @@ -300,6 +312,8 @@ impl ListResources for ServerListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await?; let names = parse_wildcards(&filters.names); @@ -334,12 +348,14 @@ impl ListResources for StackListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let (servers, mut stacks) = tokio::try_join!( client .read(ListServers { query: ResourceQuery::builder().build(), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -351,6 +367,8 @@ impl ListResources for StackListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) )?; stacks.iter_mut().for_each(|stack| { @@ -406,12 +424,14 @@ impl ListResources for DeploymentListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let (servers, mut deployments) = tokio::try_join!( client .read(ListServers { query: ResourceQuery::builder().build(), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -423,6 +443,8 @@ impl ListResources for DeploymentListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) )?; deployments.iter_mut().for_each(|deployment| { @@ -479,12 +501,14 @@ impl ListResources for BuildListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let (builders, mut builds) = tokio::try_join!( client .read(ListBuilders { query: ResourceQuery::builder().build(), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -496,6 +520,8 @@ impl ListResources for BuildListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) )?; builds.iter_mut().for_each(|build| { @@ -547,6 +573,7 @@ impl ListResources for RepoListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -557,6 +584,8 @@ impl ListResources for RepoListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() @@ -595,6 +624,7 @@ impl ListResources for ProcedureListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -605,6 +635,8 @@ impl ListResources for ProcedureListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() @@ -643,6 +675,7 @@ impl ListResources for ActionListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -653,6 +686,8 @@ impl ListResources for ActionListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() @@ -691,6 +726,7 @@ impl ListResources for ResourceSyncListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -701,6 +737,8 @@ impl ListResources for ResourceSyncListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() @@ -736,6 +774,7 @@ impl ListResources for BuilderListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -746,6 +785,8 @@ impl ListResources for BuilderListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() @@ -768,6 +809,7 @@ impl ListResources for AlerterListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -778,6 +820,8 @@ impl ListResources for AlerterListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: 100, + page: page.saturating_sub(1), }) .await? .into_iter() diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index 7ce5f9cf42..da17366d86 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -162,6 +162,7 @@ async fn get_server( let servers = client .read(ListServers { query: ServerQuery::builder().names(servers).build(), + ..Default::default() }) .await? .into_iter() diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index ac3fc74064..87d1aaca32 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -48,8 +48,8 @@ impl Resolve for ListActions { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -72,8 +72,8 @@ impl Resolve for ListFullActions { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 693062767e..9320e198c6 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -48,8 +48,8 @@ impl Resolve for ListAlerters { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -72,8 +72,8 @@ impl Resolve for ListFullAlerters { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 051c70eb9f..eeef448f9e 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -56,8 +56,8 @@ impl Resolve for ListBuilds { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -80,8 +80,8 @@ impl Resolve for ListFullBuilds { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 95ccdc020d..f3fae9032e 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -48,8 +48,8 @@ impl Resolve for ListBuilders { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -72,8 +72,8 @@ impl Resolve for ListFullBuilders { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index fae40bf57e..08716128d3 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -65,8 +65,8 @@ impl Resolve for ListDeployments { let only_update_available = self.query.specific.update_available; let deployments = resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -97,8 +97,8 @@ impl Resolve for ListFullDeployments { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 7de8a1c876..faee0070d2 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -46,8 +46,8 @@ impl Resolve for ListProcedures { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -70,8 +70,8 @@ impl Resolve for ListFullProcedures { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index ad6a43b3c3..1cad5a04e9 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -46,8 +46,8 @@ impl Resolve for ListRepos { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -70,8 +70,8 @@ impl Resolve for ListFullRepos { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index 5997dd1f71..7df93ae75f 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -29,11 +29,10 @@ impl Resolve for ListSchedules { let (actions, procedures) = tokio::try_join!( list_full_for_user::( ResourceQuery { - names: Default::default(), templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), - specific: Default::default(), + ..Default::default() }, None, None, @@ -43,11 +42,10 @@ impl Resolve for ListSchedules { ), list_full_for_user::( ResourceQuery { - names: Default::default(), templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), - specific: Default::default(), + ..Default::default() }, None, None, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 58a2a729c5..6ec4598bc1 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -109,8 +109,8 @@ impl Resolve for ListServers { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -133,8 +133,8 @@ impl Resolve for ListFullServers { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 87ff4e1116..7901445e65 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -468,8 +468,8 @@ impl Resolve for ListStacks { let only_update_available = self.query.specific.update_available; let stacks = resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -506,8 +506,8 @@ impl Resolve for ListFullStacks { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 5dbfd5d5ef..6cd7131c13 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -46,8 +46,8 @@ impl Resolve for ListResourceSyncs { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -70,8 +70,8 @@ impl Resolve for ListFullResourceSyncs { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/client/core/rs/Cargo.toml b/client/core/rs/Cargo.toml index b0acef8502..cdcca188e7 100644 --- a/client/core/rs/Cargo.toml +++ b/client/core/rs/Cargo.toml @@ -52,6 +52,7 @@ bytes.workspace = true serde.workspace = true tokio.workspace = true strum.workspace = true +regex.workspace = true bson.workspace = true clap.workspace = true envy.workspace = true diff --git a/client/core/rs/src/api/read/action.rs b/client/core/rs/src/api/read/action.rs index bc1a9f0352..71a52f743f 100644 --- a/client/core/rs/src/api/read/action.rs +++ b/client/core/rs/src/api/read/action.rs @@ -2,11 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::action::{ - Action, ActionActionState, ActionListItem, ActionQuery, +use crate::entities::{ + U64, + action::{Action, ActionActionState, ActionListItem, ActionQuery}, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -63,6 +64,21 @@ pub struct ListActions { /// optional structured query to filter actions. #[serde(default)] pub query: ActionQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -93,6 +109,21 @@ pub struct ListFullActions { /// optional structured query to filter actions. #[serde(default)] pub query: ActionQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs index b72c1807fe..55980a2d5f 100644 --- a/client/core/rs/src/api/read/alerter.rs +++ b/client/core/rs/src/api/read/alerter.rs @@ -2,11 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::alerter::{ - Alerter, AlerterListItem, AlerterQuery, +use crate::entities::{ + U64, + alerter::{Alerter, AlerterListItem, AlerterQuery}, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -63,6 +64,21 @@ pub struct ListAlerters { /// Structured query to filter alerters. #[serde(default)] pub query: AlerterQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -93,6 +109,21 @@ pub struct ListFullAlerters { /// Structured query to filter alerters. #[serde(default)] pub query: AlerterQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs index 11cc9d83b6..b23264dad9 100644 --- a/client/core/rs/src/api/read/build.rs +++ b/client/core/rs/src/api/read/build.rs @@ -5,11 +5,11 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{ - I64, Version, + I64, U64, Version, build::{Build, BuildActionState, BuildListItem, BuildQuery}, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -66,6 +66,21 @@ pub struct ListBuilds { /// optional structured query to filter builds. #[serde(default)] pub query: BuildQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -96,6 +111,21 @@ pub struct ListFullBuilds { /// optional structured query to filter builds. #[serde(default)] pub query: BuildQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs index c6824d8f42..4b4753b41f 100644 --- a/client/core/rs/src/api/read/builder.rs +++ b/client/core/rs/src/api/read/builder.rs @@ -2,11 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::builder::{ - Builder, BuilderListItem, BuilderQuery, +use crate::entities::{ + U64, + builder::{Builder, BuilderListItem, BuilderQuery}, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -62,6 +63,21 @@ pub fn list_builders() {} pub struct ListBuilders { #[serde(default)] pub query: BuilderQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -91,6 +107,21 @@ pub fn list_full_builders() {} pub struct ListFullBuilders { #[serde(default)] pub query: BuilderQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index e7f50ac679..186618d680 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -15,7 +15,7 @@ use crate::entities::{ update::Log, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -73,6 +73,21 @@ pub struct ListDeployments { /// optional structured query to filter deployments. #[serde(default)] pub query: DeploymentQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -104,6 +119,21 @@ pub struct ListFullDeployments { /// optional structured query to filter deployments. #[serde(default)] pub query: DeploymentQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 675aa0577a..497c4717c0 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -16,7 +16,7 @@ use crate::entities::{ update::Log, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -113,14 +113,10 @@ pub struct ListAllContainers { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_limit")] + #[serde(default = "default_list_limit")] pub limit: U64, } -fn default_limit() -> u64 { - 100 -} - #[typeshare] pub type ListAllContainersResponse = Vec; diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 64ed527a4e..89934b9186 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -64,6 +64,12 @@ pub trait KomodoReadRequest: HasResponse {} // +pub fn default_list_limit() -> u64 { + 100 +} + +// + #[cfg(feature = "utoipa")] #[utoipa::path( post, @@ -209,7 +215,7 @@ pub fn list_image_registries_from_config() {} /// - registries in core config /// - registries configured on builds, deployments /// - registries on the optional Server or Builder -/// +/// /// Pre v2.3.0, called `ListDockerRegistriesFromConfig` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs index bd0b74b067..8f87b301d6 100644 --- a/client/core/rs/src/api/read/procedure.rs +++ b/client/core/rs/src/api/read/procedure.rs @@ -2,11 +2,15 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::procedure::{ - Procedure, ProcedureActionState, ProcedureListItem, ProcedureQuery, +use crate::entities::{ + U64, + procedure::{ + Procedure, ProcedureActionState, ProcedureListItem, + ProcedureQuery, + }, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -63,6 +67,21 @@ pub struct ListProcedures { /// optional structured query to filter procedures. #[serde(default)] pub query: ProcedureQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -93,6 +112,21 @@ pub struct ListFullProcedures { /// optional structured query to filter procedures. #[serde(default)] pub query: ProcedureQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs index be576ad240..8a155ec521 100644 --- a/client/core/rs/src/api/read/repo.rs +++ b/client/core/rs/src/api/read/repo.rs @@ -2,11 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::repo::{ - Repo, RepoActionState, RepoListItem, RepoQuery, +use crate::entities::{ + U64, + repo::{Repo, RepoActionState, RepoListItem, RepoQuery}, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -63,6 +64,21 @@ pub struct ListRepos { /// optional structured query to filter repos. #[serde(default)] pub query: RepoQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -93,6 +109,21 @@ pub struct ListFullRepos { /// optional structured query to filter repos. #[serde(default)] pub query: RepoQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index fa4ae67b59..e937d077de 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{ - I64, Timelength, + I64, Timelength, U64, server::{ PeripheryInformation, Server, ServerActionState, ServerListItem, ServerQuery, ServerState, @@ -13,7 +13,7 @@ use crate::entities::{ }, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -70,6 +70,21 @@ pub struct ListServers { /// optional structured query to filter servers. #[serde(default)] pub query: ServerQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -100,6 +115,21 @@ pub struct ListFullServers { /// optional structured query to filter servers. #[serde(default)] pub query: ServerQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 065fa8896d..2606efbdfa 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -14,7 +14,7 @@ use crate::entities::{ update::Log, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -418,6 +418,21 @@ pub struct ListStacks { /// optional structured query to filter stacks. #[serde(default)] pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -448,6 +463,21 @@ pub struct ListFullStacks { /// optional structured query to filter stacks. #[serde(default)] pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/api/read/sync.rs b/client/core/rs/src/api/read/sync.rs index afaccdcdab..37bb977516 100644 --- a/client/core/rs/src/api/read/sync.rs +++ b/client/core/rs/src/api/read/sync.rs @@ -2,12 +2,15 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::sync::{ - ResourceSync, ResourceSyncActionState, ResourceSyncListItem, - ResourceSyncQuery, +use crate::entities::{ + U64, + sync::{ + ResourceSync, ResourceSyncActionState, ResourceSyncListItem, + ResourceSyncQuery, + }, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -64,6 +67,21 @@ pub struct ListResourceSyncs { /// optional structured query to filter syncs. #[serde(default)] pub query: ResourceSyncQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -94,6 +112,21 @@ pub struct ListFullResourceSyncs { /// optional structured query to filter syncs. #[serde(default)] pub query: ResourceSyncQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/entities/config/cli/args/list.rs b/client/core/rs/src/entities/config/cli/args/list.rs index bb463af5c9..1c72d7a2b1 100644 --- a/client/core/rs/src/entities/config/cli/args/list.rs +++ b/client/core/rs/src/entities/config/cli/args/list.rs @@ -18,7 +18,7 @@ pub struct List { /// List only "in progress" / "pending" resources, like Actions / Procedures that are running (alias: `pending`) #[arg( long, - short = 'p', + short = 'i', alias = "pending", default_value_t = false )] @@ -52,6 +52,9 @@ pub struct List { /// Specify the format of the output. #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)] pub format: super::CliFormat, + /// Specify page for resource list, starting at 1. + #[arg(long, short = 'p', default_value_t = 1)] + pub page: u64, } impl From for ResourceFilters { diff --git a/client/core/rs/src/entities/resource.rs b/client/core/rs/src/entities/resource.rs index e5ef642c61..0d638e6367 100644 --- a/client/core/rs/src/entities/resource.rs +++ b/client/core/rs/src/entities/resource.rs @@ -120,10 +120,13 @@ pub struct ResourceListItem { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ResourceQuery { - #[serde(default)] + /// List of search terms. Names must contain + /// all terms to match. + #[serde(default, deserialize_with = "string_list_deserializer")] + pub terms: Vec, + /// List of exact names to return + #[serde(default, deserialize_with = "string_list_deserializer")] pub names: Vec, - #[serde(default)] - pub templates: TemplatesQueryBehavior, /// Pass Vec of tag ids or tag names #[serde(default, deserialize_with = "string_list_deserializer")] pub tags: Vec, @@ -131,6 +134,8 @@ pub struct ResourceQuery { #[serde(default)] pub tag_behavior: TagQueryBehavior, #[serde(default)] + pub templates: TemplatesQueryBehavior, + #[serde(default)] pub specific: T, } @@ -177,13 +182,36 @@ impl AddFilters for () {} impl AddFilters for ResourceQuery { fn add_filters(&self, filters: &mut Document) { + // Seperate _id query let (ids, names) = split_names(&self.names); if !ids.is_empty() { filters.insert("_id", doc! { "$in": ids }); } + + // Name queries using $and doc array. + let mut name_queries = Vec::::new(); + if !names.is_empty() { - filters.insert("name", doc! { "$in": names }); + name_queries.push(doc! { "name": { "$in": names } }); } + + for term in &self.terms { + if term.is_empty() { + continue; + } + name_queries.push(doc! { + "name": { + "$regex": regex::escape(term), + "$options": "i" + }, + }); + } + + if !name_queries.is_empty() { + filters.insert("$and", name_queries); + } + + // Template query match self.templates { TemplatesQueryBehavior::Exclude => { filters.insert("template", doc! { "$ne": true }); @@ -195,6 +223,8 @@ impl AddFilters for ResourceQuery { // No query on template field necessary } }; + + // Tag queries maybe using $or doc array if !self.tags.is_empty() { match self.tag_behavior { TagQueryBehavior::All => { @@ -210,6 +240,7 @@ impl AddFilters for ResourceQuery { } } } + self.specific.add_filters(filters); } } diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 21225a68b3..acc7e7cbbe 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -185,6 +185,13 @@ export interface ActionListItemInfo { export type ActionListItem = ResourceListItem; +export enum TagQueryBehavior { + /** Returns resources which have strictly all the tags */ + All = "All", + /** Returns resources which have one or more of the tags */ + Any = "Any", +} + export enum TemplatesQueryBehavior { /** Include templates in results. Default. */ Include = "Include", @@ -194,21 +201,20 @@ export enum TemplatesQueryBehavior { Only = "Only", } -export enum TagQueryBehavior { - /** Returns resources which have strictly all the tags */ - All = "All", - /** Returns resources which have one or more of the tags */ - Any = "Any", -} - /** Passing empty Vec is the same as not filtering by that field */ export interface ResourceQuery { + /** + * List of search terms. Names must contain + * all terms to match. + */ + terms?: string[]; + /** List of exact names to return */ names?: string[]; - templates?: TemplatesQueryBehavior; /** Pass Vec of tag ids or tag names */ tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + templates?: TemplatesQueryBehavior; specific?: T; } @@ -8652,12 +8658,42 @@ export interface LatestCommit { export interface ListActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** @@ -8812,12 +8848,42 @@ export interface ListBuildVersions { /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** @@ -8883,29 +8949,104 @@ export interface ListContainers { export interface ListDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ export interface ListFullAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ export interface ListFullBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ export interface ListFullBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** @@ -8915,36 +9056,126 @@ export interface ListFullBuilds { export interface ListFullDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ export interface ListFullProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ export interface ListFullRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ export interface ListFullResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ export interface ListFullServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ export interface ListFullStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ @@ -9065,18 +9296,63 @@ export interface ListPermissions { export interface ListProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** @@ -9106,6 +9382,21 @@ export interface ListSecrets { export interface ListServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ @@ -9118,6 +9409,21 @@ export interface ListStackServices { export interface ListStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 4c1c588274..d58b446b15 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -166,6 +166,12 @@ export interface ActionListItemInfo { schedule_error?: string; } export type ActionListItem = ResourceListItem; +export declare enum TagQueryBehavior { + /** Returns resources which have strictly all the tags */ + All = "All", + /** Returns resources which have one or more of the tags */ + Any = "Any" +} export declare enum TemplatesQueryBehavior { /** Include templates in results. Default. */ Include = "Include", @@ -174,20 +180,20 @@ export declare enum TemplatesQueryBehavior { /** Results *only* includes templates. */ Only = "Only" } -export declare enum TagQueryBehavior { - /** Returns resources which have strictly all the tags */ - All = "All", - /** Returns resources which have one or more of the tags */ - Any = "Any" -} /** Passing empty Vec is the same as not filtering by that field */ export interface ResourceQuery { + /** + * List of search terms. Names must contain + * all terms to match. + */ + terms?: string[]; + /** List of exact names to return */ names?: string[]; - templates?: TemplatesQueryBehavior; /** Pass Vec of tag ids or tag names */ tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + templates?: TemplatesQueryBehavior; specific?: T; } export interface ActionQuerySpecifics { @@ -8246,11 +8252,41 @@ export interface LatestCommit { export interface ListActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * Get a paginated list of alerts sorted by timestamp descending. @@ -8397,11 +8433,41 @@ export interface ListBuildVersions { /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * Gets a list of existing values used as extra args across other builds. @@ -8460,25 +8526,100 @@ export interface ListContainers { export interface ListDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ export interface ListFullAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ export interface ListFullBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ export interface ListFullBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * List deployments matching optional query. @@ -8487,31 +8628,121 @@ export interface ListFullBuilds { export interface ListFullDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ export interface ListFullProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ export interface ListFullRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ export interface ListFullResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ export interface ListFullServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ export interface ListFullStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ export interface ListFullSwarms { @@ -8621,16 +8852,61 @@ export interface ListPermissions { export interface ListProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * List configured schedules. @@ -8657,6 +8933,21 @@ export interface ListSecrets { export interface ListServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ export interface ListStackServices { @@ -8667,6 +8958,21 @@ export interface ListStackServices { export interface ListStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit: U64; } /** * List configs on the target Swarm. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index 5328a6d206..c663d15412 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -36,6 +36,13 @@ export var ActionState; /** Currently running */ ActionState["Running"] = "Running"; })(ActionState || (ActionState = {})); +export var TagQueryBehavior; +(function (TagQueryBehavior) { + /** Returns resources which have strictly all the tags */ + TagQueryBehavior["All"] = "All"; + /** Returns resources which have one or more of the tags */ + TagQueryBehavior["Any"] = "Any"; +})(TagQueryBehavior || (TagQueryBehavior = {})); export var TemplatesQueryBehavior; (function (TemplatesQueryBehavior) { /** Include templates in results. Default. */ @@ -45,13 +52,6 @@ export var TemplatesQueryBehavior; /** Results *only* includes templates. */ TemplatesQueryBehavior["Only"] = "Only"; })(TemplatesQueryBehavior || (TemplatesQueryBehavior = {})); -export var TagQueryBehavior; -(function (TagQueryBehavior) { - /** Returns resources which have strictly all the tags */ - TagQueryBehavior["All"] = "All"; - /** Returns resources which have one or more of the tags */ - TagQueryBehavior["Any"] = "Any"; -})(TagQueryBehavior || (TagQueryBehavior = {})); /** Types of maintenance schedules */ export var MaintenanceScheduleType; (function (MaintenanceScheduleType) { From 52a38f39f4fc12dd918adb1ab77fcf284d4d70e1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 16:07:40 -0700 Subject: [PATCH 092/203] swarm pagination and add swarm read support to cli --- bin/cli/src/command/list.rs | 95 ++++++++++++++++++- bin/core/src/api/read/swarm.rs | 8 +- client/core/rs/src/api/read/swarm.rs | 32 ++++++- .../rs/src/entities/config/cli/args/list.rs | 12 +++ 4 files changed, 141 insertions(+), 6 deletions(-) diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index 7aa6d5f3e0..acc0d8d889 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -7,7 +7,8 @@ use komodo_client::{ api::read::{ ListActions, ListAlerters, ListBuilders, ListBuilds, ListDeployments, ListProcedures, ListRepos, ListResourceSyncs, - ListSchedules, ListServers, ListStacks, ListTags, ListTerminals, + ListSchedules, ListServers, ListStacks, ListSwarms, ListTags, + ListTerminals, }, entities::{ ResourceTargetVariant, @@ -31,6 +32,7 @@ use komodo_client::{ schedule::Schedule, server::{ServerListItem, ServerListItemInfo, ServerState}, stack::{StackListItem, StackListItemInfo, StackState}, + swarm::{SwarmListItem, SwarmListItemInfo, SwarmState}, sync::{ ResourceSyncListItem, ResourceSyncListItemInfo, ResourceSyncState, @@ -55,6 +57,10 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { list_resources::(filters, list.page, false) .await } + Some(ListCommand::Swarms(filters)) => { + list_resources::(filters, list.page, false) + .await + } Some(ListCommand::Stacks(filters)) => { list_resources::(filters, list.page, false).await } @@ -106,6 +112,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { let ( tags, mut servers, + mut swarms, mut stacks, mut deployments, mut builds, @@ -119,6 +126,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { .map(|t| (t.id, t.name)) .collect::>())), ServerListItem::list(client, &filters, list.page, true), + SwarmListItem::list(client, &filters, list.page, true), StackListItem::list(client, &filters, list.page, true), DeploymentListItem::list(client, &filters, list.page, true), BuildListItem::list(client, &filters, list.page, true), @@ -134,6 +142,12 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { println!(); } + if !swarms.is_empty() { + fix_tags(&mut swarms, &tags); + print_items(swarms, filters.format, list.links)?; + println!(); + } + if !stacks.is_empty() { fix_tags(&mut stacks, &tags); print_items(stacks, filters.format, list.links)?; @@ -343,6 +357,52 @@ impl ListResources for ServerListItem { } } +impl ListResources for SwarmListItem { + type Info = SwarmListItemInfo; + async fn list( + client: &KomodoClient, + filters: &ResourceFilters, + page: u64, + _minimal: bool, + ) -> anyhow::Result> { + let servers = client + .read(ListSwarms { + query: ResourceQuery::builder() + .tags(filters.tags.clone()) + // .tag_behavior(TagQueryBehavior::Any) + .templates(filters.templates) + .build(), + limit: 100, + page: page.saturating_sub(1), + }) + .await?; + let names = parse_wildcards(&filters.names); + let server_wildcards = parse_wildcards(&filters.servers); + let mut servers = servers + .into_iter() + .filter(|server| { + let state_check = if filters.all { + true + } else if filters.down { + !matches!(server.info.state, SwarmState::Healthy) + } else if filters.in_progress { + false + } else { + matches!(server.info.state, SwarmState::Healthy) + }; + let name_items = &[server.name.as_str()]; + state_check + && matches_wildcards(&names, name_items) + && matches_wildcards(&server_wildcards, name_items) + }) + .collect::>(); + servers.sort_by(|a, b| { + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) + }); + Ok(servers) + } +} + impl ListResources for StackListItem { type Info = StackListItemInfo; async fn list( @@ -876,6 +936,39 @@ impl PrintTable for ResourceListItem { } } +impl PrintTable for ResourceListItem { + fn header(links: bool) -> &'static [&'static str] { + if links { + &["Swarm", "State", "Tags", "Link"] + } else { + &["Swarm", "State", "Tags"] + } + } + fn row(self, links: bool) -> Vec { + let color = match self.info.state { + SwarmState::Healthy => Color::Green, + SwarmState::Unhealthy => Color::Red, + SwarmState::Down => Color::Blue, + SwarmState::Unknown => Color::Magenta, + }; + let mut res = vec![ + Cell::new(self.name).add_attribute(Attribute::Bold), + Cell::new(self.info.state.to_string()) + .fg(color) + .add_attribute(Attribute::Bold), + Cell::new(self.tags.join(", ")), + ]; + if links { + res.push(Cell::new(resource_link( + &cli_config().host, + ResourceTargetVariant::Server, + &self.id, + ))) + } + res + } +} + impl PrintTable for ResourceListItem { fn header(links: bool) -> &'static [&'static str] { if links { diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 534f3a2416..6e3f52fca5 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -46,8 +46,8 @@ impl Resolve for ListSwarms { Ok( resource::list_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, @@ -70,8 +70,8 @@ impl Resolve for ListFullSwarms { Ok( resource::list_full_for_user::( self.query, - None, - None, + self.limit as i64, + self.page * self.limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index 6e9cf8cbae..6f3fe75348 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -18,7 +18,7 @@ use crate::entities::{ update::Log, }; -use super::KomodoReadRequest; +use super::{KomodoReadRequest, default_list_limit}; // @@ -75,6 +75,21 @@ pub struct ListSwarms { /// Optional structured query to filter Swarms. #[serde(default)] pub query: SwarmQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] @@ -105,6 +120,21 @@ pub struct ListFullSwarms { /// optional structured query to filter swarms. #[serde(default)] pub query: SwarmQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + #[serde(default = "default_list_limit")] + pub limit: U64, } #[typeshare] diff --git a/client/core/rs/src/entities/config/cli/args/list.rs b/client/core/rs/src/entities/config/cli/args/list.rs index 1c72d7a2b1..9a28df6105 100644 --- a/client/core/rs/src/entities/config/cli/args/list.rs +++ b/client/core/rs/src/entities/config/cli/args/list.rs @@ -45,6 +45,10 @@ pub struct List { /// Can be specified multiple times. (alias `s`) #[arg(name = "server", long, short = 's')] pub servers: Vec, + /// Filter by a particular swarm. Supports wildcard. + /// Can be specified multiple times. (alias `w`) + #[arg(name = "swarm", long, short = 'w')] + pub swarms: Vec, /// Filter by a particular builder. Supports wildcard. /// Can be specified multiple times. (alias `b`) #[arg(name = "builder", long, short = 'b')] @@ -69,6 +73,7 @@ impl From for ResourceFilters { names: value.names, tags: value.tags, servers: value.servers, + swarms: value.swarms, builders: value.builders, format: value.format, } @@ -80,6 +85,9 @@ pub enum ListCommand { /// List Servers (aliases: `server`, `sv`) #[clap(alias = "server", alias = "sv")] Servers(ResourceFilters), + /// List Swarms (aliases: `swarm`, `sw`) + #[clap(alias = "swarm", alias = "sw")] + Swarms(ResourceFilters), /// List Stacks (aliases: `stack`, `st`) #[clap(alias = "stack", alias = "st")] Stacks(ResourceFilters), @@ -157,6 +165,10 @@ pub struct ResourceFilters { /// Can be specified multiple times. (alias `s`) #[arg(name = "server", long, short = 's')] pub servers: Vec, + /// Filter by a particular swarm. Supports wildcard. + /// Can be specified multiple times. (alias `w`) + #[arg(name = "swarm", long, short = 'w')] + pub swarms: Vec, /// Filter by a particular builder. Supports wildcard. /// Can be specified multiple times. (alias `b`) #[arg(name = "builder", long, short = 'b')] From d8d731cfd57ffdbbef86b72427cdf3f578af2283 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 16:45:48 -0700 Subject: [PATCH 093/203] use Option for limit --- bin/cli/src/command/container.rs | 4 +- bin/cli/src/command/list.rs | 28 ++++---- bin/cli/src/command/terminal.rs | 3 +- bin/core/src/api/read/action.rs | 10 +-- bin/core/src/api/read/alerter.rs | 10 +-- bin/core/src/api/read/build.rs | 10 +-- bin/core/src/api/read/builder.rs | 10 +-- bin/core/src/api/read/deployment.rs | 10 +-- bin/core/src/api/read/docker.rs | 7 +- bin/core/src/api/read/procedure.rs | 10 +-- bin/core/src/api/read/repo.rs | 10 +-- bin/core/src/api/read/server.rs | 10 +-- bin/core/src/api/read/stack.rs | 17 +++-- bin/core/src/api/read/swarm.rs | 10 +-- bin/core/src/api/read/sync.rs | 10 +-- client/core/rs/src/api/read/action.rs | 8 +-- client/core/rs/src/api/read/alerter.rs | 8 +-- client/core/rs/src/api/read/build.rs | 8 +-- client/core/rs/src/api/read/builder.rs | 8 +-- client/core/rs/src/api/read/deployment.rs | 8 +-- client/core/rs/src/api/read/docker.rs | 7 +- client/core/rs/src/api/read/mod.rs | 4 +- client/core/rs/src/api/read/procedure.rs | 8 +-- client/core/rs/src/api/read/repo.rs | 8 +-- client/core/rs/src/api/read/server.rs | 8 +-- client/core/rs/src/api/read/stack.rs | 17 ++--- client/core/rs/src/api/read/swarm.rs | 8 +-- client/core/rs/src/api/read/sync.rs | 8 +-- client/core/ts/src/types.ts | 78 ++++++++++++++++------- ui/public/client/types.d.ts | 78 ++++++++++++++++------- ui/src/components/updates/resource.tsx | 14 +++- ui/src/lib/utils.ts | 13 +--- 32 files changed, 260 insertions(+), 190 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index 8c714cf7fa..36fcaf500f 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -64,7 +64,7 @@ async fn list_containers( tags: Default::default(), terms: terms.clone(), state: Default::default(), - limit: 100, + limit: Some(100), // Page is more naturally given starting as 1, 2, 3. page: if *page == 0 { 0 } else { *page - 1 }, }), @@ -152,7 +152,7 @@ pub async fn inspect_container( tags: Default::default(), terms: vec![inspect.container.clone()], state: Default::default(), - limit: 300, + limit: Some(300), page: 0, }), )?; diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index acc0d8d889..777d9ef4b9 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -58,8 +58,7 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { .await } Some(ListCommand::Swarms(filters)) => { - list_resources::(filters, list.page, false) - .await + list_resources::(filters, list.page, false).await } Some(ListCommand::Stacks(filters)) => { list_resources::(filters, list.page, false).await @@ -326,7 +325,7 @@ impl ListResources for ServerListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await?; @@ -372,7 +371,7 @@ impl ListResources for SwarmListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await?; @@ -415,6 +414,7 @@ impl ListResources for StackListItem { client .read(ListServers { query: ResourceQuery::builder().build(), + limit: Some(0), ..Default::default() }) .map(|res| res.map(|res| res @@ -427,7 +427,7 @@ impl ListResources for StackListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) )?; @@ -491,6 +491,7 @@ impl ListResources for DeploymentListItem { client .read(ListServers { query: ResourceQuery::builder().build(), + limit: Some(0), ..Default::default() }) .map(|res| res.map(|res| res @@ -503,7 +504,7 @@ impl ListResources for DeploymentListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) )?; @@ -568,6 +569,7 @@ impl ListResources for BuildListItem { client .read(ListBuilders { query: ResourceQuery::builder().build(), + limit: Some(0), ..Default::default() }) .map(|res| res.map(|res| res @@ -580,7 +582,7 @@ impl ListResources for BuildListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) )?; @@ -644,7 +646,7 @@ impl ListResources for RepoListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? @@ -695,7 +697,7 @@ impl ListResources for ProcedureListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? @@ -746,7 +748,7 @@ impl ListResources for ActionListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? @@ -797,7 +799,7 @@ impl ListResources for ResourceSyncListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? @@ -845,7 +847,7 @@ impl ListResources for BuilderListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? @@ -880,7 +882,7 @@ impl ListResources for AlerterListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: 100, + limit: Some(100), page: page.saturating_sub(1), }) .await? diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index da17366d86..e922296541 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -132,7 +132,7 @@ async fn get_server( tags: Default::default(), terms: vec![container.to_string()], state: Default::default(), - limit: 300, + limit: Some(0), page: 0, }) .await?; @@ -162,6 +162,7 @@ async fn get_server( let servers = client .read(ListServers { query: ServerQuery::builder().names(servers).build(), + limit: Some(0), ..Default::default() }) .await? diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 87d1aaca32..6052fe68a1 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -45,11 +45,12 @@ impl Resolve for ListActions { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -69,11 +70,12 @@ impl Resolve for ListFullActions { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 9320e198c6..9bb458907b 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -45,11 +45,12 @@ impl Resolve for ListAlerters { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -69,11 +70,12 @@ impl Resolve for ListFullAlerters { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index eeef448f9e..4b01bf5c0d 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -53,11 +53,12 @@ impl Resolve for ListBuilds { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -77,11 +78,12 @@ impl Resolve for ListFullBuilds { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index f3fae9032e..004feedaae 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -45,11 +45,12 @@ impl Resolve for ListBuilders { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -69,11 +70,12 @@ impl Resolve for ListFullBuilders { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 08716128d3..c0daffcb4d 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -63,10 +63,11 @@ impl Resolve for ListDeployments { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let deployments = resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -94,11 +95,12 @@ impl Resolve for ListFullDeployments { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 1b4aff1e4e..80ea89c7d5 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -95,7 +95,8 @@ impl Resolve for ListAllContainers { let mut containers = Vec::::new(); let mut skipped = 0; - let limit_usize = self.limit as usize; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit_usize = limit as usize; for server in servers { let cache = server_status_cache() @@ -116,13 +117,13 @@ impl Resolve for ListAllContainers { || self.terms.iter().all(|term| container.name.contains(term))) }); for container in more { - if skipped < self.limit * self.page { + if skipped < limit * self.page { // Eg. page 1 skips until after 100 containers, page 2 after 200. skipped += 1; } else { // push and maybe early return containers.push(container.clone()); - if containers.len() >= limit_usize { + if limit > 0 && containers.len() >= limit_usize { return Ok(containers); } } diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index faee0070d2..7d1034e985 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -43,11 +43,12 @@ impl Resolve for ListProcedures { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -67,11 +68,12 @@ impl Resolve for ListFullProcedures { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 1cad5a04e9..25c7dc19ff 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -43,11 +43,12 @@ impl Resolve for ListRepos { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -67,11 +68,12 @@ impl Resolve for ListFullRepos { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 6ec4598bc1..fdc19db884 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -106,11 +106,12 @@ impl Resolve for ListServers { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -130,11 +131,12 @@ impl Resolve for ListFullServers { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 7901445e65..854722aeac 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -95,7 +95,8 @@ impl Resolve for ListAllStackServices { let mut services = Vec::::new(); let mut skipped = 0; - let limit_usize = self.limit as usize; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit_usize = limit as usize; for stack in stacks { let cache = @@ -111,13 +112,13 @@ impl Resolve for ListAllStackServices { || self.terms.iter().all(|term| service.service.contains(term))) }); for service in more { - if skipped < self.limit * self.page { + if skipped < limit * self.page { // Eg. page 1 skips until after 100 services, page 2 after 200. skipped += 1; } else { // push and maybe early return services.push(service.clone()); - if services.len() >= limit_usize { + if limit > 0 && services.len() >= limit_usize { return Ok(services); } } @@ -466,10 +467,11 @@ impl Resolve for ListStacks { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let stacks = resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -503,11 +505,12 @@ impl Resolve for ListFullStacks { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 6e3f52fca5..6bdaec8ce6 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -43,11 +43,12 @@ impl Resolve for ListSwarms { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -67,11 +68,12 @@ impl Resolve for ListFullSwarms { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 6cd7131c13..448cd30d50 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -43,11 +43,12 @@ impl Resolve for ListResourceSyncs { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, @@ -67,11 +68,12 @@ impl Resolve for ListFullResourceSyncs { } else { get_all_tags(None).await? }; + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( resource::list_full_for_user::( self.query, - self.limit as i64, - self.page * self.limit, + limit as i64, + self.page * limit, user, PermissionLevel::Read.into(), &all_tags, diff --git a/client/core/rs/src/api/read/action.rs b/client/core/rs/src/api/read/action.rs index 71a52f743f..805fe981f9 100644 --- a/client/core/rs/src/api/read/action.rs +++ b/client/core/rs/src/api/read/action.rs @@ -7,7 +7,7 @@ use crate::entities::{ action::{Action, ActionActionState, ActionListItem, ActionQuery}, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -77,8 +77,7 @@ pub struct ListActions { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -122,8 +121,7 @@ pub struct ListFullActions { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs index 55980a2d5f..6af14acdc7 100644 --- a/client/core/rs/src/api/read/alerter.rs +++ b/client/core/rs/src/api/read/alerter.rs @@ -7,7 +7,7 @@ use crate::entities::{ alerter::{Alerter, AlerterListItem, AlerterQuery}, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -77,8 +77,7 @@ pub struct ListAlerters { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -122,8 +121,7 @@ pub struct ListFullAlerters { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs index b23264dad9..d59d324b40 100644 --- a/client/core/rs/src/api/read/build.rs +++ b/client/core/rs/src/api/read/build.rs @@ -9,7 +9,7 @@ use crate::entities::{ build::{Build, BuildActionState, BuildListItem, BuildQuery}, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -79,8 +79,7 @@ pub struct ListBuilds { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -124,8 +123,7 @@ pub struct ListFullBuilds { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs index 4b4753b41f..c4698e2555 100644 --- a/client/core/rs/src/api/read/builder.rs +++ b/client/core/rs/src/api/read/builder.rs @@ -7,7 +7,7 @@ use crate::entities::{ builder::{Builder, BuilderListItem, BuilderQuery}, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -76,8 +76,7 @@ pub struct ListBuilders { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -120,8 +119,7 @@ pub struct ListFullBuilders { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index 186618d680..43689a8551 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -15,7 +15,7 @@ use crate::entities::{ update::Log, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -86,8 +86,7 @@ pub struct ListDeployments { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -132,8 +131,7 @@ pub struct ListFullDeployments { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 497c4717c0..3111cdf69f 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -16,7 +16,7 @@ use crate::entities::{ update::Log, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -111,10 +111,11 @@ pub struct ListAllContainers { /// Set the limit for number of containers per-page. /// `limit: 100` is default. /// + /// Passing `limit: 0` returns all results (unlimited). + /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 89934b9186..4be53fdf97 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -64,9 +64,7 @@ pub trait KomodoReadRequest: HasResponse {} // -pub fn default_list_limit() -> u64 { - 100 -} +pub const DEFAULT_LIST_LIMIT: u64 = 100; // diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs index 8f87b301d6..fd41b5a8d3 100644 --- a/client/core/rs/src/api/read/procedure.rs +++ b/client/core/rs/src/api/read/procedure.rs @@ -10,7 +10,7 @@ use crate::entities::{ }, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -80,8 +80,7 @@ pub struct ListProcedures { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -125,8 +124,7 @@ pub struct ListFullProcedures { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs index 8a155ec521..35070813ee 100644 --- a/client/core/rs/src/api/read/repo.rs +++ b/client/core/rs/src/api/read/repo.rs @@ -7,7 +7,7 @@ use crate::entities::{ repo::{Repo, RepoActionState, RepoListItem, RepoQuery}, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -77,8 +77,7 @@ pub struct ListRepos { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -122,8 +121,7 @@ pub struct ListFullRepos { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index e937d077de..9dec2fb7b3 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -13,7 +13,7 @@ use crate::entities::{ }, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -83,8 +83,7 @@ pub struct ListServers { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -128,8 +127,7 @@ pub struct ListFullServers { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 2606efbdfa..205aced31c 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -14,7 +14,7 @@ use crate::entities::{ update::Log, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -124,14 +124,11 @@ pub struct ListAllStackServices { /// Set the limit for number of services per-page. /// `limit: 100` is default. /// + /// Passing `limit: 0` returns all results (unlimited). + /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_limit")] - pub limit: U64, -} - -fn default_limit() -> u64 { - 100 + pub limit: Option, } #[typeshare] @@ -431,8 +428,7 @@ pub struct ListStacks { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -476,8 +472,7 @@ pub struct ListFullStacks { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index 6f3fe75348..1475c2bfaf 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -18,7 +18,7 @@ use crate::entities::{ update::Log, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -88,8 +88,7 @@ pub struct ListSwarms { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -133,8 +132,7 @@ pub struct ListFullSwarms { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/sync.rs b/client/core/rs/src/api/read/sync.rs index 37bb977516..3a971559c2 100644 --- a/client/core/rs/src/api/read/sync.rs +++ b/client/core/rs/src/api/read/sync.rs @@ -10,7 +10,7 @@ use crate::entities::{ }, }; -use super::{KomodoReadRequest, default_list_limit}; +use super::KomodoReadRequest; // @@ -80,8 +80,7 @@ pub struct ListResourceSyncs { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] @@ -125,8 +124,7 @@ pub struct ListFullResourceSyncs { /// /// Note: the page logic relies on this being consistent /// across queries for more pages. - #[serde(default = "default_list_limit")] - pub limit: U64, + pub limit: Option, } #[typeshare] diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index acc7e7cbbe..2647f12b81 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -8672,7 +8672,7 @@ export interface ListActions { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ @@ -8693,7 +8693,7 @@ export interface ListAlerters { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -8772,10 +8772,12 @@ export interface ListAllContainers { * Set the limit for number of containers per-page. * `limit: 100` is default. * + * Passing `limit: 0` returns all results (unlimited). + * * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -8803,10 +8805,12 @@ export interface ListAllStackServices { * Set the limit for number of services per-page. * `limit: 100` is default. * + * Passing `limit: 0` returns all results (unlimited). + * * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -8862,7 +8866,7 @@ export interface ListBuilders { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builds matching optional query. Response: [ListBuildsResponse]. */ @@ -8883,7 +8887,7 @@ export interface ListBuilds { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -8963,7 +8967,7 @@ export interface ListDeployments { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ @@ -8984,7 +8988,7 @@ export interface ListFullActions { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ @@ -9005,7 +9009,7 @@ export interface ListFullAlerters { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ @@ -9025,7 +9029,7 @@ export interface ListFullBuilders { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ @@ -9046,7 +9050,7 @@ export interface ListFullBuilds { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -9070,7 +9074,7 @@ export interface ListFullDeployments { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ @@ -9091,7 +9095,7 @@ export interface ListFullProcedures { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ @@ -9112,7 +9116,7 @@ export interface ListFullRepos { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ @@ -9133,7 +9137,7 @@ export interface ListFullResourceSyncs { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ @@ -9154,7 +9158,7 @@ export interface ListFullServers { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ @@ -9175,13 +9179,28 @@ export interface ListFullStacks { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ export interface ListFullSwarms { /** optional structured query to filter swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** @@ -9310,7 +9329,7 @@ export interface ListProcedures { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List repos matching optional query. Response: [ListReposResponse]. */ @@ -9331,7 +9350,7 @@ export interface ListRepos { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ @@ -9352,7 +9371,7 @@ export interface ListResourceSyncs { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -9396,7 +9415,7 @@ export interface ListServers { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ @@ -9423,7 +9442,7 @@ export interface ListStacks { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** @@ -9495,6 +9514,21 @@ export interface ListSwarmTasks { export interface ListSwarms { /** Optional structured query to filter Swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index d58b446b15..7bcd71a73b 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -8266,7 +8266,7 @@ export interface ListActions { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { @@ -8286,7 +8286,7 @@ export interface ListAlerters { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * Get a paginated list of alerts sorted by timestamp descending. @@ -8362,10 +8362,12 @@ export interface ListAllContainers { * Set the limit for number of containers per-page. * `limit: 100` is default. * + * Passing `limit: 0` returns all results (unlimited). + * * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * List all stack services part of the target stacks. @@ -8392,10 +8394,12 @@ export interface ListAllStackServices { * Set the limit for number of services per-page. * `limit: 100` is default. * + * Passing `limit: 0` returns all results (unlimited). + * * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * Gets list of api keys for the calling user. @@ -8447,7 +8451,7 @@ export interface ListBuilders { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { @@ -8467,7 +8471,7 @@ export interface ListBuilds { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * Gets a list of existing values used as extra args across other builds. @@ -8540,7 +8544,7 @@ export interface ListDeployments { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { @@ -8560,7 +8564,7 @@ export interface ListFullActions { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ export interface ListFullAlerters { @@ -8580,7 +8584,7 @@ export interface ListFullAlerters { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ export interface ListFullBuilders { @@ -8599,7 +8603,7 @@ export interface ListFullBuilders { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ export interface ListFullBuilds { @@ -8619,7 +8623,7 @@ export interface ListFullBuilds { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * List deployments matching optional query. @@ -8642,7 +8646,7 @@ export interface ListFullDeployments { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ export interface ListFullProcedures { @@ -8662,7 +8666,7 @@ export interface ListFullProcedures { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ export interface ListFullRepos { @@ -8682,7 +8686,7 @@ export interface ListFullRepos { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ export interface ListFullResourceSyncs { @@ -8702,7 +8706,7 @@ export interface ListFullResourceSyncs { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ export interface ListFullServers { @@ -8722,7 +8726,7 @@ export interface ListFullServers { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ export interface ListFullStacks { @@ -8742,12 +8746,27 @@ export interface ListFullStacks { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ export interface ListFullSwarms { /** optional structured query to filter swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** * List git provider accounts matching optional query. @@ -8866,7 +8885,7 @@ export interface ListProcedures { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { @@ -8886,7 +8905,7 @@ export interface ListRepos { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { @@ -8906,7 +8925,7 @@ export interface ListResourceSyncs { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * List configured schedules. @@ -8947,7 +8966,7 @@ export interface ListServers { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ export interface ListStackServices { @@ -8972,7 +8991,7 @@ export interface ListStacks { * Note: the page logic relies on this being consistent * across queries for more pages. */ - limit: U64; + limit?: U64; } /** * List configs on the target Swarm. @@ -9036,6 +9055,21 @@ export interface ListSwarmTasks { export interface ListSwarms { /** Optional structured query to filter Swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** * List the processes running on the target server. diff --git a/ui/src/components/updates/resource.tsx b/ui/src/components/updates/resource.tsx index 3a1d279721..2264ffc4d1 100644 --- a/ui/src/components/updates/resource.tsx +++ b/ui/src/components/updates/resource.tsx @@ -5,11 +5,19 @@ import { useMemo } from "react"; import UpdatesSection from "./section"; export default function ResourceUpdates({ type, id }: Types.ResourceTarget) { - const deployments = useRead("ListDeployments", {}).data; + const deployment = useRead( + "GetDeployment", + { deployment: id }, + { enabled: type === "Deployment" }, + ).data; + const buildId = + deployment?.config?.image?.type === "Build" + ? deployment.config.image.params.build_id + : undefined; const query = useMemo( - () => getUpdateQuery({ type, id }, deployments), - [type, id, deployments], + () => getUpdateQuery({ type, id }, buildId), + [type, id], ); // const alerts = useRead("ListAlerts", { diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index a622bd39d5..f7b612c23f 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -169,15 +169,8 @@ export function logToHtml(log: string) { return convert_ansi.toHtml(sanitized); } -export function getUpdateQuery( - target: Types.ResourceTarget, - deployments: Types.DeploymentListItem[] | undefined, -) { - const build_id = - target.type === "Deployment" - ? deployments?.find((d) => d.id === target.id)?.info.build_id - : undefined; - if (build_id) { +export function getUpdateQuery(target: Types.ResourceTarget, buildId?: string) { + if (buildId) { return { $or: [ { @@ -186,7 +179,7 @@ export function getUpdateQuery( }, { "target.type": "Build", - "target.id": build_id, + "target.id": buildId, operation: { $in: [Types.Operation.RunBuild, Types.Operation.CancelBuild], }, From ffa10ce027757051d9004f521e7d17aae157be5f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 18:16:51 -0700 Subject: [PATCH 094/203] resource selector and omni search using pagination / search --- ui/src/app/topbar/omni-search/hooks.tsx | 5 +- ui/src/lib/hooks.ts | 102 +++++++++++++++++++++--- ui/src/pages/terminals/new.tsx | 5 +- ui/src/resources/action/index.tsx | 12 +-- ui/src/resources/alerter/index.tsx | 12 +-- ui/src/resources/build/index.tsx | 10 ++- ui/src/resources/builder/index.tsx | 12 +-- ui/src/resources/deployment/index.tsx | 12 +-- ui/src/resources/index.ts | 7 +- ui/src/resources/procedure/index.tsx | 12 +-- ui/src/resources/repo/index.tsx | 10 ++- ui/src/resources/selector.tsx | 75 ++++++++++------- ui/src/resources/server/index.tsx | 20 +++-- ui/src/resources/stack/index.tsx | 10 ++- ui/src/resources/swarm/index.tsx | 10 ++- ui/src/resources/sync/index.tsx | 12 +-- 16 files changed, 229 insertions(+), 97 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index c1aeeb214d..4e08d60bf1 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -45,11 +45,12 @@ export function useOmniSearch(): { search .toLowerCase() .split(" ") + .map((term) => term.trim()) .filter((term) => term), [search], ); - const debouncedTerms = useDebounce(searchTerms, 700); + const debouncedTerms = useDebounce(searchTerms, 500); const containersQuery: Types.ListAllContainers = useMemo( () => ({ @@ -101,7 +102,7 @@ export function useOmniSearch(): { }, [_terminals, searchTerms]); const user = useUser().data; - const resources = useAllResources(15_000); + const resources = useAllResources(debouncedTerms, 10, 15_000); const [_, setSettingsView] = useSettingsView(); const _actions = useMemo(() => { diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 035eb0fb8e..d9ada858c7 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -199,19 +199,97 @@ export type ResourceMap = { [Resource in UsableResource]: Types.ResourceListItem[] | undefined; }; -export function useAllResources(refetchInterval?: number): ResourceMap { +export function useAllResources( + terms?: string[], + limit?: number, + refetchInterval?: number, +): ResourceMap { return { - Swarm: useRead("ListSwarms", {}, { refetchInterval }).data, - Server: useRead("ListServers", {}, { refetchInterval }).data, - Stack: useRead("ListStacks", {}, { refetchInterval }).data, - Deployment: useRead("ListDeployments", {}, { refetchInterval }).data, - Build: useRead("ListBuilds", {}, { refetchInterval }).data, - Repo: useRead("ListRepos", {}, { refetchInterval }).data, - Procedure: useRead("ListProcedures", {}, { refetchInterval }).data, - Action: useRead("ListActions", {}, { refetchInterval }).data, - Builder: useRead("ListBuilders", {}, { refetchInterval }).data, - Alerter: useRead("ListAlerters", {}, { refetchInterval }).data, - ResourceSync: useRead("ListResourceSyncs", {}, { refetchInterval }).data, + Swarm: useRead( + "ListSwarms", + { + query: { terms: terms?.filter((term) => "swarm".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Server: useRead( + "ListServers", + { + query: { terms: terms?.filter((term) => "server".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Stack: useRead( + "ListStacks", + { + query: { terms: terms?.filter((term) => "stack".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Deployment: useRead( + "ListDeployments", + { + query: { terms: terms?.filter((term) => "deployment".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Build: useRead( + "ListBuilds", + { + query: { terms: terms?.filter((term) => "build".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Repo: useRead( + "ListRepos", + { + query: { terms: terms?.filter((term) => "repo".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Procedure: useRead( + "ListProcedures", + { + query: { terms: terms?.filter((term) => "procedure".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Action: useRead( + "ListActions", + { + query: { terms: terms?.filter((term) => "action".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Builder: useRead( + "ListBuilders", + { + query: { terms: terms?.filter((term) => "builder".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + Alerter: useRead( + "ListAlerters", + { + query: { terms: terms?.filter((term) => "alerter".includes(term)) }, + limit, + }, + { refetchInterval }, + ).data, + ResourceSync: useRead( + "ListResourceSyncs", + { query: { terms: terms?.filter((term) => "sync".includes(term)) } }, + { refetchInterval }, + ).data, }; } diff --git a/ui/src/pages/terminals/new.tsx b/ui/src/pages/terminals/new.tsx index 65e79b3464..b0066ffdb2 100644 --- a/ui/src/pages/terminals/new.tsx +++ b/ui/src/pages/terminals/new.tsx @@ -236,7 +236,10 @@ function CreateServerTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstServer = (useRead("ListServers", {}).data ?? [])[0]?.id ?? ""; + const firstServer = + (useRead("ListServers", {}).data?.filter( + (s) => s.info.state === Types.ServerState.Ok, + ) ?? [])[0]?.id ?? ""; const [server, _setServer] = useState(firstServer); const [changed, setChanged] = useState(false); const setServer = (server: string) => { diff --git a/ui/src/resources/action/index.tsx b/ui/src/resources/action/index.tsx index 9420864e11..1f95cd5518 100644 --- a/ui/src/resources/action/index.tsx +++ b/ui/src/resources/action/index.tsx @@ -16,9 +16,9 @@ import BatchExecutions from "@/components/batch-executions"; import { RunAction } from "./executions"; export function useAction(id: string | undefined, useName?: boolean) { - return useRead("ListActions", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListActions", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullAction(id: string) { @@ -28,9 +28,11 @@ export function useFullAction(id: string) { export const ActionComponents: RequiredResourceComponents< Types.ActionConfig, {}, - Types.ActionListItemInfo + Types.ActionListItemInfo, + Types.ActionQuerySpecifics > = { - useList: () => useRead("ListActions", {}).data, + useList: (query, limit, page) => + useRead("ListActions", { query, limit, page }).data, useListItem: useAction, useFull: useFullAction, diff --git a/ui/src/resources/alerter/index.tsx b/ui/src/resources/alerter/index.tsx index c56c633a71..25725d240e 100644 --- a/ui/src/resources/alerter/index.tsx +++ b/ui/src/resources/alerter/index.tsx @@ -11,9 +11,9 @@ import BatchExecutions from "@/components/batch-executions"; import { ConfirmButton } from "mogh_ui"; export function useAlerter(id: string | undefined, useName?: boolean) { - return useRead("ListAlerters", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListAlerters", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullAlerter(id: string) { @@ -24,9 +24,11 @@ export function useFullAlerter(id: string) { export const AlerterComponents: RequiredResourceComponents< Types.AlerterConfig, undefined, - Types.AlerterListItemInfo + Types.AlerterListItemInfo, + Types.AlerterQuerySpecifics > = { - useList: () => useRead("ListAlerters", {}).data, + useList: (query, limit, page) => + useRead("ListAlerters", { query, limit, page }).data, useListItem: useAlerter, useFull: useFullAlerter, diff --git a/ui/src/resources/build/index.tsx b/ui/src/resources/build/index.tsx index 3909d80426..5876c49042 100644 --- a/ui/src/resources/build/index.tsx +++ b/ui/src/resources/build/index.tsx @@ -21,8 +21,8 @@ import ResourceSelector from "../selector"; import { hexColorByIntention } from "mogh_ui"; export function useBuild(id: string | undefined, useName?: boolean) { - return useRead("ListBuilds", {}).data?.find((r) => - useName ? r.name === id : r.id === id, + return useRead("ListBuilds", { query: { names: id ? [id] : [] } }).data?.find( + (r) => (useName ? r.name === id : r.id === id), ); } @@ -33,9 +33,11 @@ export function useFullBuild(id: string) { export const BuildComponents: RequiredResourceComponents< Types.BuildConfig, Types.BuildInfo, - Types.BuildListItemInfo + Types.BuildListItemInfo, + Types.BuildQuerySpecifics > = { - useList: () => useRead("ListBuilds", {}).data, + useList: (query, limit, page) => + useRead("ListBuilds", { query, limit, page }).data, useListItem: useBuild, useFull: useFullBuild, diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index 07e1ddc677..3d19901c1c 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -13,9 +13,9 @@ import { useState } from "react"; import { Group, Select } from "@mantine/core"; export function useBuilder(id: string | undefined, useName?: boolean) { - return useRead("ListBuilders", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListBuilders", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullBuilder(id: string) { @@ -26,9 +26,11 @@ export function useFullBuilder(id: string) { export const BuilderComponents: RequiredResourceComponents< Types.BuilderConfig, undefined, - Types.BuilderListItemInfo + Types.BuilderListItemInfo, + Types.BuilderQuerySpecifics > = { - useList: () => useRead("ListBuilders", {}).data, + useList: (query, limit, page) => + useRead("ListBuilders", { query, limit, page }).data, useListItem: useBuilder, useFull: useFullBuilder, diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index cf5f034680..b0f9544621 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -29,9 +29,9 @@ import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; export function useDeployment(id: string | undefined, useName?: boolean) { - return useRead("ListDeployments", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListDeployments", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullDeployment(id: string) { @@ -45,9 +45,11 @@ export function useFullDeployment(id: string) { export const DeploymentComponents: RequiredResourceComponents< Types.DeploymentConfig, Types.DeploymentInfo, - Types.DeploymentListItemInfo + Types.DeploymentListItemInfo, + Types.DeploymentQuerySpecifics > = { - useList: () => useRead("ListDeployments", {}).data, + useList: (query, limit, page) => + useRead("ListDeployments", { query, limit, page }).data, useListItem: useDeployment, useFull: useFullDeployment, diff --git a/ui/src/resources/index.ts b/ui/src/resources/index.ts index 4be6caf769..66e2b02635 100644 --- a/ui/src/resources/index.ts +++ b/ui/src/resources/index.ts @@ -62,8 +62,13 @@ export interface RequiredResourceComponents< Config = any, Info = any, ListItemInfo = any, + ResourceQuerySpecifics = any, > { - useList: () => Types.ResourceListItem[] | undefined; + useList: ( + query?: Types.ResourceQuery, + limit?: number, + page?: number, + ) => Types.ResourceListItem[] | undefined; useListItem: ( id: string | undefined, useName?: boolean, diff --git a/ui/src/resources/procedure/index.tsx b/ui/src/resources/procedure/index.tsx index bbe75109cc..a8b729bf60 100644 --- a/ui/src/resources/procedure/index.tsx +++ b/ui/src/resources/procedure/index.tsx @@ -17,9 +17,9 @@ import { updateLogToHtml } from "@/lib/utils"; import { hexColorByIntention } from "mogh_ui"; export function useProcedure(id: string | undefined, useName?: boolean) { - return useRead("ListProcedures", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListProcedures", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullProcedure(id: string) { @@ -30,9 +30,11 @@ export function useFullProcedure(id: string) { export const ProcedureComponents: RequiredResourceComponents< Types.ProcedureConfig, undefined, - Types.ProcedureListItemInfo + Types.ProcedureListItemInfo, + Types.ProcedureQuerySpecifics > = { - useList: () => useRead("ListProcedures", {}).data, + useList: (query, limit, page) => + useRead("ListProcedures", { query, limit, page }).data, useListItem: useProcedure, useFull: useFullProcedure, diff --git a/ui/src/resources/repo/index.tsx b/ui/src/resources/repo/index.tsx index d60f4d8173..567f8e1147 100644 --- a/ui/src/resources/repo/index.tsx +++ b/ui/src/resources/repo/index.tsx @@ -18,8 +18,8 @@ import RepoLink from "@/components/repo-link"; import { hexColorByIntention } from "mogh_ui"; export function useRepo(id: string | undefined, useName?: boolean) { - return useRead("ListRepos", {}).data?.find((r) => - useName ? r.name === id : r.id === id, + return useRead("ListRepos", { query: { names: id ? [id] : [] } }).data?.find( + (r) => (useName ? r.name === id : r.id === id), ); } @@ -30,9 +30,11 @@ export function useFullRepo(id: string) { export const RepoComponents: RequiredResourceComponents< Types.RepoConfig, Types.RepoInfo, - Types.RepoListItemInfo + Types.RepoListItemInfo, + Types.RepoQuerySpecifics > = { - useList: () => useRead("ListRepos", {}).data, + useList: (query, limit, page) => + useRead("ListRepos", { query, limit, page }).data, useListItem: useRepo, useFull: useFullRepo, diff --git a/ui/src/resources/selector.tsx b/ui/src/resources/selector.tsx index 7bef4b19a2..e43d377378 100644 --- a/ui/src/resources/selector.tsx +++ b/ui/src/resources/selector.tsx @@ -11,11 +11,12 @@ import { InputWrapperProps, Text, } from "@mantine/core"; -import { filterBySplit } from "mogh_ui"; import { ChevronsUpDown } from "lucide-react"; import { fmtResourceType } from "@/lib/formatting"; import { ICONS } from "@/lib/icons"; -import { useSearchCombobox } from "mogh_ui"; +import { useDebounce, useSearchCombobox } from "mogh_ui"; +import { useMemo } from "react"; +import { useRead } from "@/lib/hooks"; export interface ResourceSelectorProps extends ComboboxProps { type: UsableResource; @@ -47,35 +48,48 @@ export default function ResourceSelector({ clearable = true, ...comboboxProps }: ResourceSelectorProps) { - const templateFilterFn = - templates === Types.TemplatesQueryBehavior.Exclude - ? (r: Types.ResourceListItem) => !r.template - : templates === Types.TemplatesQueryBehavior.Only - ? (r: Types.ResourceListItem) => r.template - : () => true; - const Components = ResourceComponents[type]; - const resources = Components.useList()?.filter( - (r) => - templateFilterFn(r) && - (!state || (r.info as any).state === state) && - (!excludeIds || r.id === selected || !excludeIds?.includes(r.id)), + const { search, setSearch, combobox } = useSearchCombobox(); + + const terms = useMemo( + () => + search + .split(" ") + .map((item) => item.trim()) + .filter((item) => !!item), + [search], ); - const name = resources?.find((r) => r.id === selected)?.name; - const { search, setSearch, combobox } = useSearchCombobox(); + const Components = ResourceComponents[type]; + const selectedResource = Components.useListItem(selected); - const filtered = filterBySplit(resources, search, (item) => item.name).sort( - (a, b) => { - if (a.name > b.name) { - return 1; - } else if (a.name < b.name) { - return -1; - } else { - return 0; - } - }, + const { data: __resources } = useRead(`List${type}s`, { + query: { templates, terms }, + limit: 10, + }); + + const _resources = useMemo( + () => + (__resources?.filter( + (r) => + (!state || (r.info as any).state === state) && + (!excludeIds || r.id === selected || !excludeIds?.includes(r.id)), + ) ?? []) as Array>, + [__resources], ); + // Prevent flashing when typing / fetching + const resources = useDebounce(_resources, 100); + + const name = selectedResource?.name; + + if ( + terms.length === 0 && + selectedResource && + resources.every((resource) => resource.id !== selectedResource.id) + ) { + resources.push(selectedResource); + } + const Selector = ( - {filtered.map((resource) => ( + {/* {isFetching && ( +
+ +
+ )} */} + {resources.map((resource) => ( @@ -142,7 +161,7 @@ export default function ResourceSelector({ ))} - {filtered.length === 0 && ( + {resources.length === 0 && ( No results. )}
diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index a4e5b74731..cc79773c7f 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -25,21 +25,27 @@ import ServerCpuUsage from "./stats/current/cpu"; import { HoverError } from "mogh_ui"; export function useServer(id: string | undefined, useName?: boolean) { - return useRead("ListServers", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListServers", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } -export function useFullServer(id: string) { - return useRead("GetServer", { server: id }, { refetchInterval: 30_000 }).data; +export function useFullServer(id: string | undefined) { + return useRead( + "GetServer", + { server: id! }, + { refetchInterval: 30_000, enabled: !!id }, + ).data; } export const ServerComponents: RequiredResourceComponents< Types.ServerConfig, Types.ServerInfo, - Types.ServerListItemInfo + Types.ServerListItemInfo, + Types.ServerQuerySpecifics > = { - useList: () => useRead("ListServers", {}).data, + useList: (query, limit, page) => + useRead("ListServers", { query, limit, page }).data, useListItem: useServer, useFull: useFullServer, diff --git a/ui/src/resources/stack/index.tsx b/ui/src/resources/stack/index.tsx index 592d4e9f5e..590c881cd5 100644 --- a/ui/src/resources/stack/index.tsx +++ b/ui/src/resources/stack/index.tsx @@ -37,8 +37,8 @@ import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; export function useStack(id: string | undefined, useName?: boolean) { - return useRead("ListStacks", {}).data?.find((r) => - useName ? r.name === id : r.id === id, + return useRead("ListStacks", { query: { names: id ? [id] : [] } }).data?.find( + (r) => (useName ? r.name === id : r.id === id), ); } @@ -49,9 +49,11 @@ export function useFullStack(id: string) { export const StackComponents: RequiredResourceComponents< Types.StackConfig, Types.StackInfo, - Types.StackListItemInfo + Types.StackListItemInfo, + Types.StackQuerySpecifics > = { - useList: () => useRead("ListStacks", {}).data, + useList: (query, limit, page) => + useRead("ListStacks", { query, limit, page }).data, useListItem: useStack, useFull: useFullStack, diff --git a/ui/src/resources/swarm/index.tsx b/ui/src/resources/swarm/index.tsx index 4d98a3364b..f2f086eb90 100644 --- a/ui/src/resources/swarm/index.tsx +++ b/ui/src/resources/swarm/index.tsx @@ -21,8 +21,8 @@ import { HoverError } from "mogh_ui"; import { hexColorByIntention } from "mogh_ui"; export function useSwarm(id: string | undefined, useName?: boolean) { - return useRead("ListSwarms", {}).data?.find((r) => - useName ? r.name === id : r.id === id, + return useRead("ListSwarms", { query: { names: id ? [id] : [] } }).data?.find( + (r) => (useName ? r.name === id : r.id === id), ); } @@ -33,9 +33,11 @@ export function useFullSwarm(id: string) { export const SwarmComponents: RequiredResourceComponents< Types.SwarmConfig, Types.SwarmInfo, - Types.SwarmListItemInfo + Types.SwarmListItemInfo, + Types.SwarmQuerySpecifics > = { - useList: () => useRead("ListSwarms", {}).data, + useList: (query, limit, page) => + useRead("ListSwarms", { query, limit, page }).data, useListItem: useSwarm, useFull: useFullSwarm, diff --git a/ui/src/resources/sync/index.tsx b/ui/src/resources/sync/index.tsx index ab712563fd..8739db9d93 100644 --- a/ui/src/resources/sync/index.tsx +++ b/ui/src/resources/sync/index.tsx @@ -17,9 +17,9 @@ import BatchExecutions from "@/components/batch-executions"; import { hexColorByIntention } from "mogh_ui"; export function useResourceSync(id: string | undefined, useName?: boolean) { - return useRead("ListResourceSyncs", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); + return useRead("ListResourceSyncs", { + query: { names: id ? [id] : [] }, + }).data?.find((r) => (useName ? r.name === id : r.id === id)); } export function useFullResourceSync(id: string) { @@ -30,9 +30,11 @@ export function useFullResourceSync(id: string) { export const ResourceSyncComponents: RequiredResourceComponents< Types.ResourceSyncConfig, Types.ResourceSyncInfo, - Types.ResourceSyncListItemInfo + Types.ResourceSyncListItemInfo, + Types.ResourceSyncQuerySpecifics > = { - useList: () => useRead("ListResourceSyncs", {}).data, + useList: (query, limit, page) => + useRead("ListResourceSyncs", { query, limit, page }).data, useListItem: useResourceSync, useFull: useFullResourceSync, From 54bfe10e66b068ca89d21b6e706c1f7a481b0e98 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 18:17:23 -0700 Subject: [PATCH 095/203] deploy 2.3.0-dev-27 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cd8280c32..0e2d340958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index dac88b55da..6d585555b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-26" +version = "2.3.0-dev-27" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From d9022d00117d8e97d3fe80396f1012d41dfa2b20 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 Jul 2026 23:46:55 -0700 Subject: [PATCH 096/203] update available disable action state polling in small --- ui/src/components/terminal/section.tsx | 2 +- ui/src/pages/docker/container/executions.tsx | 8 ++++---- ui/src/resources/action/executions.tsx | 2 +- ui/src/resources/deployment/executions.tsx | 12 ++++++------ .../resources/deployment/update-available.tsx | 4 ++-- ui/src/resources/procedure/executions.tsx | 2 +- ui/src/resources/repo/executions.tsx | 6 +++--- ui/src/resources/server/executions.tsx | 2 +- ui/src/resources/server/index.tsx | 17 ++++++++--------- ui/src/resources/stack/executions.tsx | 12 ++++++------ ui/src/resources/stack/update-available.tsx | 7 ++++--- ui/src/resources/sync/executions.tsx | 2 +- 12 files changed, 38 insertions(+), 38 deletions(-) diff --git a/ui/src/components/terminal/section.tsx b/ui/src/components/terminal/section.tsx index 1d83b078ad..d662171f1a 100644 --- a/ui/src/components/terminal/section.tsx +++ b/ui/src/components/terminal/section.tsx @@ -26,7 +26,7 @@ export default function TerminalSection({ target, }, { - refetchInterval: 5000, + refetchInterval: 5_000, }, ); diff --git a/ui/src/pages/docker/container/executions.tsx b/ui/src/pages/docker/container/executions.tsx index 5d85d4c37e..353cab35f5 100644 --- a/ui/src/pages/docker/container/executions.tsx +++ b/ui/src/pages/docker/container/executions.tsx @@ -26,7 +26,7 @@ const DestroyContainer = ({ const destroying = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.pruning_containers; if (!container) { @@ -61,7 +61,7 @@ const RestartContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container || state !== Types.ContainerStateStatusEnum.Running) { @@ -96,7 +96,7 @@ const StartStopContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container) { @@ -146,7 +146,7 @@ const PauseUnpauseContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container) { diff --git a/ui/src/resources/action/executions.tsx b/ui/src/resources/action/executions.tsx index 90759e5dc5..11c1669140 100644 --- a/ui/src/resources/action/executions.tsx +++ b/ui/src/resources/action/executions.tsx @@ -7,7 +7,7 @@ import { Types } from "komodo_client"; export function RunAction({ id }: { id: string }) { const running = - (useRead("GetActionActionState", { action: id }, { refetchInterval: 5000 }) + (useRead("GetActionActionState", { action: id }, { refetchInterval: 5_000 }) .data?.running ?? 0) > 0; const { mutateAsync: run, isPending: runPending } = useExecute("RunAction"); const { mutateAsync: cancel, isPending: cancelPending } = diff --git a/ui/src/resources/deployment/executions.tsx b/ui/src/resources/deployment/executions.tsx index 043c53cc36..ed12d3b0a7 100644 --- a/ui/src/resources/deployment/executions.tsx +++ b/ui/src/resources/deployment/executions.tsx @@ -101,7 +101,7 @@ export function DestroyDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.destroying; const pending = isPending || destroying; @@ -145,7 +145,7 @@ export function PullDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -172,7 +172,7 @@ export function RestartDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -204,7 +204,7 @@ export function StartStopDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -241,7 +241,7 @@ function StopDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.stopping; const pending = isPending || stopping; @@ -317,7 +317,7 @@ export function PauseUnpauseDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; diff --git a/ui/src/resources/deployment/update-available.tsx b/ui/src/resources/deployment/update-available.tsx index d53fa56093..fe0fc74c35 100644 --- a/ui/src/resources/deployment/update-available.tsx +++ b/ui/src/resources/deployment/update-available.tsx @@ -32,12 +32,12 @@ export default function DeploymentUpdateAvailable({ }, }, ); + const deploying = useRead( "GetDeploymentActionState", { deployment: id }, - { refetchInterval: 5_000 }, + { refetchInterval: 5_000, enabled: !small && canExecute }, ).data?.deploying; - const pending = isPending || deploying; const deployment = useDeployment(id); diff --git a/ui/src/resources/procedure/executions.tsx b/ui/src/resources/procedure/executions.tsx index be63f4e179..8d0a5b05a6 100644 --- a/ui/src/resources/procedure/executions.tsx +++ b/ui/src/resources/procedure/executions.tsx @@ -9,7 +9,7 @@ export function RunProcedure({ id }: { id: string }) { const running = useRead( "GetProcedureActionState", { procedure: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.running; const { mutateAsync: run, isPending: runPending } = useExecute("RunProcedure"); diff --git a/ui/src/resources/repo/executions.tsx b/ui/src/resources/repo/executions.tsx index 77f68f0b3f..741df6bceb 100644 --- a/ui/src/resources/repo/executions.tsx +++ b/ui/src/resources/repo/executions.tsx @@ -10,7 +10,7 @@ export function CloneRepo({ id }: { id: string }) { const cloning = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.cloning; const info = useRepo(id)?.info; if (!info?.server_id) return null; @@ -34,7 +34,7 @@ export function PullRepo({ id }: { id: string }) { const pulling = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.pulling; const info = useRepo(id)?.info; if (!info?.server_id) return null; @@ -59,7 +59,7 @@ export function BuildRepo({ id }: { id: string }) { const building = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.building; const updates = useRead("ListUpdates", { query: { diff --git a/ui/src/resources/server/executions.tsx b/ui/src/resources/server/executions.tsx index dad1dc00a4..d91bae6f0a 100644 --- a/ui/src/resources/server/executions.tsx +++ b/ui/src/resources/server/executions.tsx @@ -16,7 +16,7 @@ export const Prune = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const { canExecute } = usePermissions({ type: "Server", id: serverId }); diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index cc79773c7f..674e975578 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -197,7 +197,6 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, }, ).data?.core_count ?? 0; return ( @@ -223,7 +222,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; @@ -250,7 +249,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; return ( @@ -274,7 +273,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; const diskTotalGb = stats?.disks.reduce( @@ -314,7 +313,7 @@ export const ServerComponents: RequiredResourceComponents< const starting = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.starting_containers; const dontShow = useRead("ListContainers", { @@ -348,7 +347,7 @@ export const ServerComponents: RequiredResourceComponents< const restarting = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.restarting_containers; const pending = isPending || restarting; return ( @@ -372,7 +371,7 @@ export const ServerComponents: RequiredResourceComponents< const pausing = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.pausing_containers; const dontShow = useRead("ListContainers", { @@ -407,7 +406,7 @@ export const ServerComponents: RequiredResourceComponents< const unpausing = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.unpausing_containers; const dontShow = useRead("ListContainers", { @@ -439,7 +438,7 @@ export const ServerComponents: RequiredResourceComponents< const stopping = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.stopping_containers; const pending = isPending || stopping; return ( diff --git a/ui/src/resources/stack/executions.tsx b/ui/src/resources/stack/executions.tsx index 964b3dd7b2..78369f9750 100644 --- a/ui/src/resources/stack/executions.tsx +++ b/ui/src/resources/stack/executions.tsx @@ -18,7 +18,7 @@ export const DeployStack = ({ const deploying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.deploying; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -82,7 +82,7 @@ export const DestroyStack = ({ const destroying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.destroying; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -126,7 +126,7 @@ export const PullStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if ( @@ -163,7 +163,7 @@ export const RestartStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -211,7 +211,7 @@ export const StartStopStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -285,7 +285,7 @@ export const PauseUnpauseStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = diff --git a/ui/src/resources/stack/update-available.tsx b/ui/src/resources/stack/update-available.tsx index 9950ffce52..bec48c1147 100644 --- a/ui/src/resources/stack/update-available.tsx +++ b/ui/src/resources/stack/update-available.tsx @@ -40,11 +40,14 @@ export default function StackUpdateAvailable({ }, }, ); + const deploying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000, enabled: !small && canExecute }, ).data?.deploying; + const pending = isPending || deploying; + const stack = useStack(id); const fullStack = useFullStack(id); const info = stack?.info; @@ -62,8 +65,6 @@ export default function StackUpdateAvailable({ const updateAvailable = servicesWithUpdate.length > 0; - const pending = isPending || deploying; - // No quick deploy action / check for update button if (small || !canExecute) { if (!updateAvailable) { diff --git a/ui/src/resources/sync/executions.tsx b/ui/src/resources/sync/executions.tsx index 64fa0f3117..aeb6c935b6 100644 --- a/ui/src/resources/sync/executions.tsx +++ b/ui/src/resources/sync/executions.tsx @@ -43,7 +43,7 @@ export function ExecuteSync({ id }: { id: string }) { const syncing = useRead( "GetResourceSyncActionState", { sync: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.syncing; const sync = useFullResourceSync(id); const { view } = useResourceSyncTabsView(sync); From 43cc9ef9dfc1b7a08ea4108aa74562d643b14713 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 00:44:07 -0700 Subject: [PATCH 097/203] resources page support pagination --- ui/src/lib/hooks.ts | 16 -- ui/src/pages/containers.tsx | 308 +++++++++++++++--------------- ui/src/pages/dashboard/tables.tsx | 31 +-- ui/src/pages/resources.tsx | 85 ++++++--- 4 files changed, 226 insertions(+), 214 deletions(-) diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index d9ada858c7..819f452157 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -316,22 +316,6 @@ export function useCheckResourceExists() { }; } -export function useFilterResources( - resources?: Types.ResourceListItem[], - search?: string, -) { - const searchSplit = search?.toLowerCase()?.split(" ") || []; - return ( - resources?.filter((resource) => - searchSplit.length > 0 - ? searchSplit.every((search) => - resource.name.toLowerCase().includes(search), - ) - : true, - ) ?? [] - ); -} - export function usePushRecentlyViewed({ type, id }: Types.ResourceTarget) { const userInvalidate = useUserInvalidate(); diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index cf8574aa13..be91dc215c 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -18,7 +18,7 @@ export default function Containers() { const [page, setPage] = useState(0); const [selectedServers, setSelectedServers] = useState([]); - const debouncedSearch = useDebounce(search, 700); + const debouncedSearch = useDebounce(search, 500); const searchTerms = useMemo( () => debouncedSearch.split(" ").filter((term) => term), [debouncedSearch], @@ -46,161 +46,6 @@ export default function Containers() { limit: 100, }).data ?? []; - const Table = useMemo(() => { - return ( - ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "server_id", - size: 200, - sortingFn: (a, b) => { - const sa = serverName(a.original.server_id!); - const sb = serverName(b.original.server_id!); - - if (!sa && !sb) return 0; - if (!sa) return -1; - if (!sb) return 1; - - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "state", - size: 160, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const state = row.original?.state; - return ( - - ); - }, - }, - { - accessorKey: "image", - size: 300, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "networks.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => - (row.original.networks?.length ?? 0) > 0 ? ( - - {row.original.networks?.map((network) => ( - - ))} - - ) : ( - row.original.network_mode && ( - - ) - ), - }, - { - 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 }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "volumes.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - {row.original.volumes?.map((volume) => ( - - ))} - - ), - }, - ]} - /> - ); - }, [serverName, containers]); - return ( - {Table} + ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "server_id", + size: 200, + sortingFn: (a, b) => { + const sa = serverName(a.original.server_id!); + const sb = serverName(b.original.server_id!); + + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + if (sa > sb) return 1; + else if (sa < sb) return -1; + else return 0; + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "state", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + + ); + }, + }, + { + accessorKey: "image", + size: 300, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "networks.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => + (row.original.networks?.length ?? 0) > 0 ? ( + + {row.original.networks?.map((network) => ( + + ))} + + ) : ( + row.original.network_mode && ( + + ) + ), + }, + { + 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 }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "volumes.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.volumes?.map((volume) => ( + + ))} + + ), + }, + ]} + />
); diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index b177d8048d..a9b3ff7878 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -1,5 +1,5 @@ import TagsFilter from "@/components/tags/filter"; -import { useFilterResources, useRead } from "@/lib/hooks"; +import { useRead, useTagsFilter, useTemplatesQueryBehavior } from "@/lib/hooks"; import { usableResourcePath } from "@/lib/utils"; import { RequiredResourceComponents, @@ -7,10 +7,9 @@ import { UsableResource, } from "@/resources"; import { ICONS } from "@/lib/icons"; -import { Section } from "mogh_ui"; +import { Section, useDebounce } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; -import { Types } from "komodo_client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import DashboardNoResources from "./no-resources"; import { ShowHideButton } from "mogh_ui"; @@ -48,18 +47,26 @@ function TableSection({ RC: RequiredResourceComponents; search?: string; }) { - const resources = useRead(`List${type}s`, {}).data; - - const filtered = useFilterResources( - resources as Types.ResourceListItem[], - search, + const terms = useMemo( + () => + search + ?.toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term), + [search], ); - let count = filtered.length; + const tags = useTagsFilter(); + const [templates] = useTemplatesQueryBehavior(); + const _resources = + useRead(`List${type}s`, { query: { terms, tags, templates } }).data ?? []; + // Prevent flashing when typing / fetching + const resources = useDebounce(_resources, 100); const [show, setShow] = useState(true); - if (!count) return; + if (!resources.length) return; const Icon = ICONS[type]; @@ -79,7 +86,7 @@ function TableSection({ } actions={} > - {show && } + {show && }
); } diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 7868be7b55..0c34ad467b 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,7 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useFilterByUpdateAvailable, - useFilterResources, useRead, useResourceParamType, useSetTitle, @@ -11,8 +10,8 @@ import { } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; -import { Page } from "mogh_ui"; -import { Group, Stack } from "@mantine/core"; +import { Page, useDebounce } from "mogh_ui"; +import { Group, Pagination, Stack } from "@mantine/core"; import { TableSkeleton } from "mogh_ui"; import TemplateQuerySelector from "@/components/template-query-selector"; import TagsFilter from "@/components/tags/filter"; @@ -23,37 +22,46 @@ import { SearchInput } from "mogh_ui"; import { LabelledSwitch } from "mogh_ui"; export default function Resources({ _type }: { _type?: UsableResource }) { - const is_admin = useUser().data?.admin ?? false; - const disable_non_admin_create = + const isAdmin = useUser().data?.admin ?? false; + const disableNonAdminCreate = useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true; + const __type = useResourceParamType()!; const type = _type ? _type : __type; + const name = type === "ResourceSync" ? "Resource Sync" : type; useSetTitle(name + "s"); + const [search, setSearch] = useState(""); + const terms = useMemo( + () => + search + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term), + [search], + ); + const [filterUpdateAvailable, toggleFilterUpdateAvailable] = useFilterByUpdateAvailable(); + const tags = useTagsFilter(); - const query = - type === "Stack" || type === "Deployment" - ? { - tags, - query: { - specific: { update_available: filterUpdateAvailable }, - }, - } - : { tags }; - const [templatesQueryBehavior] = useTemplatesQueryBehavior(); - const resources = useRead(`List${type}s`, query).data; - const templatesFilterFn = - templatesQueryBehavior === Types.TemplatesQueryBehavior.Exclude - ? (resource: Types.ResourceListItem) => !resource.template - : templatesQueryBehavior === Types.TemplatesQueryBehavior.Only - ? (resource: Types.ResourceListItem) => resource.template - : () => true; - const filtered = useFilterResources(resources as any, search).filter( - templatesFilterFn, - ); + const [templates] = useTemplatesQueryBehavior(); + const query: Types.ResourceQuery = { + terms, + tags, + templates, + specific: + type === "Stack" || type === "Deployment" + ? { update_available: filterUpdateAvailable } + : undefined, + }; + const [page, setPage] = useState(0); + const _resources = useRead(`List${type}s`, { query, page }).data ?? []; + + // Prevent flashing when typing / fetching + const resources = useDebounce(_resources, 100); const RC = ResourceComponents[type]; @@ -61,7 +69,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) { return ; } - const targets = filtered?.map((resource) => ({ type, id: resource.id })); + const targets = resources.map((resource) => ({ type, id: resource.id })); return ( - {(is_admin || !disable_non_admin_create) && } + {(isAdmin || !disableNonAdminCreate) && } + {/* PAGINATION (only shown when needed) */} + {(resources.length >= 100 || page > 0) && ( + = 100 ? page + 2 : page + 1} + value={page + 1} + onChange={(page) => setPage(page - 1)} + > + + + + + + + + )} @@ -98,7 +121,11 @@ export default function Resources({ _type }: { _type?: UsableResource }) { - {filtered ? : } + {resources ? ( + + ) : ( + + )} ); From 2b09fc17ebdd79849e65994d065df17b4dc25f9c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 01:12:26 -0700 Subject: [PATCH 098/203] fix Image bug, improve resources page debounce --- ui/src/pages/resources.tsx | 16 ++++++++++++---- ui/src/resources/deployment/table.tsx | 26 +++++++++++--------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 0c34ad467b..03a4437ba7 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useFilterByUpdateAvailable, useRead, @@ -10,7 +10,7 @@ import { } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; -import { Page, useDebounce } from "mogh_ui"; +import { Page } from "mogh_ui"; import { Group, Pagination, Stack } from "@mantine/core"; import { TableSkeleton } from "mogh_ui"; import TemplateQuerySelector from "@/components/template-query-selector"; @@ -60,8 +60,16 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const [page, setPage] = useState(0); const _resources = useRead(`List${type}s`, { query, page }).data ?? []; - // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 100); + // Debounce: prevents flashing when typing / fetching. + // Must also set immediately upon change of resource type + const [resources, setResources] = useState(_resources); + useEffect(() => setResources(_resources), [type]); + useEffect(() => { + const handler = setTimeout(() => setResources(_resources), 100); + return () => { + clearTimeout(handler); + }; + }, [_resources]); const RC = ResourceComponents[type]; diff --git a/ui/src/resources/deployment/table.tsx b/ui/src/resources/deployment/table.tsx index 4ca57cbfbc..d858db1f27 100644 --- a/ui/src/resources/deployment/table.tsx +++ b/ui/src/resources/deployment/table.tsx @@ -1,4 +1,4 @@ -import { useRead, useResourceName, useSelectedResources } from "@/lib/hooks"; +import { useResourceName, useSelectedResources } from "@/lib/hooks"; import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; import { Group, BoxProps } from "@mantine/core"; @@ -111,21 +111,17 @@ const Image = ({ buildId: string | undefined; image: string; }) => { - const builds = useRead("ListBuilds", {}).data; if (buildId) { - const build = builds?.find((build) => build.id === buildId); - if (build) { - return ; - } else { - return undefined; - } + return ; } else { - const [img] = image.split(":"); - return ( - - - {img} - - ); + const img = image?.split(":")?.[0]; + if (img) { + return ( + + + {img} + + ); + } } }; From d50febbe585751276f1c5f02a9582d1dda1c9a89 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 01:18:18 -0700 Subject: [PATCH 099/203] fix omni missing ! --- ui/src/lib/hooks.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 819f452157..2aeb560a9b 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -208,7 +208,7 @@ export function useAllResources( Swarm: useRead( "ListSwarms", { - query: { terms: terms?.filter((term) => "swarm".includes(term)) }, + query: { terms: terms?.filter((term) => !"swarm".includes(term)) }, limit, }, { refetchInterval }, @@ -216,7 +216,7 @@ export function useAllResources( Server: useRead( "ListServers", { - query: { terms: terms?.filter((term) => "server".includes(term)) }, + query: { terms: terms?.filter((term) => !"server".includes(term)) }, limit, }, { refetchInterval }, @@ -224,7 +224,7 @@ export function useAllResources( Stack: useRead( "ListStacks", { - query: { terms: terms?.filter((term) => "stack".includes(term)) }, + query: { terms: terms?.filter((term) => !"stack".includes(term)) }, limit, }, { refetchInterval }, @@ -232,7 +232,7 @@ export function useAllResources( Deployment: useRead( "ListDeployments", { - query: { terms: terms?.filter((term) => "deployment".includes(term)) }, + query: { terms: terms?.filter((term) => !"deployment".includes(term)) }, limit, }, { refetchInterval }, @@ -240,7 +240,7 @@ export function useAllResources( Build: useRead( "ListBuilds", { - query: { terms: terms?.filter((term) => "build".includes(term)) }, + query: { terms: terms?.filter((term) => !"build".includes(term)) }, limit, }, { refetchInterval }, @@ -248,7 +248,7 @@ export function useAllResources( Repo: useRead( "ListRepos", { - query: { terms: terms?.filter((term) => "repo".includes(term)) }, + query: { terms: terms?.filter((term) => !"repo".includes(term)) }, limit, }, { refetchInterval }, @@ -256,7 +256,7 @@ export function useAllResources( Procedure: useRead( "ListProcedures", { - query: { terms: terms?.filter((term) => "procedure".includes(term)) }, + query: { terms: terms?.filter((term) => !"procedure".includes(term)) }, limit, }, { refetchInterval }, @@ -264,7 +264,7 @@ export function useAllResources( Action: useRead( "ListActions", { - query: { terms: terms?.filter((term) => "action".includes(term)) }, + query: { terms: terms?.filter((term) => !"action".includes(term)) }, limit, }, { refetchInterval }, @@ -272,7 +272,7 @@ export function useAllResources( Builder: useRead( "ListBuilders", { - query: { terms: terms?.filter((term) => "builder".includes(term)) }, + query: { terms: terms?.filter((term) => !"builder".includes(term)) }, limit, }, { refetchInterval }, @@ -280,14 +280,14 @@ export function useAllResources( Alerter: useRead( "ListAlerters", { - query: { terms: terms?.filter((term) => "alerter".includes(term)) }, + query: { terms: terms?.filter((term) => !"alerter".includes(term)) }, limit, }, { refetchInterval }, ).data, ResourceSync: useRead( "ListResourceSyncs", - { query: { terms: terms?.filter((term) => "sync".includes(term)) } }, + { query: { terms: terms?.filter((term) => !"sync".includes(term)) } }, { refetchInterval }, ).data, }; From 29fa8078e3654d6b3b3f51733340c576aa859188 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 11:36:27 -0700 Subject: [PATCH 100/203] procedure failure update log provides link to view failed update --- bin/core/src/helpers/procedure.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 8f58949f64..3155911d56 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -28,6 +28,7 @@ use crate::{ execute::{ExecuteArgs, ExecuteRequest}, write::WriteArgs, }, + config::core_config, resource::{KomodoResource, list_full_for_user_using_pattern}, state::{all_resources_cache, db_client}, }; @@ -505,9 +506,10 @@ async fn execute_execution( Ok(()) } else { Err(anyhow!( - "{}: execution not successful. see update '{}'", + "{}: Execution not successful, see update: '{}/updates/{}'", colored("ERROR", Color::Red), - bold(&update.id), + core_config().host, + update.id, )) } } From 9b75b632c471a317ad0c9a7c8aa7c1aa04bf1e76 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 13:21:33 -0700 Subject: [PATCH 101/203] fix omni search link ids with brute force query --- ui/src/app/topbar/omni-search/hooks.tsx | 49 ++-- ui/src/pages/containers.tsx | 333 +++++++++++++----------- ui/src/pages/dashboard/tables.tsx | 54 ++-- ui/src/pages/resources.tsx | 37 +-- 4 files changed, 253 insertions(+), 220 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 4e08d60bf1..1f8968890d 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -1,5 +1,4 @@ import { - ResourceMap, SettingsView, useAllResources, useRead, @@ -21,6 +20,10 @@ import { DOCKER_LINK_ICONS } from "@/components/docker/link"; import { Types } from "komodo_client"; import { hexColorByIntention, useDebounce } from "mogh_ui"; import { containerStateIntention, swarmStateIntention } from "@/lib/color"; +import { ServerComponents } from "@/resources/server"; +import { SwarmComponents } from "@/resources/swarm"; +import { StackComponents } from "@/resources/stack"; +import { DeploymentComponents } from "@/resources/deployment"; const ITEM_LIMIT = 7; let count = 0; @@ -101,6 +104,11 @@ export function useOmniSearch(): { }); }, [_terminals, searchTerms]); + const servers = ServerComponents.useList(undefined, 0); + const swarms = SwarmComponents.useList(undefined, 0); + const stacks = StackComponents.useList(undefined, 0); + const deployments = DeploymentComponents.useList(undefined, 0); + const user = useUser().data; const resources = useAllResources(debouncedTerms, 10, 15_000); const [_, setSettingsView] = useSettingsView(); @@ -217,14 +225,11 @@ export function useOmniSearch(): { ), description: info.swarm_id ? "Swarm: " + - resources.Swarm?.find( - (swarm) => info.swarm_id === swarm.id, - )?.name + swarms?.find((swarm) => info.swarm_id === swarm.id)?.name : info.server_id ? "Server: " + - resources.Server?.find( - (server) => info.server_id === server.id, - )?.name + servers?.find((server) => info.server_id === server.id) + ?.name : undefined, }; }) ?? [], @@ -239,9 +244,8 @@ export function useOmniSearch(): { label: container.name, description: "Server: " + - resources.Server?.find( - (server) => container.server_id === server.id, - )?.name, + servers?.find((server) => container.server_id === server.id) + ?.name, onClick: () => nav( `/servers/${container.server_id}/container/${container.name}`, @@ -269,8 +273,7 @@ export function useOmniSearch(): { label: service.service, description: "Stack: " + - resources.Stack?.find((stack) => service.stack_id === stack.id) - ?.name, + stacks?.find((stack) => service.stack_id === stack.id)?.name, onClick: () => nav(`/stacks/${service.stack_id}/service/${service.service}`), leftSection: , @@ -284,7 +287,12 @@ export function useOmniSearch(): { terminals?.map((terminal) => ({ id: JSON.stringify(terminal.target) + " " + terminal.name, label: terminal.name, - description: terminalTargetDescription(terminal.target, resources), + description: terminalTargetDescription( + terminal.target, + servers, + stacks, + deployments, + ), onClick: () => nav(terminalLink(terminal)), leftSection: , })) ?? [], @@ -322,35 +330,34 @@ export function useOmniSearch(): { function terminalTargetDescription( target: Types.TerminalTarget, - resources: ResourceMap, + servers: Types.ServerListItem[] | undefined, + stacks: Types.StackListItem[] | undefined, + deployments: Types.DeploymentListItem[] | undefined, ) { switch (target.type) { case "Server": return ( "Server: " + - resources.Server?.find((server) => target.params.server === server.id) - ?.name + servers?.find((server) => target.params.server === server.id)?.name ); case "Container": return ( "Server: " + - resources.Server?.find((server) => target.params.server === server.id) - ?.name + + servers?.find((server) => target.params.server === server.id)?.name + ", Container: " + target.params.container ); case "Stack": return ( "Stack: " + - resources.Stack?.find((stack) => target.params.stack === stack.id) - ?.name + + stacks?.find((stack) => target.params.stack === stack.id)?.name + ", Service: " + target.params.service ); case "Deployment": return ( "Deployment: " + - resources.Deployment?.find( + deployments?.find( (deployment) => target.params.deployment === deployment.id, )?.name ); diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index be91dc215c..fc48d92575 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -14,17 +14,22 @@ import { SearchInput } from "mogh_ui"; import TagsFilter from "@/components/tags/filter"; export default function Containers() { - const [search, setSearch] = useState(""); - const [page, setPage] = useState(0); const [selectedServers, setSelectedServers] = useState([]); - const debouncedSearch = useDebounce(search, 500); - const searchTerms = useMemo( - () => debouncedSearch.split(" ").filter((term) => term), - [debouncedSearch], - ); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 200); + const searchTerms = useMemo(() => { + // back to first page if search changes + setPage(0); + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); - const servers = useRead("ListServers", {}).data; + const servers = useRead("ListServers", { limit: 0 }).data; const serverNames = useMemo( () => servers?.map((server) => server.name) || [], [servers], @@ -37,7 +42,7 @@ export default function Containers() { const tags = useTagsFilter(); - const containers = + const _containers = useRead("ListAllContainers", { terms: searchTerms, servers: selectedServers, @@ -46,6 +51,165 @@ export default function Containers() { limit: 100, }).data ?? []; + // Prevent flashes during loading + const containers = useDebounce(_containers, 200); + + const Table = useMemo( + () => ( + ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "server_id", + size: 200, + sortingFn: (a, b) => { + const sa = serverName(a.original.server_id!); + const sb = serverName(b.original.server_id!); + + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + if (sa > sb) return 1; + else if (sa < sb) return -1; + else return 0; + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "state", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + + ); + }, + }, + { + accessorKey: "image", + size: 300, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "networks.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => + (row.original.networks?.length ?? 0) > 0 ? ( + + {row.original.networks?.map((network) => ( + + ))} + + ) : ( + row.original.network_mode && ( + + ) + ), + }, + { + 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 }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "volumes.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.volumes?.map((volume) => ( + + ))} + + ), + }, + ]} + /> + ), + [containers, serverName], + ); + return (
- ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "server_id", - size: 200, - sortingFn: (a, b) => { - const sa = serverName(a.original.server_id!); - const sb = serverName(b.original.server_id!); - - if (!sa && !sb) return 0; - if (!sa) return -1; - if (!sb) return 1; - - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "state", - size: 160, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const state = row.original?.state; - return ( - - ); - }, - }, - { - accessorKey: "image", - size: 300, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "networks.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => - (row.original.networks?.length ?? 0) > 0 ? ( - - {row.original.networks?.map((network) => ( - - ))} - - ) : ( - row.original.network_mode && ( - - ) - ), - }, - { - 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 }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "volumes.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - {row.original.volumes?.map((volume) => ( - - ))} - - ), - }, - ]} - /> + {Table} ); diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index a9b3ff7878..436d731048 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -17,6 +17,27 @@ import { SearchInput } from "mogh_ui"; export default function DashboardTables() { const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo(() => { + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); + + const Tables = useMemo( + () => + Object.entries(ResourceComponents).map(([type, RC]) => ( + + )), + [terms], + ); return ( @@ -26,14 +47,7 @@ export default function DashboardTables() { - {Object.entries(ResourceComponents).map(([type, RC]) => ( - - ))} + {Tables} ); } @@ -41,30 +55,24 @@ export default function DashboardTables() { function TableSection({ type, RC, - search, + terms, }: { type: UsableResource; RC: RequiredResourceComponents; - search?: string; + terms: string[]; }) { - const terms = useMemo( - () => - search - ?.toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [search], - ); - + const [show, setShow] = useState(true); const tags = useTagsFilter(); const [templates] = useTemplatesQueryBehavior(); const _resources = useRead(`List${type}s`, { query: { terms, tags, templates } }).data ?? []; // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 100); + const resources = useDebounce(_resources, 200); - const [show, setShow] = useState(true); + const Table = useMemo( + () => show && , + [resources, show], + ); if (!resources.length) return; @@ -86,7 +94,7 @@ function TableSection({ } actions={} > - {show && } + {Table}
); } diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 03a4437ba7..c43fb49dc6 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -10,7 +10,7 @@ import { } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; -import { Page } from "mogh_ui"; +import { Page, useDebounce } from "mogh_ui"; import { Group, Pagination, Stack } from "@mantine/core"; import { TableSkeleton } from "mogh_ui"; import TemplateQuerySelector from "@/components/template-query-selector"; @@ -32,16 +32,18 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const name = type === "ResourceSync" ? "Resource Sync" : type; useSetTitle(name + "s"); + const [page, setPage] = useState(0); const [search, setSearch] = useState(""); - const terms = useMemo( - () => - search - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [search], - ); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo(() => { + // Set to page 0 whenever search changes + setPage(0); + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); const [filterUpdateAvailable, toggleFilterUpdateAvailable] = useFilterByUpdateAvailable(); @@ -57,7 +59,6 @@ export default function Resources({ _type }: { _type?: UsableResource }) { ? { update_available: filterUpdateAvailable } : undefined, }; - const [page, setPage] = useState(0); const _resources = useRead(`List${type}s`, { query, page }).data ?? []; // Debounce: prevents flashing when typing / fetching. @@ -65,7 +66,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const [resources, setResources] = useState(_resources); useEffect(() => setResources(_resources), [type]); useEffect(() => { - const handler = setTimeout(() => setResources(_resources), 100); + const handler = setTimeout(() => setResources(_resources), 200); return () => { clearTimeout(handler); }; @@ -73,6 +74,12 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const RC = ResourceComponents[type]; + const Table = useMemo( + () => + resources && RC ? : , + [resources], + ); + if (!RC) { return ; } @@ -129,11 +136,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
- {resources ? ( - - ) : ( - - )} + {Table}
); From 64dde24c5186bf794ae15e5bb55d239e3f2719ef Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 23:22:58 -0700 Subject: [PATCH 102/203] improve command error message formatting --- lib/command/src/lib.rs | 2 +- lib/command/src/output.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index c33b6632db..c42fd23a80 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -264,7 +264,7 @@ async fn run_command( }; kill_process_group(pid); - CommandOutput::from_err(std::io::Error::other(killed_reason)) + CommandOutput::from_err_message(killed_reason) } /// Sends `SIGKILL` to the entire process group led by `pid`. diff --git a/lib/command/src/output.rs b/lib/command/src/output.rs index b2016e8bac..7403fac0c9 100644 --- a/lib/command/src/output.rs +++ b/lib/command/src/output.rs @@ -29,7 +29,15 @@ impl CommandOutput { Self { status: ExitStatus::from_raw(1), stdout: "".to_string(), - stderr: format!("{e:#?}"), + stderr: format!("{e:?}"), + } + } + + pub fn from_err_message(e: String) -> Self { + Self { + status: ExitStatus::from_raw(1), + stdout: "".to_string(), + stderr: e, } } From 4ddd346e14972fd9bc84be47093d0478a85f8608 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 8 Jul 2026 23:23:16 -0700 Subject: [PATCH 103/203] fix dashboard query for right resources --- ui/src/pages/dashboard/recents.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/dashboard/recents.tsx b/ui/src/pages/dashboard/recents.tsx index 6bd326a2c4..c672e99c40 100644 --- a/ui/src/pages/dashboard/recents.tsx +++ b/ui/src/pages/dashboard/recents.tsx @@ -38,8 +38,12 @@ export default function DashboardRecents() { function RecentRow({ type }: { type: UsableResource }) { const nav = useNavigate(); - const _recents = useUser().data?.recents?.[type]?.slice(0, 6); - const _resources = useRead(`List${type}s`, {}).data; + const _recents = useUser().data?.recents?.[type]?.slice(0, 6) ?? []; + const _resources = useRead( + `List${type}s`, + { query: { names: _recents } }, + { enabled: _recents.length > 0 }, + ).data; const recents = _recents?.filter( (recent) => !_resources?.every((resource) => resource.id !== recent), ); From 97ce46d89aaf56f493d705bdb4576421906339b3 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 12:49:57 -0700 Subject: [PATCH 104/203] attach linked resource names in addition to ids for efficient usage with pagination --- bin/core/src/api/read/terminal.rs | 4 +++ bin/core/src/monitor/mod.rs | 3 +- bin/core/src/monitor/resources.rs | 2 ++ bin/core/src/resource/deployment.rs | 36 +++++++++++++------ bin/core/src/resource/repo.rs | 10 +++++- bin/core/src/resource/stack.rs | 14 ++++++++ bin/periphery/src/api/terminal.rs | 3 ++ bin/periphery/src/docker/container.rs | 1 + bin/periphery/src/terminal.rs | 1 + client/core/rs/src/entities/deployment.rs | 6 ++++ .../core/rs/src/entities/docker/container.rs | 3 ++ client/core/rs/src/entities/repo.rs | 3 ++ client/core/rs/src/entities/stack.rs | 9 +++++ client/core/rs/src/entities/terminal.rs | 4 +++ 14 files changed, 87 insertions(+), 12 deletions(-) diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index 82ce0cb87f..a9a6ea0063 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -174,6 +174,7 @@ async fn list_all_terminals_for_user( server_id.clone() }), }; + terminal.target_name = Some(server_name.clone()); terminal }), TerminalTarget::Container { container, .. } => { @@ -186,6 +187,7 @@ async fn list_all_terminals_for_user( }, container, }; + terminal.target_name = Some(server_name.clone()); terminal }) } @@ -199,6 +201,7 @@ async fn list_all_terminals_for_user( }, service, }; + terminal.target_name = Some(s.name.clone()); terminal }) } @@ -212,6 +215,7 @@ async fn list_all_terminals_for_user( d.id.clone() }, }; + terminal.target_name = Some(d.name.clone()); terminal }, ) diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs index eb00500001..eec92b2097 100644 --- a/bin/core/src/monitor/mod.rs +++ b/bin/core/src/monitor/mod.rs @@ -193,7 +193,8 @@ pub fn refresh_server_cache( if let Some(docker) = &mut docker { docker.containers.iter_mut().for_each(|container| { - container.server_id = Some(server.id.clone()) + container.server_id = Some(server.id.clone()); + container.server_name = Some(server.name.clone()); }); } diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 6897c70072..5baab23c00 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -105,6 +105,7 @@ pub async fn update_swarm_stack_cache( StackService { stack_id: stack.id.clone(), + stack_name: stack.name.clone(), service: service_name.clone(), container: None, swarm_service, @@ -193,6 +194,7 @@ pub async fn update_server_stack_cache( StackService { stack_id: stack.id.clone(), + stack_name: stack.name.clone(), service: service_name.clone(), image: image.clone(), container, diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index be0aebeabd..ecf18cd56f 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -34,7 +34,10 @@ use crate::{ swarm::swarm_request, }, monitor::{refresh_server_cache, refresh_swarm_cache}, - state::{action_states, db_client, deployment_status_cache}, + state::{ + action_states, all_resources_cache, db_client, + deployment_status_cache, + }, }; use super::get_check_permissions; @@ -104,17 +107,28 @@ impl super::KomodoResource for Deployment { } else { status.as_ref().map(|s| s.curr.state).unwrap_or_default() }; + let all = all_resources_cache().load(); + let server_name = all + .servers + .get(&deployment.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); + let swarm_name = all + .swarms + .get(&deployment.config.swarm_id) + .map(|swarm| swarm.name.clone()) + .unwrap_or_default(); let (build_image, build_id) = match deployment.config.image { DeploymentImage::Build { build_id, version } => { - let (build_name, build_id, build_version) = - super::get::(&build_id) - .await - .map(|b| (b.name, b.id, b.config.version)) - .unwrap_or(( - String::from("unknown"), - String::new(), - Default::default(), - )); + let (build_name, build_id, build_version) = all + .builds + .get(&build_id) + .map(|b| (b.name.clone(), b.id.clone(), b.config.version)) + .unwrap_or(( + String::from("unknown"), + String::new(), + Default::default(), + )); let version = if version.is_none() { build_version.to_string() } else { @@ -171,7 +185,9 @@ impl super::KomodoResource for Deployment { image, update_available, swarm_id: deployment.config.swarm_id, + swarm_name, server_id: deployment.config.server_id, + server_name, build_id, }, } diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 940b16204d..2774e3c105 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -26,7 +26,8 @@ use crate::{ config::core_config, helpers::{periphery_client, repo_link}, state::{ - action_states, db_client, repo_state_cache, repo_status_cache, + action_states, all_resources_cache, db_client, repo_state_cache, + repo_status_cache, }, }; @@ -63,6 +64,12 @@ impl super::KomodoResource for Repo { let state = get_repo_state(&repo.id).await; let status = repo_status_cache().get(&repo.id).await.unwrap_or_default(); + let server_name = all_resources_cache() + .load() + .servers + .get(&repo.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); RepoListItem { name: repo.name, id: repo.id, @@ -71,6 +78,7 @@ impl super::KomodoResource for Repo { resource_type: ResourceTargetVariant::Repo, info: RepoListItemInfo { server_id: repo.config.server_id, + server_name, builder_id: repo.config.builder_id, last_pulled_at: repo.info.last_pulled_at, last_built_at: repo.info.last_built_at, diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index ac7e9f99e7..2f6abe2b73 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -205,6 +205,18 @@ impl super::KomodoResource for Stack { (false, None) }; + let all = all_resources_cache().load(); + let server_name = all + .servers + .get(&stack.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); + let swarm_name = all + .swarms + .get(&stack.config.swarm_id) + .map(|swarm| swarm.name.clone()) + .unwrap_or_default(); + StackListItem { name: stack.name, id: stack.id, @@ -218,7 +230,9 @@ impl super::KomodoResource for Stack { project_missing, file_contents: !stack.config.file_contents.is_empty(), swarm_id: stack.config.swarm_id, + swarm_name, server_id: stack.config.server_id, + server_name, linked_repo: stack.config.linked_repo, missing_files: stack.info.missing_files, files_on_host: stack.config.files_on_host, diff --git a/bin/periphery/src/api/terminal.rs b/bin/periphery/src/api/terminal.rs index 6a1d44cf30..34a8d84370 100644 --- a/bin/periphery/src/api/terminal.rs +++ b/bin/periphery/src/api/terminal.rs @@ -74,6 +74,7 @@ impl Resolve for CreateServerTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: TerminalTarget::Server { server: None }, + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, @@ -131,6 +132,7 @@ impl Resolve for CreateContainerExecTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, @@ -185,6 +187,7 @@ impl Resolve for CreateContainerAttachTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, diff --git a/bin/periphery/src/docker/container.rs b/bin/periphery/src/docker/container.rs index 0ac4fc128a..5c111142da 100644 --- a/bin/periphery/src/docker/container.rs +++ b/bin/periphery/src/docker/container.rs @@ -37,6 +37,7 @@ impl DockerClient { let stats = stats.get(&name).cloned(); anyhow::Ok(ContainerListItem { server_id: None, + server_name: None, name, stats, id: container.id, diff --git a/bin/periphery/src/terminal.rs b/bin/periphery/src/terminal.rs index 445748e156..bfeea7e111 100644 --- a/bin/periphery/src/terminal.rs +++ b/bin/periphery/src/terminal.rs @@ -185,6 +185,7 @@ pub async fn list_terminals( .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index 8e3c2eecab..a9bacb5e8c 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -57,8 +57,14 @@ pub struct DeploymentListItemInfo { pub update_available: bool, /// The swarm that deployment is deployed on, when in Swarm mode. pub swarm_id: String, + /// The name of the swarm that deployment is deployed on, when in Swarm mode. + #[serde(default)] + pub swarm_name: String, /// The server that deployment is deployed on, when in Server mode. pub server_id: String, + /// The name of the server that deployment is deployed on, when in Server mode. + #[serde(default)] + pub server_name: String, /// An attached Komodo Build, if it exists. pub build_id: Option, } diff --git a/client/core/rs/src/entities/docker/container.rs b/client/core/rs/src/entities/docker/container.rs index aa2c9a033f..719e921129 100644 --- a/client/core/rs/src/entities/docker/container.rs +++ b/client/core/rs/src/entities/docker/container.rs @@ -22,6 +22,9 @@ pub struct ContainerListItem { /// The Server which hosts the container. #[serde(skip_serializing_if = "Option::is_none")] pub server_id: Option, + /// The name of the Server which hosts the container. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_name: Option, /// The first name in Names, not including the initial '/' pub name: String, /// The ID of this container diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs index 3c48af4478..47a83825d1 100644 --- a/client/core/rs/src/entities/repo.rs +++ b/client/core/rs/src/entities/repo.rs @@ -29,6 +29,9 @@ pub type RepoListItem = ResourceListItem; pub struct RepoListItemInfo { /// The server that repo sits on. pub server_id: String, + /// The name of the server that repo sits on. + #[serde(default)] + pub server_name: String, /// The builder that builds the repo. pub builder_id: String, /// Repo last cloned / pulled timestamp in ms. diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index b0160cd030..6a16c4ee21 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -151,8 +151,14 @@ pub type StackListItem = ResourceListItem; pub struct StackListItemInfo { /// The swarm that stack is deployed on, when in Swarm mode. pub swarm_id: String, + /// The name of the swarm that stack is deployed on, when in Swarm mode. + #[serde(default)] + pub swarm_name: String, /// The server that stack is deployed on, when in Server mode. pub server_id: String, + /// The name of the server that stack is deployed on, when in Server mode. + #[serde(default)] + pub server_name: String, /// Whether stack is using files on host mode pub files_on_host: bool, /// Whether stack has file contents defined. @@ -815,6 +821,9 @@ pub struct StackServiceNames { pub struct StackService { /// The stack which the service is a part of. pub stack_id: String, + /// The name of the stack which the service is a part of. + #[serde(default)] + pub stack_name: String, /// The service name pub service: String, /// The service image diff --git a/client/core/rs/src/entities/terminal.rs b/client/core/rs/src/entities/terminal.rs index 7f5caeaed6..19def3958e 100644 --- a/client/core/rs/src/entities/terminal.rs +++ b/client/core/rs/src/entities/terminal.rs @@ -16,6 +16,10 @@ pub struct Terminal { pub name: String, /// The target resource of the Terminal. pub target: TerminalTarget, + /// The name of the target resource (Server / Stack / Deployment). + /// Resolved by Core when listing all terminals for a user. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_name: Option, /// The command used to init the shell. pub command: String, /// The size of the terminal history in memory. From 7fd099e3b27db931edb3e6f2f23c75c37898af5e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 13:10:03 -0700 Subject: [PATCH 105/203] fix pagination bugs and improve pagination efficiency --- client/core/ts/src/types.ts | 19 +++ ui/public/client/types.d.ts | 19 +++ ui/src/app/topbar/omni-search/hooks.tsx | 79 +++------ ui/src/app/topbar/omni-search/index.tsx | 8 +- ui/src/components/export-toml.tsx | 28 +++- ui/src/components/list-pagination.tsx | 35 ++++ ui/src/lib/hooks.ts | 152 +++++++++++++----- ui/src/pages/containers.tsx | 44 +++-- ui/src/pages/dashboard/tables.tsx | 31 +++- ui/src/pages/resource.tsx | 6 +- ui/src/pages/resources.tsx | 34 ++-- ui/src/resources/action/index.tsx | 9 +- ui/src/resources/alerter/config/resources.tsx | 2 +- ui/src/resources/alerter/index.tsx | 9 +- ui/src/resources/build/index.tsx | 15 +- ui/src/resources/builder/index.tsx | 12 +- ui/src/resources/deployment/executions.tsx | 8 +- ui/src/resources/deployment/index.tsx | 10 +- ui/src/resources/deployment/table.tsx | 13 +- ui/src/resources/new.tsx | 5 +- ui/src/resources/procedure/index.tsx | 9 +- ui/src/resources/repo/index.tsx | 9 +- ui/src/resources/server/index.tsx | 10 +- ui/src/resources/stack/index.tsx | 14 +- ui/src/resources/stack/table.tsx | 13 +- ui/src/resources/swarm/index.tsx | 9 +- ui/src/resources/sync/index.tsx | 10 +- ui/src/resources/tags.tsx | 4 +- 28 files changed, 372 insertions(+), 244 deletions(-) create mode 100644 ui/src/components/list-pagination.tsx diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 2647f12b81..8b71bbf870 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1467,8 +1467,12 @@ export interface DeploymentListItemInfo { update_available: boolean; /** The swarm that deployment is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that deployment is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that deployment is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that deployment is deployed on, when in Server mode. */ + server_name?: string; /** An attached Komodo Build, if it exists. */ build_id?: string; } @@ -4958,6 +4962,8 @@ export interface Port { export interface ContainerListItem { /** The Server which hosts the container. */ server_id?: string; + /** The name of the Server which hosts the container. */ + server_name?: string; /** The first name in Names, not including the initial '/' */ name: string; /** The ID of this container */ @@ -5031,6 +5037,8 @@ export enum StackServiceState { export interface StackService { /** The stack which the service is a part of. */ stack_id: string; + /** The name of the stack which the service is a part of. */ + stack_name?: string; /** The service name */ service: string; /** The service image */ @@ -5295,6 +5303,8 @@ export enum RepoState { export interface RepoListItemInfo { /** The server that repo sits on. */ server_id: string; + /** The name of the server that repo sits on. */ + server_name?: string; /** The builder that builds the repo. */ builder_id: string; /** Repo last cloned / pulled timestamp in ms. */ @@ -5499,8 +5509,12 @@ export enum StackState { export interface StackListItemInfo { /** The swarm that stack is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that stack is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that stack is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that stack is deployed on, when in Server mode. */ + server_name?: string; /** Whether stack is using files on host mode */ files_on_host: boolean; /** Whether stack has file contents defined. */ @@ -5716,6 +5730,11 @@ export interface Terminal { name: string; /** The target resource of the Terminal. */ target: TerminalTarget; + /** + * The name of the target resource (Server / Stack / Deployment). + * Resolved by Core when listing all terminals for a user. + */ + target_name?: string; /** The command used to init the shell. */ command: string; /** The size of the terminal history in memory. */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 7bcd71a73b..df2860bf09 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -1611,8 +1611,12 @@ export interface DeploymentListItemInfo { update_available: boolean; /** The swarm that deployment is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that deployment is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that deployment is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that deployment is deployed on, when in Server mode. */ + server_name?: string; /** An attached Komodo Build, if it exists. */ build_id?: string; } @@ -4902,6 +4906,8 @@ export interface Port { export interface ContainerListItem { /** The Server which hosts the container. */ server_id?: string; + /** The name of the Server which hosts the container. */ + server_name?: string; /** The first name in Names, not including the initial '/' */ name: string; /** The ID of this container */ @@ -4972,6 +4978,8 @@ export declare enum StackServiceState { export interface StackService { /** The stack which the service is a part of. */ stack_id: string; + /** The name of the stack which the service is a part of. */ + stack_name?: string; /** The service name */ service: string; /** The service image */ @@ -5192,6 +5200,8 @@ export declare enum RepoState { export interface RepoListItemInfo { /** The server that repo sits on. */ server_id: string; + /** The name of the server that repo sits on. */ + server_name?: string; /** The builder that builds the repo. */ builder_id: string; /** Repo last cloned / pulled timestamp in ms. */ @@ -5378,8 +5388,12 @@ export declare enum StackState { export interface StackListItemInfo { /** The swarm that stack is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that stack is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that stack is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that stack is deployed on, when in Server mode. */ + server_name?: string; /** Whether stack is using files on host mode */ files_on_host: boolean; /** Whether stack has file contents defined. */ @@ -5582,6 +5596,11 @@ export interface Terminal { name: string; /** The target resource of the Terminal. */ target: TerminalTarget; + /** + * The name of the target resource (Server / Stack / Deployment). + * Resolved by Core when listing all terminals for a user. + */ + target_name?: string; /** The command used to init the shell. */ command: string; /** The size of the terminal history in memory. */ diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 1f8968890d..7c2ebf2ad9 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -20,15 +20,11 @@ import { DOCKER_LINK_ICONS } from "@/components/docker/link"; import { Types } from "komodo_client"; import { hexColorByIntention, useDebounce } from "mogh_ui"; import { containerStateIntention, swarmStateIntention } from "@/lib/color"; -import { ServerComponents } from "@/resources/server"; -import { SwarmComponents } from "@/resources/swarm"; -import { StackComponents } from "@/resources/stack"; -import { DeploymentComponents } from "@/resources/deployment"; const ITEM_LIMIT = 7; let count = 0; -export function useOmniSearch(): { +export function useOmniSearch(opened: boolean): { search: string; setSearch: (value: string) => void; actions: SpotlightActionGroupData[]; @@ -68,8 +64,8 @@ export function useOmniSearch(): { const containers = useRead("ListAllContainers", containersQuery, { refetchInterval: 15_000, - // Only fetch when there is query typed - enabled: !!debouncedTerms.length, + // Only fetch when open and there is query typed + enabled: opened && !!debouncedTerms.length, }).data; const servicesQuery: Types.ListAllStackServices = useMemo( @@ -85,14 +81,14 @@ export function useOmniSearch(): { const services = useRead("ListAllStackServices", servicesQuery, { refetchInterval: 15_000, - // Only fetch when there is query typed - enabled: !!debouncedTerms.length, + // Only fetch when open and there is query typed + enabled: opened && !!debouncedTerms.length, }).data; const _terminals = useRead( "ListTerminals", {}, - { refetchInterval: 15_000 }, + { refetchInterval: 15_000, enabled: opened }, ).data; const terminals = useMemo(() => { return _terminals?.filter((c) => { @@ -104,13 +100,8 @@ export function useOmniSearch(): { }); }, [_terminals, searchTerms]); - const servers = ServerComponents.useList(undefined, 0); - const swarms = SwarmComponents.useList(undefined, 0); - const stacks = StackComponents.useList(undefined, 0); - const deployments = DeploymentComponents.useList(undefined, 0); - const user = useUser().data; - const resources = useAllResources(debouncedTerms, 10, 15_000); + const resources = useAllResources(debouncedTerms, 10, 15_000, opened); const [_, setSettingsView] = useSettingsView(); const _actions = useMemo(() => { @@ -209,8 +200,10 @@ export function useOmniSearch(): { }) .map((resource) => { const info = resource.info as { - swarm_id: string; - server_id: string; + swarm_id?: string; + swarm_name?: string; + server_id?: string; + server_name?: string; }; return { id: type + " " + resource.name, @@ -224,12 +217,9 @@ export function useOmniSearch(): { ), description: info.swarm_id - ? "Swarm: " + - swarms?.find((swarm) => info.swarm_id === swarm.id)?.name + ? "Swarm: " + info.swarm_name : info.server_id - ? "Server: " + - servers?.find((server) => info.server_id === server.id) - ?.name + ? "Server: " + info.server_name : undefined, }; }) ?? [], @@ -242,10 +232,7 @@ export function useOmniSearch(): { containers?.map((container) => ({ id: container.server_id ?? "" + " " + container.name, label: container.name, - description: - "Server: " + - servers?.find((server) => container.server_id === server.id) - ?.name, + description: "Server: " + container.server_name, onClick: () => nav( `/servers/${container.server_id}/container/${container.name}`, @@ -271,9 +258,7 @@ export function useOmniSearch(): { return { id: service.stack_id + " " + service.service, label: service.service, - description: - "Stack: " + - stacks?.find((stack) => service.stack_id === stack.id)?.name, + description: "Stack: " + service.stack_name, onClick: () => nav(`/stacks/${service.stack_id}/service/${service.service}`), leftSection: , @@ -289,16 +274,14 @@ export function useOmniSearch(): { label: terminal.name, description: terminalTargetDescription( terminal.target, - servers, - stacks, - deployments, + terminal.target_name, ), onClick: () => nav(terminalLink(terminal)), leftSection: , })) ?? [], }, ]; - }, [resources]); + }, [resources, containers, services, terminals, searchTerms]); // LIMIT the action count for performance. // Reset count on render before creating actual actions. @@ -330,36 +313,18 @@ export function useOmniSearch(): { function terminalTargetDescription( target: Types.TerminalTarget, - servers: Types.ServerListItem[] | undefined, - stacks: Types.StackListItem[] | undefined, - deployments: Types.DeploymentListItem[] | undefined, + target_name: string | undefined, ) { switch (target.type) { case "Server": - return ( - "Server: " + - servers?.find((server) => target.params.server === server.id)?.name - ); + return "Server: " + target_name; case "Container": return ( - "Server: " + - servers?.find((server) => target.params.server === server.id)?.name + - ", Container: " + - target.params.container + "Server: " + target_name + ", Container: " + target.params.container ); case "Stack": - return ( - "Stack: " + - stacks?.find((stack) => target.params.stack === stack.id)?.name + - ", Service: " + - target.params.service - ); + return "Stack: " + target_name + ", Service: " + target.params.service; case "Deployment": - return ( - "Deployment: " + - deployments?.find( - (deployment) => target.params.deployment === deployment.id, - )?.name - ); + return "Deployment: " + target_name; } } diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index 1fa594a774..1c7c98ced8 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -5,9 +5,13 @@ import { ICONS } from "@/lib/icons"; import { useShiftKeyListener } from "mogh_ui"; import classes from "./index.module.scss"; import { useLocalStorage } from "@mantine/hooks"; +import { useState } from "react"; export default function OmniSearch({}: {}) { - const { search, setSearch, actions } = useOmniSearch(); + // Gates all omni search queries, so nothing is fetched / polled + // until the spotlight is actually opened. + const [opened, setOpened] = useState(false); + const { search, setSearch, actions } = useOmniSearch(opened); useShiftKeyListener("S", () => spotlight.open()); const [clearOnClose, setClearOnClose] = useLocalStorage({ key: "omnisearch.clearOnClose", @@ -45,6 +49,8 @@ export default function OmniSearch({}: {}) { query={search} onQueryChange={setSearch} clearQueryOnClose={clearOnClose} + onSpotlightOpen={() => setOpened(true)} + onSpotlightClose={() => setOpened(false)} styles={{ content: { borderRadius: "var(--mantine-radius-sm)" } }} > ; + }; userGroups?: string[]; tags?: string[]; includeVariables?: boolean; @@ -37,20 +47,32 @@ export default function ExportToml(props: ExportTomlProps) { function ExportTomlInner({ targets, + listQuery, userGroups, tags, includeVariables, }: ExportTomlProps) { - const useAll = !(targets || userGroups || includeVariables); + const useAll = !(targets || listQuery || userGroups || includeVariables); + + const listTargets = useRead( + `List${listQuery?.type ?? "Server"}s`, + { query: listQuery?.query, limit: 0 }, + { enabled: !!listQuery }, + ).data?.map( + (resource) => + ({ type: listQuery!.type, id: resource.id }) as Types.ResourceTarget, + ); + + const exportTargets = listQuery ? listTargets : targets; const { data: resourcesData, isPending: resourcesPending } = useRead( "ExportResourcesToToml", { - targets: targets ? targets : [], + targets: exportTargets ? exportTargets : [], user_groups: userGroups ? userGroups : [], include_variables: includeVariables, }, - { enabled: !useAll }, + { enabled: !useAll && (!listQuery || !!listTargets) }, ); const { data: allData, isPending: allPending } = useRead( diff --git a/ui/src/components/list-pagination.tsx b/ui/src/components/list-pagination.tsx new file mode 100644 index 0000000000..88178c34ea --- /dev/null +++ b/ui/src/components/list-pagination.tsx @@ -0,0 +1,35 @@ +import { Group, Pagination } from "@mantine/core"; + +/** Matches the server side default page size on `List` calls. */ +export const RESOURCE_PAGE_SIZE = 100; + +/** + * Pagination controls for the paginated `List` calls. + * Only renders when needed, ie the results fill the page + * or the user is past the first page. + */ +export default function ListPagination({ + page, + setPage, + count, +}: { + page: number; + setPage: (page: number) => void; + count: number; +}) { + if (count < RESOURCE_PAGE_SIZE && page === 0) return null; + return ( + = RESOURCE_PAGE_SIZE ? page + 2 : page + 1} + value={page + 1} + onChange={(page) => setPage(page - 1)} + > + + + + + + + + ); +} diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 2aeb560a9b..62ed6070d0 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -12,7 +12,7 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { atom, useAtom } from "jotai"; import { atomFamily } from "jotai-family"; import { atomWithHash } from "jotai-location"; @@ -180,14 +180,6 @@ export function atomWithStorage(key: string, init: T) { ); } -export function useResourceName(type: UsableResource) { - const resources = useRead(`List${type}s`, {}).data; - return useCallback( - (id: string) => resources?.find((resource) => resource.id === id)?.name, - [resources], - ); -} - export function useResourceParamType() { const type = useParams().type; if (!type) return undefined; @@ -195,6 +187,100 @@ export function useResourceParamType() { return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource; } +// ============== BATCHED LIST ITEM LOOKUP ============== + +/** + * All list item lookups mounting within the window (eg. the rows of a + * rendering table) are combined into a single `Lists` request. + */ +const LIST_ITEM_BATCH_MS = 10; + +/** Matches the server side ObjectId parse used to split `query.names` into ids / names. */ +const OBJECT_ID_REGEX = /^[0-9a-f]{24}$/i; + +type ListItemBatch = { + names: Set; + promise: Promise>>; +}; + +const listItemBatches = new Map(); + +function batchedListItemFetch( + type: UsableResource, + name: string, +): Promise[]> { + // The server ANDs the id filter / name filter it extracts from + // `query.names`, so id lookups must be batched separately from + // name lookups to avoid empty intersections. + const bucket = `${type}:${OBJECT_ID_REGEX.test(name) ? "id" : "name"}`; + let batch = listItemBatches.get(bucket); + if (!batch) { + const names = new Set(); + const promise = new Promise[]>( + (resolve, reject) => { + setTimeout(() => { + listItemBatches.delete(bucket); + komodo_client() + .read(`List${type}s`, { + query: { names: [...names] }, + // limit: 0 so large batches aren't truncated to the default page size + limit: 0, + }) + .then(resolve, reject); + }, LIST_ITEM_BATCH_MS); + }, + ).then((resources) => { + const map = new Map>(); + for (const resource of resources) { + map.set(resource.id, resource); + map.set(resource.name, resource); + } + return map; + }); + batch = { names, promise }; + listItemBatches.set(bucket, batch); + } + batch.names.add(name); + return batch.promise.then((map) => { + const resource = map.get(name); + return resource ? [resource] : []; + }); +} + +/** + * The underlying query for [useListItem]. The query key mirrors the + * plain `useRead("Lists", { query: { names: [id] } })` call, so cache + * entries are shared and `useInvalidate(["Lists"])` style prefix + * invalidations reach these too. + */ +export function useListItemQuery( + type: UsableResource, + id: string | undefined, +) { + const hasJwt = !!MoghAuth.LOGIN_TOKENS.jwt(); + return useQuery({ + queryKey: [`List${type}s`, { query: { names: id ? [id] : [] } }], + queryFn: () => batchedListItemFetch(type, id!), + enabled: hasJwt && !!id, + }); +} + +/** + * Look up a single resource list item by id (or name, with `useName`). + * Concurrent lookups are transparently batched into a single + * `Lists` call per resource type, instead of a request per id. + */ +export function useListItem( + type: T, + id: string | undefined, + useName?: boolean, +): ReadResponses[`List${T}s`][number] | undefined { + const { data } = useListItemQuery(type, id); + return (data as Types.ResourceListItem[] | undefined)?.find((r) => + useName ? r.name === id : r.id === id, + ) as ReadResponses[`List${T}s`][number] | undefined; +} + export type ResourceMap = { [Resource in UsableResource]: Types.ResourceListItem[] | undefined; }; @@ -203,6 +289,7 @@ export function useAllResources( terms?: string[], limit?: number, refetchInterval?: number, + enabled?: boolean, ): ResourceMap { return { Swarm: useRead( @@ -211,7 +298,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"swarm".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Server: useRead( "ListServers", @@ -219,7 +306,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"server".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Stack: useRead( "ListStacks", @@ -227,7 +314,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"stack".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Deployment: useRead( "ListDeployments", @@ -235,7 +322,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"deployment".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Build: useRead( "ListBuilds", @@ -243,7 +330,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"build".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Repo: useRead( "ListRepos", @@ -251,7 +338,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"repo".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Procedure: useRead( "ListProcedures", @@ -259,7 +346,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"procedure".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Action: useRead( "ListActions", @@ -267,7 +354,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"action".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Builder: useRead( "ListBuilders", @@ -275,7 +362,7 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"builder".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, Alerter: useRead( "ListAlerters", @@ -283,12 +370,15 @@ export function useAllResources( query: { terms: terms?.filter((term) => !"alerter".includes(term)) }, limit, }, - { refetchInterval }, + { refetchInterval, enabled }, ).data, ResourceSync: useRead( "ListResourceSyncs", - { query: { terms: terms?.filter((term) => !"sync".includes(term)) } }, - { refetchInterval }, + { + query: { terms: terms?.filter((term) => !"sync".includes(term)) }, + limit, + }, + { refetchInterval, enabled }, ).data, }; } @@ -304,18 +394,6 @@ export function useNoResources() { return true; } -/** returns function that takes a resource target and checks if it exists */ -export function useCheckResourceExists() { - const resources = useAllResources(); - return (target: Types.ResourceTarget) => { - return ( - resources[target.type as UsableResource]?.some( - (resource) => resource.id === target.id, - ) || false - ); - }; -} - export function usePushRecentlyViewed({ type, id }: Types.ResourceTarget) { const userInvalidate = useUserInvalidate(); @@ -323,11 +401,7 @@ export function usePushRecentlyViewed({ type, id }: Types.ResourceTarget) { onSuccess: userInvalidate, }).mutate; - const exists = useRead(`List${type as UsableResource}s`, {}).data?.find( - (r) => r.id === id, - ) - ? true - : false; + const exists = useListItem(type as UsableResource, id) ? true : false; useEffect(() => { exists && push({ resource: { type, id } }); @@ -594,7 +668,7 @@ export function useUserTargetPermissions(user_target: Types.UserTarget) { const permissions = useRead("ListUserTargetPermissions", { user_target, }).data; - const allResources = useAllResources(); + const allResources = useAllResources(undefined, 0); const perms: (Types.Permission & { name: string })[] = []; for (const [resource_type, resources] of Object.entries(allResources)) { addUserTargetPermissions( diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index fc48d92575..9ca7d26699 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -6,12 +6,15 @@ import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; -import { Group, MultiSelect, Pagination, Stack } from "@mantine/core"; -import { useCallback, useMemo, useState } from "react"; +import { Group, MultiSelect, Stack } from "@mantine/core"; +import { useEffect, useMemo, useState } from "react"; import { DividedChildren } from "mogh_ui"; import ResourceLink from "@/resources/link"; import { SearchInput } from "mogh_ui"; import TagsFilter from "@/components/tags/filter"; +import ListPagination, { + RESOURCE_PAGE_SIZE, +} from "@/components/list-pagination"; export default function Containers() { const [selectedServers, setSelectedServers] = useState([]); @@ -35,20 +38,21 @@ export default function Containers() { [servers], ); - const serverName = useCallback( - (id: string) => servers?.find((server) => server.id === id)?.name, - [servers], - ); - const tags = useTagsFilter(); + // Set to page 0 whenever any filter changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [selectedServers, tags]); + const _containers = useRead("ListAllContainers", { terms: searchTerms, servers: selectedServers, tags, page, - limit: 100, + limit: RESOURCE_PAGE_SIZE, }).data ?? []; // Prevent flashes during loading @@ -78,8 +82,8 @@ export default function Containers() { accessorKey: "server_id", size: 200, sortingFn: (a, b) => { - const sa = serverName(a.original.server_id!); - const sb = serverName(b.original.server_id!); + const sa = a.original.server_name; + const sb = b.original.server_name; if (!sa && !sb) return 0; if (!sa) return -1; @@ -207,7 +211,7 @@ export default function Containers() { ]} /> ), - [containers, serverName], + [containers], ); return ( @@ -227,19 +231,11 @@ export default function Containers() { searchable clearable /> - {/* PAGINATION */} - = 100 ? page + 2 : page + 1} - value={page + 1} - onChange={(page) => setPage(page - 1)} - > - - - - - - - + diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 436d731048..5359ee9267 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -9,7 +9,8 @@ import { import { ICONS } from "@/lib/icons"; import { Section, useDebounce } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import ListPagination from "@/components/list-pagination"; import { Link } from "react-router-dom"; import DashboardNoResources from "./no-resources"; import { ShowHideButton } from "mogh_ui"; @@ -64,8 +65,17 @@ function TableSection({ const [show, setShow] = useState(true); const tags = useTagsFilter(); const [templates] = useTemplatesQueryBehavior(); + + const [page, setPage] = useState(0); + // Set to page 0 whenever any filter changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, tags, templates]); + const _resources = - useRead(`List${type}s`, { query: { terms, tags, templates } }).data ?? []; + useRead(`List${type}s`, { query: { terms, tags, templates }, page }).data ?? + []; // Prevent flashing when typing / fetching const resources = useDebounce(_resources, 200); @@ -74,7 +84,9 @@ function TableSection({ [resources, show], ); - if (!resources.length) return; + // Keep the section visible on empty pages past the first, + // so the pagination controls remain available to navigate back. + if (!resources.length && page === 0) return; const Icon = ICONS[type]; @@ -92,7 +104,18 @@ function TableSection({ {type + "s"} } - actions={} + actions={ + + {show && ( + + )} + + + } > {Table} diff --git a/ui/src/pages/resource.tsx b/ui/src/pages/resource.tsx index 4a48f1993b..ef77e51d01 100644 --- a/ui/src/pages/resource.tsx +++ b/ui/src/pages/resource.tsx @@ -1,9 +1,9 @@ import ExportToml from "@/components/export-toml"; import ResourceUpdates from "@/components/updates/resource"; import { + useListItemQuery, usePermissions, usePushRecentlyViewed, - useRead, useResourceParamType, useSetTitle, } from "@/lib/hooks"; @@ -36,7 +36,9 @@ export default function Resource() { function ResourceInner({ type, id }: { type: UsableResource; id: string }) { const RC = ResourceComponents[type]; - const resources = useRead(`List${type}s`, {}).data; + // Same query as `useListItem` under the hood, so they share the cache entry. + // Used to distinguish "still loading" from "resource doesn't exist". + const resources = useListItemQuery(type, id).data; const resource = RC.useListItem(id); const { canCreate, canExecute } = usePermissions({ type, id }); diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index c43fb49dc6..4ed49af945 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -11,7 +11,8 @@ import { import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; import { Page, useDebounce } from "mogh_ui"; -import { Group, Pagination, Stack } from "@mantine/core"; +import { Group, Stack } from "@mantine/core"; +import ListPagination from "@/components/list-pagination"; import { TableSkeleton } from "mogh_ui"; import TemplateQuerySelector from "@/components/template-query-selector"; import TagsFilter from "@/components/tags/filter"; @@ -50,6 +51,13 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const tags = useTagsFilter(); const [templates] = useTemplatesQueryBehavior(); + + // Set to page 0 whenever the resource type or any filter changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [type, tags, templates, filterUpdateAvailable]); + const query: Types.ResourceQuery = { terms, tags, @@ -84,8 +92,6 @@ export default function Resources({ _type }: { _type?: UsableResource }) { return ; } - const targets = resources.map((resource) => ({ type, id: resource.id })); - return ( {type === "Server" && } - + } > @@ -103,21 +109,11 @@ export default function Resources({ _type }: { _type?: UsableResource }) { {(isAdmin || !disableNonAdminCreate) && } - {/* PAGINATION (only shown when needed) */} - {(resources.length >= 100 || page > 0) && ( - = 100 ? page + 2 : page + 1} - value={page + 1} - onChange={(page) => setPage(page - 1)} - > - - - - - - - - )} + diff --git a/ui/src/resources/action/index.tsx b/ui/src/resources/action/index.tsx index 1f95cd5518..1d687f55ac 100644 --- a/ui/src/resources/action/index.tsx +++ b/ui/src/resources/action/index.tsx @@ -1,5 +1,5 @@ import { actionStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -16,9 +16,7 @@ import BatchExecutions from "@/components/batch-executions"; import { RunAction } from "./executions"; export function useAction(id: string | undefined, useName?: boolean) { - return useRead("ListActions", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Action", id, useName); } export function useFullAction(id: string) { @@ -75,8 +73,7 @@ export const ActionComponents: RequiredResourceComponents< Table: ActionTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListActions", {}).data?.find((r) => r.id === id) - ?.info.state; + const state = useAction(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(actionStateIntention(state)); diff --git a/ui/src/resources/alerter/config/resources.tsx b/ui/src/resources/alerter/config/resources.tsx index 2002967bbf..0384ed313c 100644 --- a/ui/src/resources/alerter/config/resources.tsx +++ b/ui/src/resources/alerter/config/resources.tsx @@ -22,7 +22,7 @@ export default function AlerterConfigResources({ disabled: boolean; blacklist: boolean; }) { - const resourcesMap = useAllResources(); + const resourcesMap = useAllResources(undefined, 0); const allResources = [ ...(resourcesMap.Server?.map((server) => { return { diff --git a/ui/src/resources/alerter/index.tsx b/ui/src/resources/alerter/index.tsx index 25725d240e..3efcaa7f23 100644 --- a/ui/src/resources/alerter/index.tsx +++ b/ui/src/resources/alerter/index.tsx @@ -1,4 +1,4 @@ -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -11,9 +11,7 @@ import BatchExecutions from "@/components/batch-executions"; import { ConfirmButton } from "mogh_ui"; export function useAlerter(id: string | undefined, useName?: boolean) { - return useRead("ListAlerters", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Alerter", id, useName); } export function useFullAlerter(id: string) { @@ -57,8 +55,7 @@ export const AlerterComponents: RequiredResourceComponents< Table: AlerterTable, Icon: ({ id, size = "1rem", noColor }) => { - const enabled = useRead("ListAlerters", {}).data?.find((r) => r.id === id) - ?.info.enabled; + const enabled = useAlerter(id)?.info.enabled; const color = enabled === undefined || noColor ? undefined diff --git a/ui/src/resources/build/index.tsx b/ui/src/resources/build/index.tsx index 5876c49042..f4d78db933 100644 --- a/ui/src/resources/build/index.tsx +++ b/ui/src/resources/build/index.tsx @@ -1,5 +1,11 @@ import { buildStateIntention } from "@/lib/color"; -import { useInvalidate, usePermissions, useRead, useWrite } from "@/lib/hooks"; +import { + useInvalidate, + useListItem, + usePermissions, + useRead, + useWrite, +} from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -21,9 +27,7 @@ import ResourceSelector from "../selector"; import { hexColorByIntention } from "mogh_ui"; export function useBuild(id: string | undefined, useName?: boolean) { - return useRead("ListBuilds", { query: { names: id ? [id] : [] } }).data?.find( - (r) => (useName ? r.name === id : r.id === id), - ); + return useListItem("Build", id, useName); } export function useFullBuild(id: string) { @@ -108,8 +112,7 @@ export const BuildComponents: RequiredResourceComponents< Table: BuildTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListBuilds", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useBuild(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(buildStateIntention(state)); diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index 3d19901c1c..ef89116d9a 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -1,4 +1,4 @@ -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -13,9 +13,7 @@ import { useState } from "react"; import { Group, Select } from "@mantine/core"; export function useBuilder(id: string | undefined, useName?: boolean) { - return useRead("ListBuilders", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Builder", id, useName); } export function useFullBuilder(id: string) { @@ -82,9 +80,9 @@ export const BuilderComponents: RequiredResourceComponents< (builder?.info.builder_type === "Server" && builder.info.instance_type?.split(",").map((id) => id.trim())) || []; - const servers = useRead("ListServers", {}).data?.filter((s) => - configured.includes(s.id), - ); + const servers = useRead("ListServers", { + query: { names: configured }, + }).data?.filter((s) => configured.includes(s.id)); const coreVersion = useRead("GetVersion", {}).data?.version; const intent = !servers?.length ? "Neutral" diff --git a/ui/src/resources/deployment/executions.tsx b/ui/src/resources/deployment/executions.tsx index ed12d3b0a7..ff7495eccf 100644 --- a/ui/src/resources/deployment/executions.tsx +++ b/ui/src/resources/deployment/executions.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { Group, Select, Stack, Text } from "@mantine/core"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { Types } from "komodo_client"; import { parseKeyValue } from "@/lib/utils"; import { useDeployment } from "."; @@ -23,8 +23,7 @@ export function DeployDeployment({ id }: DeploymentId) { const { mutateAsync: deploy, isPending } = useExecute("Deploy"); - const deployments = useRead("ListDeployments", {}).data; - const deployment_item = deployments?.find((d) => d.id === id); + const deployment_item = useListItem("Deployment", id); const deploying = useRead( "GetDeploymentActionState", @@ -93,8 +92,7 @@ export function DestroyDeployment({ id }: DeploymentId) { const { mutateAsync: destroy, isPending } = useExecute("DestroyDeployment"); - const deployments = useRead("ListDeployments", {}).data; - const state = deployments?.find((d) => d.id === id)?.info.state; + const state = useListItem("Deployment", id)?.info.state; const destroying = useRead( "GetDeploymentActionState", diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index b0f9544621..d35d773e57 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -1,5 +1,5 @@ import { deploymentStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -29,9 +29,7 @@ import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; export function useDeployment(id: string | undefined, useName?: boolean) { - return useRead("ListDeployments", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Deployment", id, useName); } export function useFullDeployment(id: string) { @@ -114,9 +112,7 @@ export const DeploymentComponents: RequiredResourceComponents< Table: DeploymentTable, Icon: ({ id, size = "1rem", noColor }) => { - const info = useRead("ListDeployments", {}).data?.find( - (r) => r.id === id, - )?.info; + const info = useDeployment(id)?.info; const color = noColor ? undefined : info && diff --git a/ui/src/resources/deployment/table.tsx b/ui/src/resources/deployment/table.tsx index d858db1f27..8f6533fd3e 100644 --- a/ui/src/resources/deployment/table.tsx +++ b/ui/src/resources/deployment/table.tsx @@ -1,4 +1,4 @@ -import { useResourceName, useSelectedResources } from "@/lib/hooks"; +import { useSelectedResources } from "@/lib/hooks"; import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; import { Group, BoxProps } from "@mantine/core"; @@ -14,9 +14,6 @@ export default function DeploymentTable({ }: { resources: Types.DeploymentListItem[]; } & BoxProps) { - const swarmName = useResourceName("Swarm"); - const serverName = useResourceName("Server"); - const [_, setSelectedResources] = useSelectedResources("Deployment"); return ( @@ -63,11 +60,11 @@ export default function DeploymentTable({ accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id - ? swarmName(a.original.info.swarm_id) - : serverName(a.original.info.server_id); + ? a.original.info.swarm_name + : a.original.info.server_name; const name_b = b.original.info.swarm_id - ? swarmName(b.original.info.swarm_id) - : serverName(b.original.info.server_id); + ? b.original.info.swarm_name + : b.original.info.server_name; if (!name_a && !name_b) return 0; if (!name_a) return 1; diff --git a/ui/src/resources/new.tsx b/ui/src/resources/new.tsx index 5aa33d716d..ba4b949cf3 100644 --- a/ui/src/resources/new.tsx +++ b/ui/src/resources/new.tsx @@ -31,8 +31,9 @@ export default function NewResource({ }: NewResourceProps) { const nav = useNavigate(); const showTemplateSelector = - (useRead(`List${type}s`, {}).data?.filter((r) => r.template).length ?? 0) > - 0; + (useRead(`List${type}s`, { + query: { templates: Types.TemplatesQueryBehavior.Only }, + }).data?.length ?? 0) > 0; const { mutateAsync: create, isPending: createPending } = useWrite( `Create${type}`, diff --git a/ui/src/resources/procedure/index.tsx b/ui/src/resources/procedure/index.tsx index a8b729bf60..330903007e 100644 --- a/ui/src/resources/procedure/index.tsx +++ b/ui/src/resources/procedure/index.tsx @@ -1,5 +1,5 @@ import { procedureStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -17,9 +17,7 @@ import { updateLogToHtml } from "@/lib/utils"; import { hexColorByIntention } from "mogh_ui"; export function useProcedure(id: string | undefined, useName?: boolean) { - return useRead("ListProcedures", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Procedure", id, useName); } export function useFullProcedure(id: string) { @@ -80,8 +78,7 @@ export const ProcedureComponents: RequiredResourceComponents< Table: ProcedureTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListProcedures", {}).data?.find((r) => r.id === id) - ?.info.state; + const state = useProcedure(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(procedureStateIntention(state)); diff --git a/ui/src/resources/repo/index.tsx b/ui/src/resources/repo/index.tsx index 567f8e1147..06bea3382e 100644 --- a/ui/src/resources/repo/index.tsx +++ b/ui/src/resources/repo/index.tsx @@ -1,5 +1,5 @@ import { repoStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from "@/resources"; import { Types } from "komodo_client"; @@ -18,9 +18,7 @@ import RepoLink from "@/components/repo-link"; import { hexColorByIntention } from "mogh_ui"; export function useRepo(id: string | undefined, useName?: boolean) { - return useRead("ListRepos", { query: { names: id ? [id] : [] } }).data?.find( - (r) => (useName ? r.name === id : r.id === id), - ); + return useListItem("Repo", id, useName); } export function useFullRepo(id: string) { @@ -84,8 +82,7 @@ export const RepoComponents: RequiredResourceComponents< Table: RepoTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListRepos", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useRepo(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(repoStateIntention(state)); diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 674e975578..119ac4eec3 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -1,5 +1,5 @@ import { serverStateIntention } from "@/lib/color"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -25,9 +25,7 @@ import ServerCpuUsage from "./stats/current/cpu"; import { HoverError } from "mogh_ui"; export function useServer(id: string | undefined, useName?: boolean) { - return useRead("ListServers", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("Server", id, useName); } export function useFullServer(id: string | undefined) { @@ -102,9 +100,7 @@ export const ServerComponents: RequiredResourceComponents< Icon: ({ id, size = "1rem", noColor }) => { const coreVersion = useRead("GetVersion", {}).data?.version; - const info = useRead("ListServers", {}).data?.find( - (r) => r.id === id, - )?.info; + const info = useServer(id)?.info; const state = info?.state; const color = noColor ? undefined diff --git a/ui/src/resources/stack/index.tsx b/ui/src/resources/stack/index.tsx index 590c881cd5..555d757f66 100644 --- a/ui/src/resources/stack/index.tsx +++ b/ui/src/resources/stack/index.tsx @@ -1,5 +1,11 @@ import { stackStateIntention } from "@/lib/color"; -import { useInvalidate, usePermissions, useRead, useWrite } from "@/lib/hooks"; +import { + useInvalidate, + useListItem, + usePermissions, + useRead, + useWrite, +} from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { Types } from "komodo_client"; import { StatusBadge } from "mogh_ui"; @@ -37,9 +43,7 @@ import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; export function useStack(id: string | undefined, useName?: boolean) { - return useRead("ListStacks", { query: { names: id ? [id] : [] } }).data?.find( - (r) => (useName ? r.name === id : r.id === id), - ); + return useListItem("Stack", id, useName); } export function useFullStack(id: string) { @@ -118,7 +122,7 @@ export const StackComponents: RequiredResourceComponents< Table: StackTable, Icon: ({ id, size = "1rem", noColor }) => { - const info = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info; + const info = useStack(id)?.info; const color = noColor ? undefined : info && diff --git a/ui/src/resources/stack/table.tsx b/ui/src/resources/stack/table.tsx index bf25927860..dc99f3d459 100644 --- a/ui/src/resources/stack/table.tsx +++ b/ui/src/resources/stack/table.tsx @@ -1,4 +1,4 @@ -import { useResourceName, useSelectedResources } from "@/lib/hooks"; +import { useSelectedResources } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Group, BoxProps } from "@mantine/core"; import { Types } from "komodo_client"; @@ -14,9 +14,6 @@ export default function StackTable({ }: { resources: Types.StackListItem[]; } & BoxProps) { - const swarmName = useResourceName("Swarm"); - const serverName = useResourceName("Server"); - const [_, setSelectedResources] = useSelectedResources("Stack"); return ( @@ -59,11 +56,11 @@ export default function StackTable({ accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id - ? swarmName(a.original.info.swarm_id) - : serverName(a.original.info.server_id); + ? a.original.info.swarm_name + : a.original.info.server_name; const name_b = b.original.info.swarm_id - ? swarmName(b.original.info.swarm_id) - : serverName(b.original.info.server_id); + ? b.original.info.swarm_name + : b.original.info.server_name; if (!name_a && !name_b) return 0; if (!name_a) return 1; diff --git a/ui/src/resources/swarm/index.tsx b/ui/src/resources/swarm/index.tsx index f2f086eb90..725204ae5d 100644 --- a/ui/src/resources/swarm/index.tsx +++ b/ui/src/resources/swarm/index.tsx @@ -3,7 +3,7 @@ import { swarmNodeStateIntention, swarmTaskStateIntention, } from "@/lib/color"; -import { usePermissions, useRead } from "@/lib/hooks"; +import { useListItem, usePermissions, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -21,9 +21,7 @@ import { HoverError } from "mogh_ui"; import { hexColorByIntention } from "mogh_ui"; export function useSwarm(id: string | undefined, useName?: boolean) { - return useRead("ListSwarms", { query: { names: id ? [id] : [] } }).data?.find( - (r) => (useName ? r.name === id : r.id === id), - ); + return useListItem("Swarm", id, useName); } export function useFullSwarm(id: string) { @@ -73,8 +71,7 @@ export const SwarmComponents: RequiredResourceComponents< Table: SwarmTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListSwarms", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useSwarm(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(swarmStateIntention(state)); diff --git a/ui/src/resources/sync/index.tsx b/ui/src/resources/sync/index.tsx index 8739db9d93..e169f5399a 100644 --- a/ui/src/resources/sync/index.tsx +++ b/ui/src/resources/sync/index.tsx @@ -1,5 +1,5 @@ import { resourceSyncStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { Types } from "komodo_client"; import { fmtDate, StatusBadge } from "mogh_ui"; @@ -17,9 +17,7 @@ import BatchExecutions from "@/components/batch-executions"; import { hexColorByIntention } from "mogh_ui"; export function useResourceSync(id: string | undefined, useName?: boolean) { - return useRead("ListResourceSyncs", { - query: { names: id ? [id] : [] }, - }).data?.find((r) => (useName ? r.name === id : r.id === id)); + return useListItem("ResourceSync", id, useName); } export function useFullResourceSync(id: string) { @@ -88,9 +86,7 @@ export const ResourceSyncComponents: RequiredResourceComponents< Table: ResourceSyncTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListResourceSyncs", {}).data?.find( - (r) => r.id === id, - )?.info.state; + const state = useResourceSync(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(resourceSyncStateIntention(state)); diff --git a/ui/src/resources/tags.tsx b/ui/src/resources/tags.tsx index 7f6258efbc..0c7d94a1dd 100644 --- a/ui/src/resources/tags.tsx +++ b/ui/src/resources/tags.tsx @@ -1,6 +1,6 @@ import Tags from "@/components/tags"; import { tagColor } from "@/lib/color"; -import { useInvalidate, useRead, useWrite } from "@/lib/hooks"; +import { useInvalidate, useListItem, useRead, useWrite } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { filterBySplit } from "mogh_ui"; import { @@ -28,7 +28,7 @@ export const ResourceTags = ({ }) => { const inv = useInvalidate(); const { type, id } = target; - const resource = useRead(`List${type}s`, {}).data?.find((d) => d.id === id); + const resource = useListItem(type, id); const { mutate } = useWrite("UpdateResourceMeta", { onSuccess: () => { inv([`List${type}s`]); From ac23de398594d246dcd82176bc9938b63f37c8bc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 13:11:36 -0700 Subject: [PATCH 106/203] some brute force pagination fixes still with limit: 0 --- ui/src/pages/dashboard/active.tsx | 12 ++++++------ ui/src/pages/terminals/new.tsx | 2 +- ui/src/resources/build/tabs.tsx | 7 ++++--- ui/src/resources/deployment/config/index.tsx | 2 +- ui/src/resources/repo/tabs.tsx | 11 ++++++----- ui/src/resources/server/table/standard.tsx | 6 +++--- ui/src/resources/server/tabs.tsx | 16 +++++++++------- ui/src/resources/swarm/tabs.tsx | 4 ++-- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/ui/src/pages/dashboard/active.tsx b/ui/src/pages/dashboard/active.tsx index 84b56faf3e..b677a2d1d2 100644 --- a/ui/src/pages/dashboard/active.tsx +++ b/ui/src/pages/dashboard/active.tsx @@ -19,7 +19,7 @@ import { ReactNode } from "react"; export default function DashboardActiveResources() { const stacks = - useRead("ListStacks", {}).data?.filter((stack) => + useRead("ListStacks", { limit: 0 }).data?.filter((stack) => [ Types.StackState.Deploying, Types.StackState.Restarting, @@ -27,7 +27,7 @@ export default function DashboardActiveResources() { ].includes(stack.info.state), ) ?? []; const deployments = - useRead("ListDeployments", {}).data?.filter((deployment) => + useRead("ListDeployments", { limit: 0 }).data?.filter((deployment) => [ Types.DeploymentState.Deploying, Types.DeploymentState.Restarting, @@ -35,11 +35,11 @@ export default function DashboardActiveResources() { ].includes(deployment.info.state), ) ?? []; const builds = - useRead("ListBuilds", {}).data?.filter( + useRead("ListBuilds", { limit: 0 }).data?.filter( (build) => build.info.state === Types.BuildState.Building, ) ?? []; const repos = - useRead("ListRepos", {}).data?.filter((repo) => + useRead("ListRepos", { limit: 0 }).data?.filter((repo) => [ Types.RepoState.Building, Types.RepoState.Cloning, @@ -47,11 +47,11 @@ export default function DashboardActiveResources() { ].includes(repo.info.state), ) ?? []; const procedures = - useRead("ListProcedures", {}).data?.filter( + useRead("ListProcedures", { limit: 0 }).data?.filter( (procedure) => procedure.info.state === Types.ProcedureState.Running, ) ?? []; const actions = - useRead("ListActions", {}).data?.filter( + useRead("ListActions", { limit: 0 }).data?.filter( (action) => action.info.state === Types.ActionState.Running, ) ?? []; const globalAutoUpdates = diff --git a/ui/src/pages/terminals/new.tsx b/ui/src/pages/terminals/new.tsx index b0066ffdb2..493cb89dbb 100644 --- a/ui/src/pages/terminals/new.tsx +++ b/ui/src/pages/terminals/new.tsx @@ -237,7 +237,7 @@ function CreateServerTerminal({ }) { const nav = useNavigate(); const firstServer = - (useRead("ListServers", {}).data?.filter( + (useRead("ListServers", { limit: 0 }).data?.filter( (s) => s.info.state === Types.ServerState.Ok, ) ?? [])[0]?.id ?? ""; const [server, _setServer] = useState(firstServer); diff --git a/ui/src/resources/build/tabs.tsx b/ui/src/resources/build/tabs.tsx index ea1265c183..babaee2fc4 100644 --- a/ui/src/resources/build/tabs.tsx +++ b/ui/src/resources/build/tabs.tsx @@ -25,9 +25,10 @@ export default function BuildTabs({ id }: { id: string }) { const info = useBuild(id)?.info; - const deployments = useRead("ListDeployments", {}).data?.filter( - (deployment) => deployment.info.build_id === id, - ); + const deployments = useRead("ListDeployments", { + query: { specific: { build_ids: [id] } }, + limit: 0, + }).data; const deploymentsDisabled = (deployments?.length || 0) === 0; const hasBeenBuilt = !!useRead("ListUpdates", { diff --git a/ui/src/resources/deployment/config/index.tsx b/ui/src/resources/deployment/config/index.tsx index a3b1f8b08c..9c5023934d 100644 --- a/ui/src/resources/deployment/config/index.tsx +++ b/ui/src/resources/deployment/config/index.tsx @@ -28,7 +28,7 @@ export default function DeploymentConfig({ }) { const { canWrite } = usePermissions({ type: "Deployment", id }); const config = useFullDeployment(id)?.config; - const builds = useRead("ListBuilds", {}).data; + const builds = useRead("ListBuilds", { limit: 0 }).data; const globalDisabled = useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false; const swarmsExist = useRead("ListSwarms", {}).data?.length ? true : false; diff --git a/ui/src/resources/repo/tabs.tsx b/ui/src/resources/repo/tabs.tsx index 44c76e919c..dad904551b 100644 --- a/ui/src/resources/repo/tabs.tsx +++ b/ui/src/resources/repo/tabs.tsx @@ -18,17 +18,18 @@ export default function RepoTabs({ id }: { id: string }) { }); const info = useRepo(id)?.info; const stacks = - useRead("ListStacks", {}).data?.filter( - (stack) => stack.info.linked_repo === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { linked_repos: [id] } }, + limit: 0, + }).data ?? []; const noStacks = stacks.length === 0; const builds = - useRead("ListBuilds", {}).data?.filter( + useRead("ListBuilds", { limit: 0 }).data?.filter( (build) => build.info.linked_repo === id, ) ?? []; const noBuilds = builds.length === 0; const syncs = - useRead("ListResourceSyncs", {}).data?.filter( + useRead("ListResourceSyncs", { limit: 0 }).data?.filter( (sync) => sync.info.linked_repo === id, ) ?? []; const noSyncs = syncs.length === 0; diff --git a/ui/src/resources/server/table/standard.tsx b/ui/src/resources/server/table/standard.tsx index 2aad1f83dd..92a98422c5 100644 --- a/ui/src/resources/server/table/standard.tsx +++ b/ui/src/resources/server/table/standard.tsx @@ -14,9 +14,9 @@ export default function StandardServerTable({ resources: Types.ServerListItem[]; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Server"); - const deployments = useRead("ListDeployments", {}).data; - const stacks = useRead("ListStacks", {}).data; - const repos = useRead("ListRepos", {}).data; + const deployments = useRead("ListDeployments", { limit: 0 }).data; + const stacks = useRead("ListStacks", { limit: 0 }).data; + const repos = useRead("ListRepos", { limit: 0 }).data; const resourcesCount = useCallback( (id: string) => { return ( diff --git a/ui/src/resources/server/tabs.tsx b/ui/src/resources/server/tabs.tsx index 97cce2a72f..7973a7bc43 100644 --- a/ui/src/resources/server/tabs.tsx +++ b/ui/src/resources/server/tabs.tsx @@ -30,17 +30,19 @@ export default function ServerTabs({ id }: { id: string }) { !specificTerminal || (serverInfo?.terminals_disabled ?? true); const stacks = - useRead("ListStacks", {}).data?.filter( - (stack) => stack.info.server_id === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { server_ids: [id] } }, + limit: 0, + }).data ?? []; const noStacks = stacks.length === 0; const deployments = - useRead("ListDeployments", {}).data?.filter( - (deployment) => deployment.info.server_id === id, - ) ?? []; + useRead("ListDeployments", { + query: { specific: { server_ids: [id] } }, + limit: 0, + }).data ?? []; const noDeployments = deployments.length === 0; const repos = - useRead("ListRepos", {}).data?.filter( + useRead("ListRepos", { limit: 0 }).data?.filter( (repo) => repo.info.server_id === id, ) ?? []; const noRepos = repos.length === 0; diff --git a/ui/src/resources/swarm/tabs.tsx b/ui/src/resources/swarm/tabs.tsx index 08fe8e9292..62adb7b0e1 100644 --- a/ui/src/resources/swarm/tabs.tsx +++ b/ui/src/resources/swarm/tabs.tsx @@ -27,12 +27,12 @@ export default function SwarmTabs({ id }: { id: string }) { const swarmInfo = useSwarm(id)?.info; const stacks = - useRead("ListStacks", {}).data?.filter( + useRead("ListStacks", { limit: 0 }).data?.filter( (stack) => stack.info.swarm_id === id, ) ?? []; const noStacks = stacks.length === 0; const deployments = - useRead("ListDeployments", {}).data?.filter( + useRead("ListDeployments", { limit: 0 }).data?.filter( (deployment) => deployment.info.swarm_id === id, ) ?? []; const noDeployments = deployments.length === 0; From 10391c8399506fa20f28fe73d428d489120c0d56 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 13:15:41 -0700 Subject: [PATCH 107/203] deploy 2.3.0-dev-28 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e2d340958..0b9c6c4f84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6d585555b7..13523617d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-27" +version = "2.3.0-dev-28" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 9a14fb392044be734414125577ae32450da71a4e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 15:31:53 -0700 Subject: [PATCH 108/203] replace limit: 0 server query with proper search based multi selector --- ui/src/pages/containers.tsx | 16 ++----- ui/src/resources/multi-selector.tsx | 72 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 ui/src/resources/multi-selector.tsx diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 9ca7d26699..ae97afabf9 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -6,12 +6,13 @@ import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; -import { Group, MultiSelect, Stack } from "@mantine/core"; +import { Group, Stack } from "@mantine/core"; import { useEffect, useMemo, useState } from "react"; import { DividedChildren } from "mogh_ui"; import ResourceLink from "@/resources/link"; import { SearchInput } from "mogh_ui"; import TagsFilter from "@/components/tags/filter"; +import ResourceMultiSelector from "@/resources/multi-selector"; import ListPagination, { RESOURCE_PAGE_SIZE, } from "@/components/list-pagination"; @@ -32,12 +33,6 @@ export default function Containers() { .filter((term) => term); }, [debouncedSearch]); - const servers = useRead("ListServers", { limit: 0 }).data; - const serverNames = useMemo( - () => servers?.map((server) => server.name) || [], - [servers], - ); - const tags = useTagsFilter(); // Set to page 0 whenever any filter changes, @@ -223,13 +218,10 @@ export default function Containers() { - void; + templates?: Types.TemplatesQueryBehavior; + placeholder?: string; +}) { + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo( + () => + debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term), + [debouncedSearch], + ); + + const _resources = + useRead(`List${type}s`, { query: { templates, terms }, limit: 10 }).data ?? + []; + // Prevent flashing when typing / fetching + const resources = useDebounce(_resources, 200); + + // Keep the selected names in the options, + // even when they don't match the current search. + const data = useMemo(() => { + const names = resources.map((resource) => resource.name); + for (const name of value) { + if (!names.includes(name)) { + names.push(name); + } + } + return names; + }, [resources, value]); + + return ( + options} + /> + ); +} From 8f7854599b830e4c3653de47bec479062704b2fa Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 15:44:13 -0700 Subject: [PATCH 109/203] list api support filtering by state, use with dashboard active table --- bin/core/src/api/read/action.rs | 30 +++++--- bin/core/src/api/read/build.rs | 30 +++++--- bin/core/src/api/read/deployment.rs | 11 +++ bin/core/src/api/read/procedure.rs | 30 +++++--- bin/core/src/api/read/repo.rs | 30 +++++--- bin/core/src/api/read/stack.rs | 11 +++ client/core/rs/src/entities/action.rs | 7 +- client/core/rs/src/entities/build.rs | 5 ++ client/core/rs/src/entities/deployment.rs | 5 ++ client/core/rs/src/entities/procedure.rs | 7 +- client/core/rs/src/entities/repo.rs | 15 +++- client/core/rs/src/entities/stack.rs | 4 ++ client/core/ts/src/types.ts | 32 ++++++++- ui/public/client/types.d.ts | 32 ++++++++- ui/src/pages/dashboard/active.tsx | 84 +++++++++++++++-------- 15 files changed, 258 insertions(+), 75 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 6052fe68a1..d313263be3 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -45,18 +45,28 @@ impl Resolve for ListActions { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_for_user::( - self.query, - limit as i64, - self.page * limit, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let actions = resource::list_for_user::( + self.query, + limit as i64, + self.page * limit, + user, + PermissionLevel::Read.into(), + &all_tags, ) + .await?; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let actions = if states.is_empty() { + actions + } else { + actions + .into_iter() + .filter(|action| states.contains(&action.info.state)) + .collect() + }; + Ok(actions) } } diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 4b01bf5c0d..0848180959 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -53,18 +53,28 @@ impl Resolve for ListBuilds { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_for_user::( - self.query, - limit as i64, - self.page * limit, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let builds = resource::list_for_user::( + self.query, + limit as i64, + self.page * limit, + user, + PermissionLevel::Read.into(), + &all_tags, ) + .await?; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let builds = if states.is_empty() { + builds + } else { + builds + .into_iter() + .filter(|build| states.contains(&build.info.state)) + .collect() + }; + Ok(builds) } } diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index c0daffcb4d..7fa09caa0e 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -63,6 +63,7 @@ impl Resolve for ListDeployments { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let deployments = resource::list_for_user::( self.query, @@ -81,6 +82,16 @@ impl Resolve for ListDeployments { } else { deployments }; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let deployments = if states.is_empty() { + deployments + } else { + deployments + .into_iter() + .filter(|deployment| states.contains(&deployment.info.state)) + .collect() + }; Ok(deployments) } } diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 7d1034e985..3cd7c64214 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -43,18 +43,28 @@ impl Resolve for ListProcedures { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_for_user::( - self.query, - limit as i64, - self.page * limit, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let procedures = resource::list_for_user::( + self.query, + limit as i64, + self.page * limit, + user, + PermissionLevel::Read.into(), + &all_tags, ) + .await?; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let procedures = if states.is_empty() { + procedures + } else { + procedures + .into_iter() + .filter(|procedure| states.contains(&procedure.info.state)) + .collect() + }; + Ok(procedures) } } diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 25c7dc19ff..b57ee4a7e2 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -43,18 +43,28 @@ impl Resolve for ListRepos { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_for_user::( - self.query, - limit as i64, - self.page * limit, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let repos = resource::list_for_user::( + self.query, + limit as i64, + self.page * limit, + user, + PermissionLevel::Read.into(), + &all_tags, ) + .await?; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let repos = if states.is_empty() { + repos + } else { + repos + .into_iter() + .filter(|repo| states.contains(&repo.info.state)) + .collect() + }; + Ok(repos) } } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 854722aeac..1a3be18424 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -467,6 +467,7 @@ impl Resolve for ListStacks { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let stacks = resource::list_for_user::( self.query, @@ -491,6 +492,16 @@ impl Resolve for ListStacks { } else { stacks }; + // The state is not stored on the database, + // it can only be filtered after they are attached to list items. + let stacks = if states.is_empty() { + stacks + } else { + stacks + .into_iter() + .filter(|stack| states.contains(&stack.info.state)) + .collect() + }; Ok(stacks) } } diff --git a/client/core/rs/src/entities/action.rs b/client/core/rs/src/entities/action.rs index 92ce10bc6f..cc983cdc45 100644 --- a/client/core/rs/src/entities/action.rs +++ b/client/core/rs/src/entities/action.rs @@ -261,7 +261,12 @@ pub type ActionQuery = ResourceQuery; Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ActionQuerySpecifics {} +pub struct ActionQuerySpecifics { + /// Query only for Actions matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, +} impl super::resource::AddFilters for ActionQuerySpecifics { fn add_filters(&self, _filters: &mut Document) {} diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index a403178340..cde42303ac 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -724,6 +724,11 @@ pub struct BuildQuerySpecifics { /// defaults to 0 which is a no op #[serde(default)] pub built_since: I64, + + /// Query only for Builds matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for BuildQuerySpecifics { diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index a9bacb5e8c..a8bb3051c9 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -615,6 +615,11 @@ pub struct DeploymentQuerySpecifics { /// Query only for Deployments with available image updates. #[serde(default)] pub update_available: bool, + + /// Query only for Deployments matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for DeploymentQuerySpecifics { diff --git a/client/core/rs/src/entities/procedure.rs b/client/core/rs/src/entities/procedure.rs index a534da0da2..e787b17a98 100644 --- a/client/core/rs/src/entities/procedure.rs +++ b/client/core/rs/src/entities/procedure.rs @@ -261,7 +261,12 @@ pub type ProcedureQuery = ResourceQuery; Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ProcedureQuerySpecifics {} +pub struct ProcedureQuerySpecifics { + /// Query only for Procedures matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, +} impl super::resource::AddFilters for ProcedureQuerySpecifics { fn add_filters(&self, _: &mut Document) {} diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs index 47a83825d1..6d4cefceab 100644 --- a/client/core/rs/src/entities/repo.rs +++ b/client/core/rs/src/entities/repo.rs @@ -60,7 +60,15 @@ pub struct RepoListItemInfo { #[typeshare] #[derive( - Debug, Clone, Copy, Default, Serialize, Deserialize, Display, + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + Display, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum RepoState { @@ -341,7 +349,12 @@ pub type RepoQuery = ResourceQuery; #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RepoQuerySpecifics { /// Filter repos by their repo. + #[serde(default)] pub repos: Vec, + /// Query only for Repos matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for RepoQuerySpecifics { diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 6a16c4ee21..43ccf317ea 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -954,6 +954,10 @@ pub struct StackQuerySpecifics { /// Query only for Stack with available image updates. #[serde(default)] pub update_available: bool, + /// Query only for Stacks matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for StackQuerySpecifics { diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 8b71bbf870..4941eff26f 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -219,6 +219,11 @@ export interface ResourceQuery { } export interface ActionQuerySpecifics { + /** + * Query only for Actions matching these states. + * If empty, does not filter by state. + */ + states?: ActionState[]; } export type ActionQuery = ResourceQuery; @@ -829,6 +834,11 @@ export interface BuildQuerySpecifics { * defaults to 0 which is a no op */ built_since?: I64; + /** + * Query only for Builds matching these states. + * If empty, does not filter by state. + */ + states?: BuildState[]; } export type BuildQuery = ResourceQuery; @@ -1494,6 +1504,11 @@ export interface DeploymentQuerySpecifics { build_ids?: string[]; /** Query only for Deployments with available image updates. */ update_available?: boolean; + /** + * Query only for Deployments matching these states. + * If empty, does not filter by state. + */ + states?: DeploymentState[]; } export type DeploymentQuery = ResourceQuery; @@ -5774,6 +5789,11 @@ export type ListVolumesResponse = VolumeListItem[]; export type MongoDocument = any; export interface ProcedureQuerySpecifics { + /** + * Query only for Procedures matching these states. + * If empty, does not filter by state. + */ + states?: ProcedureState[]; } export type ProcedureQuery = ResourceQuery; @@ -5782,7 +5802,12 @@ export type PushRecentlyViewedResponse = NoData; export interface RepoQuerySpecifics { /** Filter repos by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Repos matching these states. + * If empty, does not filter by state. + */ + states?: RepoState[]; } export type RepoQuery = ResourceQuery; @@ -5826,6 +5851,11 @@ export interface StackQuerySpecifics { repos?: string[]; /** Query only for Stack with available image updates. */ update_available?: boolean; + /** + * Query only for Stacks matching these states. + * If empty, does not filter by state. + */ + states?: StackState[]; } export type StackQuery = ResourceQuery; diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index df2860bf09..5b7b5a8e38 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -197,6 +197,11 @@ export interface ResourceQuery { specific?: T; } export interface ActionQuerySpecifics { + /** + * Query only for Actions matching these states. + * If empty, does not filter by state. + */ + states?: ActionState[]; } export type ActionQuery = ResourceQuery; export type AlerterEndpoint = @@ -817,6 +822,11 @@ export interface BuildQuerySpecifics { * defaults to 0 which is a no op */ built_since?: I64; + /** + * Query only for Builds matching these states. + * If empty, does not filter by state. + */ + states?: BuildState[]; } export type BuildQuery = ResourceQuery; export type BuilderConfig = @@ -1636,6 +1646,11 @@ export interface DeploymentQuerySpecifics { build_ids?: string[]; /** Query only for Deployments with available image updates. */ update_available?: boolean; + /** + * Query only for Deployments matching these states. + * If empty, does not filter by state. + */ + states?: DeploymentState[]; } export type DeploymentQuery = ResourceQuery; /** Response containing pretty formatted toml contents. */ @@ -5631,12 +5646,22 @@ export interface VolumeListItem { export type ListVolumesResponse = VolumeListItem[]; export type MongoDocument = any; export interface ProcedureQuerySpecifics { + /** + * Query only for Procedures matching these states. + * If empty, does not filter by state. + */ + states?: ProcedureState[]; } export type ProcedureQuery = ResourceQuery; export type PushRecentlyViewedResponse = NoData; export interface RepoQuerySpecifics { /** Filter repos by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Repos matching these states. + * If empty, does not filter by state. + */ + states?: RepoState[]; } export type RepoQuery = ResourceQuery; export interface ResourceSyncQuerySpecifics { @@ -5669,6 +5694,11 @@ export interface StackQuerySpecifics { repos?: string[]; /** Query only for Stack with available image updates. */ update_available?: boolean; + /** + * Query only for Stacks matching these states. + * If empty, does not filter by state. + */ + states?: StackState[]; } export type StackQuery = ResourceQuery; export interface SwarmQuerySpecifics { diff --git a/ui/src/pages/dashboard/active.tsx b/ui/src/pages/dashboard/active.tsx index b677a2d1d2..afb13aa582 100644 --- a/ui/src/pages/dashboard/active.tsx +++ b/ui/src/pages/dashboard/active.tsx @@ -19,41 +19,65 @@ import { ReactNode } from "react"; export default function DashboardActiveResources() { const stacks = - useRead("ListStacks", { limit: 0 }).data?.filter((stack) => - [ - Types.StackState.Deploying, - Types.StackState.Restarting, - Types.StackState.Removing, - ].includes(stack.info.state), - ) ?? []; + useRead("ListStacks", { + query: { + specific: { + states: [ + Types.StackState.Deploying, + Types.StackState.Restarting, + Types.StackState.Removing, + ], + }, + }, + limit: 0, + }).data ?? []; const deployments = - useRead("ListDeployments", { limit: 0 }).data?.filter((deployment) => - [ - Types.DeploymentState.Deploying, - Types.DeploymentState.Restarting, - Types.DeploymentState.Removing, - ].includes(deployment.info.state), - ) ?? []; + useRead("ListDeployments", { + query: { + specific: { + states: [ + Types.DeploymentState.Deploying, + Types.DeploymentState.Restarting, + Types.DeploymentState.Removing, + ], + }, + }, + limit: 0, + }).data ?? []; const builds = - useRead("ListBuilds", { limit: 0 }).data?.filter( - (build) => build.info.state === Types.BuildState.Building, - ) ?? []; + useRead("ListBuilds", { + query: { + specific: { states: [Types.BuildState.Building] }, + }, + limit: 0, + }).data ?? []; const repos = - useRead("ListRepos", { limit: 0 }).data?.filter((repo) => - [ - Types.RepoState.Building, - Types.RepoState.Cloning, - Types.RepoState.Pulling, - ].includes(repo.info.state), - ) ?? []; + useRead("ListRepos", { + query: { + specific: { + states: [ + Types.RepoState.Building, + Types.RepoState.Cloning, + Types.RepoState.Pulling, + ], + }, + }, + limit: 0, + }).data ?? []; const procedures = - useRead("ListProcedures", { limit: 0 }).data?.filter( - (procedure) => procedure.info.state === Types.ProcedureState.Running, - ) ?? []; + useRead("ListProcedures", { + query: { + specific: { states: [Types.ProcedureState.Running] }, + }, + limit: 0, + }).data ?? []; const actions = - useRead("ListActions", { limit: 0 }).data?.filter( - (action) => action.info.state === Types.ActionState.Running, - ) ?? []; + useRead("ListActions", { + query: { + specific: { states: [Types.ActionState.Running] }, + }, + limit: 0, + }).data ?? []; const globalAutoUpdates = useRead("ListUpdates", { query: { From ac9037d84a0b3839c1df6b52db1bd55c161cb3cb Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 16:00:26 -0700 Subject: [PATCH 110/203] Fix state filter pagination and create terminal limit: 0 queries --- bin/core/src/api/read/action.rs | 23 +++++++----- bin/core/src/api/read/build.rs | 23 +++++++----- bin/core/src/api/read/deployment.rs | 41 ++++++++++++--------- bin/core/src/api/read/procedure.rs | 23 +++++++----- bin/core/src/api/read/repo.rs | 23 +++++++----- bin/core/src/api/read/server.rs | 37 +++++++++++++------ bin/core/src/api/read/stack.rs | 52 +++++++++++++++------------ bin/core/src/resource/mod.rs | 24 +++++++++++++ client/core/rs/src/entities/server.rs | 7 +++- client/core/ts/src/types.ts | 5 +++ ui/public/client/types.d.ts | 5 +++ ui/src/pages/terminals/new.tsx | 28 +++++++++++---- 12 files changed, 202 insertions(+), 89 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index d313263be3..1067feadb1 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -47,24 +47,31 @@ impl Resolve for ListActions { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // When filtering by state, the db level pagination must be + // disabled, and applied in memory after the state filter. + let (db_limit, db_skip) = if states.is_empty() { + (limit, self.page * limit) + } else { + (0, 0) + }; let actions = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. let actions = if states.is_empty() { actions } else { - actions - .into_iter() - .filter(|action| states.contains(&action.info.state)) - .collect() + resource::filter_list_items_paginated( + actions, + |action| states.contains(&action.info.state), + limit, + self.page, + ) }; Ok(actions) } diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 0848180959..ab64cf83a9 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -55,24 +55,31 @@ impl Resolve for ListBuilds { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // When filtering by state, the db level pagination must be + // disabled, and applied in memory after the state filter. + let (db_limit, db_skip) = if states.is_empty() { + (limit, self.page * limit) + } else { + (0, 0) + }; let builds = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. let builds = if states.is_empty() { builds } else { - builds - .into_iter() - .filter(|build| states.contains(&build.info.state)) - .collect() + resource::filter_list_items_paginated( + builds, + |build| states.contains(&build.info.state), + limit, + self.page, + ) }; Ok(builds) } diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 7fa09caa0e..61ba6257d7 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -65,32 +65,39 @@ impl Resolve for ListDeployments { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // Update available / state are computed in memory rather than + // stored on the db. When filtering on them, the db level + // pagination must be disabled, and applied in memory + // after the filters. + let use_db_pagination = + !only_update_available && states.is_empty(); + let (db_limit, db_skip) = if use_db_pagination { + (limit, self.page * limit) + } else { + (0, 0) + }; let deployments = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - let deployments = if only_update_available { + let deployments = if use_db_pagination { deployments - .into_iter() - .filter(|deployment| deployment.info.update_available) - .collect() } else { - deployments - }; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. - let deployments = if states.is_empty() { - deployments - } else { - deployments - .into_iter() - .filter(|deployment| states.contains(&deployment.info.state)) - .collect() + resource::filter_list_items_paginated( + deployments, + |deployment| { + (!only_update_available || deployment.info.update_available) + && (states.is_empty() + || states.contains(&deployment.info.state)) + }, + limit, + self.page, + ) }; Ok(deployments) } diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 3cd7c64214..0fbcdc1f0a 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -45,24 +45,31 @@ impl Resolve for ListProcedures { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // When filtering by state, the db level pagination must be + // disabled, and applied in memory after the state filter. + let (db_limit, db_skip) = if states.is_empty() { + (limit, self.page * limit) + } else { + (0, 0) + }; let procedures = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. let procedures = if states.is_empty() { procedures } else { - procedures - .into_iter() - .filter(|procedure| states.contains(&procedure.info.state)) - .collect() + resource::filter_list_items_paginated( + procedures, + |procedure| states.contains(&procedure.info.state), + limit, + self.page, + ) }; Ok(procedures) } diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index b57ee4a7e2..7188d8dbb5 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -45,24 +45,31 @@ impl Resolve for ListRepos { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // When filtering by state, the db level pagination must be + // disabled, and applied in memory after the state filter. + let (db_limit, db_skip) = if states.is_empty() { + (limit, self.page * limit) + } else { + (0, 0) + }; let repos = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. let repos = if states.is_empty() { repos } else { - repos - .into_iter() - .filter(|repo| states.contains(&repo.info.state)) - .collect() + resource::filter_list_items_paginated( + repos, + |repo| states.contains(&repo.info.state), + limit, + self.page, + ) }; Ok(repos) } diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index fdc19db884..bdebdb9b68 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -106,18 +106,35 @@ impl Resolve for ListServers { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_for_user::( - self.query, - limit as i64, - self.page * limit, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + // When filtering by state, the db level pagination must be + // disabled, and applied in memory after the state filter. + let (db_limit, db_skip) = if states.is_empty() { + (limit, self.page * limit) + } else { + (0, 0) + }; + let servers = resource::list_for_user::( + self.query, + db_limit as i64, + db_skip, + user, + PermissionLevel::Read.into(), + &all_tags, ) + .await?; + let servers = if states.is_empty() { + servers + } else { + resource::filter_list_items_paginated( + servers, + |server| states.contains(&server.info.state), + limit, + self.page, + ) + }; + Ok(servers) } } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 1a3be18424..2f77399b4e 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -469,38 +469,44 @@ impl Resolve for ListStacks { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + // Update available / state are computed in memory rather than + // stored on the db. When filtering on them, the db level + // pagination must be disabled, and applied in memory + // after the filters. + let use_db_pagination = + !only_update_available && states.is_empty(); + let (db_limit, db_skip) = if use_db_pagination { + (limit, self.page * limit) + } else { + (0, 0) + }; let stacks = resource::list_for_user::( self.query, - limit as i64, - self.page * limit, + db_limit as i64, + db_skip, user, PermissionLevel::Read.into(), &all_tags, ) .await?; - let stacks = if only_update_available { + let stacks = if use_db_pagination { stacks - .into_iter() - .filter(|stack| { - stack - .info - .services - .iter() - .any(|service| service.update_available) - }) - .collect() } else { - stacks - }; - // The state is not stored on the database, - // it can only be filtered after they are attached to list items. - let stacks = if states.is_empty() { - stacks - } else { - stacks - .into_iter() - .filter(|stack| states.contains(&stack.info.state)) - .collect() + resource::filter_list_items_paginated( + stacks, + |stack| { + (!only_update_available + || stack + .info + .services + .iter() + .any(|service| service.update_available)) + && (states.is_empty() + || states.contains(&stack.info.state)) + }, + limit, + self.page, + ) }; Ok(stacks) } diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index d316ca1057..11b1bc5bda 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -276,6 +276,30 @@ pub async fn list_all_resources( }) } +/// Some list item filters (eg. state, update available) are computed +/// from in-memory caches rather than stored on the database, and can +/// only be applied after the db query converts to list items. +/// Callers using these filters should pass `(0, 0)` limit / skip to +/// the db level query, and apply the equivalent pagination here +/// after filtering. +pub fn filter_list_items_paginated( + items: Vec, + filter: impl Fn(&T) -> bool, + limit: u64, + page: u64, +) -> Vec { + items + .into_iter() + .filter(|item| filter(item)) + .skip((page * limit) as usize) + .take(if limit == 0 { + usize::MAX + } else { + limit as usize + }) + .collect() +} + pub async fn list_for_user( mut query: ResourceQuery, limit: impl Into>, diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index 823f1846e0..4e4a3fe116 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -462,6 +462,11 @@ pub type ServerQuery = ResourceQuery; #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ServerQuerySpecifics {} +pub struct ServerQuerySpecifics { + /// Query only for Servers matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, +} impl AddFilters for ServerQuerySpecifics {} diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 4941eff26f..1887863ec0 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -5828,6 +5828,11 @@ export type SearchStackLogResponse = Log; export type SearchSwarmServiceLogResponse = Log; export interface ServerQuerySpecifics { + /** + * Query only for Servers matching these states. + * If empty, does not filter by state. + */ + states?: ServerState[]; } /** Server-specific query */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 5b7b5a8e38..59cfeeb3bf 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -5674,6 +5674,11 @@ export type SearchDeploymentLogResponse = Log; export type SearchStackLogResponse = Log; export type SearchSwarmServiceLogResponse = Log; export interface ServerQuerySpecifics { + /** + * Query only for Servers matching these states. + * If empty, does not filter by state. + */ + states?: ServerState[]; } /** Server-specific query */ export type ServerQuery = ResourceQuery; diff --git a/ui/src/pages/terminals/new.tsx b/ui/src/pages/terminals/new.tsx index 493cb89dbb..db2276e7fb 100644 --- a/ui/src/pages/terminals/new.tsx +++ b/ui/src/pages/terminals/new.tsx @@ -237,9 +237,10 @@ function CreateServerTerminal({ }) { const nav = useNavigate(); const firstServer = - (useRead("ListServers", { limit: 0 }).data?.filter( - (s) => s.info.state === Types.ServerState.Ok, - ) ?? [])[0]?.id ?? ""; + (useRead("ListServers", { + query: { specific: { states: [Types.ServerState.Ok] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [server, _setServer] = useState(firstServer); const [changed, setChanged] = useState(false); const setServer = (server: string) => { @@ -295,7 +296,11 @@ function CreateContainerTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstServer = (useRead("ListServers", {}).data ?? [])[0]?.id ?? ""; + const firstServer = + (useRead("ListServers", { + query: { specific: { states: [Types.ServerState.Ok] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [params, _setParams] = useState({ server: firstServer, container: "" }); const [changed, setChanged] = useState(false); const setParams = (params: { server: string; container: string }) => { @@ -371,7 +376,15 @@ function CreateStackServiceTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstStack = (useRead("ListStacks", {}).data ?? [])[0]?.id ?? ""; + const firstStack = + (useRead("ListStacks", { + query: { + specific: { + states: [Types.StackState.Running, Types.StackState.Unhealthy], + }, + }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [params, _setParams] = useState({ stack: firstStack, service: "" }); const [changed, setChanged] = useState(false); const setParams = (params: { stack: string; service: string }) => { @@ -448,7 +461,10 @@ function CreateDeploymentTerminal({ }) { const nav = useNavigate(); const firstDeployment = - (useRead("ListDeployments", {}).data ?? [])[0]?.id ?? ""; + (useRead("ListDeployments", { + query: { specific: { states: [Types.DeploymentState.Running] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [deployment, _setDeployment] = useState(firstDeployment); const [changed, setChanged] = useState(false); const setDeployment = (deployment: string) => { From ae4e88425f42f0b5475efffc7beeb19aaa7d6881 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 16:01:51 -0700 Subject: [PATCH 111/203] deploy 2.3.0-dev-29 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b9c6c4f84..f10b5247ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 13523617d7..98dd7ba568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-28" +version = "2.3.0-dev-29" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 023754bbdb8b480a96b602e84da3bfb5eeeb1606 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 16:10:45 -0700 Subject: [PATCH 112/203] deployment / stack filter by swarm id --- client/core/rs/src/entities/deployment.rs | 10 ++++++++++ client/core/rs/src/entities/stack.rs | 9 +++++++++ client/core/ts/src/types.ts | 12 ++++++++++++ ui/public/client/types.d.ts | 12 ++++++++++++ ui/src/resources/swarm/tabs.tsx | 19 +++++++++---------- 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index a8bb3051c9..11ac1c922a 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -606,6 +606,12 @@ pub struct DeploymentQuerySpecifics { #[serde(default)] pub server_ids: Vec, + /// Query only for Deployments on these Swarms. + /// If empty, does not filter by Swarm. + /// Only accepts Swarm id (not name). + #[serde(default)] + pub swarm_ids: Vec, + /// Query only for Deployments with these Builds attached. /// If empty, does not filter by Build. /// Only accepts Build id (not name). @@ -628,6 +634,10 @@ impl super::resource::AddFilters for DeploymentQuerySpecifics { filters .insert("config.server_id", doc! { "$in": &self.server_ids }); } + if !self.swarm_ids.is_empty() { + filters + .insert("config.swarm_id", doc! { "$in": &self.swarm_ids }); + } if !self.build_ids.is_empty() { filters.insert("config.image.type", "Build"); filters.insert( diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 43ccf317ea..7a3b3aa550 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -944,6 +944,11 @@ pub struct StackQuerySpecifics { /// Only accepts Server id (not name). #[serde(default)] pub server_ids: Vec, + /// Query only for Stacks on these Swarms. + /// If empty, does not filter by Swarm. + /// Only accepts Swarm id (not name). + #[serde(default)] + pub swarm_ids: Vec, /// Query only for Stacks with these linked repos. /// Only accepts Repo id (not name). #[serde(default)] @@ -966,6 +971,10 @@ impl super::resource::AddFilters for StackQuerySpecifics { filters .insert("config.server_id", doc! { "$in": &self.server_ids }); } + if !self.swarm_ids.is_empty() { + filters + .insert("config.swarm_id", doc! { "$in": &self.swarm_ids }); + } if !self.linked_repos.is_empty() { filters.insert( "config.linked_repo", diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 1887863ec0..427ac43957 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1496,6 +1496,12 @@ export interface DeploymentQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Deployments on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Deployments with these Builds attached. * If empty, does not filter by Build. @@ -5847,6 +5853,12 @@ export interface StackQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Stacks on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Stacks with these linked repos. * Only accepts Repo id (not name). diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 59cfeeb3bf..396ea70479 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -1638,6 +1638,12 @@ export interface DeploymentQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Deployments on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Deployments with these Builds attached. * If empty, does not filter by Build. @@ -5690,6 +5696,12 @@ export interface StackQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Stacks on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Stacks with these linked repos. * Only accepts Repo id (not name). diff --git a/ui/src/resources/swarm/tabs.tsx b/ui/src/resources/swarm/tabs.tsx index 62adb7b0e1..86a90e3258 100644 --- a/ui/src/resources/swarm/tabs.tsx +++ b/ui/src/resources/swarm/tabs.tsx @@ -1,9 +1,6 @@ import { usePermissions, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { useLocalStorage } from "@mantine/hooks"; import { useMemo } from "react"; import { useSwarm } from "."; @@ -27,14 +24,16 @@ export default function SwarmTabs({ id }: { id: string }) { const swarmInfo = useSwarm(id)?.info; const stacks = - useRead("ListStacks", { limit: 0 }).data?.filter( - (stack) => stack.info.swarm_id === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { swarm_ids: [id] } }, + limit: 0, + }).data?.filter((stack) => stack.info.swarm_id === id) ?? []; const noStacks = stacks.length === 0; const deployments = - useRead("ListDeployments", { limit: 0 }).data?.filter( - (deployment) => deployment.info.swarm_id === id, - ) ?? []; + useRead("ListDeployments", { + query: { specific: { swarm_ids: [id] } }, + limit: 0, + }).data?.filter((deployment) => deployment.info.swarm_id === id) ?? []; const noDeployments = deployments.length === 0; const noResources = noDeployments && noStacks; From 5f685a29d2c188624c8a8470fc372dd08c86f3b7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 16:11:12 -0700 Subject: [PATCH 113/203] deploy 2.3.0-dev-30 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f10b5247ed..058beadbfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 98dd7ba568..270e14711e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-29" +version = "2.3.0-dev-30" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 3117aa26b4aab29c6f0b0ee0676e378f5858fdec Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 18:46:14 -0700 Subject: [PATCH 114/203] improve database query efficiency on large resource sets --- bin/core/src/api/read/action.rs | 26 +--- bin/core/src/api/read/alerter.rs | 7 +- bin/core/src/api/read/build.rs | 24 +-- bin/core/src/api/read/builder.rs | 7 +- bin/core/src/api/read/deployment.rs | 36 +---- bin/core/src/api/read/procedure.rs | 26 +--- bin/core/src/api/read/repo.rs | 24 +-- bin/core/src/api/read/server.rs | 26 +--- bin/core/src/api/read/stack.rs | 46 ++---- bin/core/src/api/read/swarm.rs | 7 +- bin/core/src/api/read/sync.rs | 7 +- bin/core/src/permission.rs | 234 ++++++++++++++++++---------- bin/core/src/resource/mod.rs | 81 +++++++--- 13 files changed, 273 insertions(+), 278 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 1067feadb1..3ef20317a4 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -47,32 +47,18 @@ impl Resolve for ListActions { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // When filtering by state, the db level pagination must be - // disabled, and applied in memory after the state filter. - let (db_limit, db_skip) = if states.is_empty() { - (limit, self.page * limit) - } else { - (0, 0) - }; - let actions = resource::list_for_user::( + let actions = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |action| { + states.is_empty() || states.contains(&action.info.state) + }, ) .await?; - let actions = if states.is_empty() { - actions - } else { - resource::filter_list_items_paginated( - actions, - |action| states.contains(&action.info.state), - limit, - self.page, - ) - }; Ok(actions) } } diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 9bb458907b..f988d8213a 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -47,13 +47,14 @@ impl Resolve for ListAlerters { }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index ab64cf83a9..a2e3ef246e 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -55,32 +55,16 @@ impl Resolve for ListBuilds { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // When filtering by state, the db level pagination must be - // disabled, and applied in memory after the state filter. - let (db_limit, db_skip) = if states.is_empty() { - (limit, self.page * limit) - } else { - (0, 0) - }; - let builds = resource::list_for_user::( + let builds = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |build| states.is_empty() || states.contains(&build.info.state), ) .await?; - let builds = if states.is_empty() { - builds - } else { - resource::filter_list_items_paginated( - builds, - |build| states.contains(&build.info.state), - limit, - self.page, - ) - }; Ok(builds) } } diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 004feedaae..a7cf154bb7 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -47,13 +47,14 @@ impl Resolve for ListBuilders { }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 61ba6257d7..7c9faf193e 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -65,40 +65,20 @@ impl Resolve for ListDeployments { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // Update available / state are computed in memory rather than - // stored on the db. When filtering on them, the db level - // pagination must be disabled, and applied in memory - // after the filters. - let use_db_pagination = - !only_update_available && states.is_empty(); - let (db_limit, db_skip) = if use_db_pagination { - (limit, self.page * limit) - } else { - (0, 0) - }; - let deployments = resource::list_for_user::( + let deployments = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |deployment| { + (!only_update_available || deployment.info.update_available) + && (states.is_empty() + || states.contains(&deployment.info.state)) + }, ) .await?; - let deployments = if use_db_pagination { - deployments - } else { - resource::filter_list_items_paginated( - deployments, - |deployment| { - (!only_update_available || deployment.info.update_available) - && (states.is_empty() - || states.contains(&deployment.info.state)) - }, - limit, - self.page, - ) - }; Ok(deployments) } } diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 0fbcdc1f0a..a378279bc4 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -45,32 +45,18 @@ impl Resolve for ListProcedures { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // When filtering by state, the db level pagination must be - // disabled, and applied in memory after the state filter. - let (db_limit, db_skip) = if states.is_empty() { - (limit, self.page * limit) - } else { - (0, 0) - }; - let procedures = resource::list_for_user::( + let procedures = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |procedure| { + states.is_empty() || states.contains(&procedure.info.state) + }, ) .await?; - let procedures = if states.is_empty() { - procedures - } else { - resource::filter_list_items_paginated( - procedures, - |procedure| states.contains(&procedure.info.state), - limit, - self.page, - ) - }; Ok(procedures) } } diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 7188d8dbb5..aed63671dd 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -45,32 +45,16 @@ impl Resolve for ListRepos { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // When filtering by state, the db level pagination must be - // disabled, and applied in memory after the state filter. - let (db_limit, db_skip) = if states.is_empty() { - (limit, self.page * limit) - } else { - (0, 0) - }; - let repos = resource::list_for_user::( + let repos = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |repo| states.is_empty() || states.contains(&repo.info.state), ) .await?; - let repos = if states.is_empty() { - repos - } else { - resource::filter_list_items_paginated( - repos, - |repo| states.contains(&repo.info.state), - limit, - self.page, - ) - }; Ok(repos) } } diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index bdebdb9b68..c5fa48788c 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -108,32 +108,18 @@ impl Resolve for ListServers { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // When filtering by state, the db level pagination must be - // disabled, and applied in memory after the state filter. - let (db_limit, db_skip) = if states.is_empty() { - (limit, self.page * limit) - } else { - (0, 0) - }; - let servers = resource::list_for_user::( + let servers = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |server| { + states.is_empty() || states.contains(&server.info.state) + }, ) .await?; - let servers = if states.is_empty() { - servers - } else { - resource::filter_list_items_paginated( - servers, - |server| states.contains(&server.info.state), - limit, - self.page, - ) - }; Ok(servers) } } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 2f77399b4e..2c6b740d7d 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -469,45 +469,25 @@ impl Resolve for ListStacks { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - // Update available / state are computed in memory rather than - // stored on the db. When filtering on them, the db level - // pagination must be disabled, and applied in memory - // after the filters. - let use_db_pagination = - !only_update_available && states.is_empty(); - let (db_limit, db_skip) = if use_db_pagination { - (limit, self.page * limit) - } else { - (0, 0) - }; - let stacks = resource::list_for_user::( + let stacks = resource::list_items_for_user::( self.query, - db_limit as i64, - db_skip, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |stack| { + (!only_update_available + || stack + .info + .services + .iter() + .any(|service| service.update_available)) + && (states.is_empty() + || states.contains(&stack.info.state)) + }, ) .await?; - let stacks = if use_db_pagination { - stacks - } else { - resource::filter_list_items_paginated( - stacks, - |stack| { - (!only_update_available - || stack - .info - .services - .iter() - .any(|service| service.update_available)) - && (states.is_empty() - || states.contains(&stack.info.state)) - }, - limit, - self.page, - ) - }; Ok(stacks) } } diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 6bdaec8ce6..5c93e6bcd8 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -45,13 +45,14 @@ impl Resolve for ListSwarms { }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 448cd30d50..09f94d316e 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -45,13 +45,14 @@ impl Resolve for ListResourceSyncs { }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index eda949c9b0..6f1a3d810c 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -4,7 +4,7 @@ use anyhow::{Context, anyhow}; use database::{ bson::Document, mongo_indexed::doc, mungos::find::find_collect, }; -use futures_util::{FutureExt, future::BoxFuture}; +use futures_util::{TryStreamExt, future::BoxFuture}; use indexmap::IndexSet; use komodo_client::{ api::read::GetPermission, @@ -16,7 +16,9 @@ use komodo_client::{ builder::Builder, deployment::Deployment, permission::SpecificPermission, - permission::{PermissionLevel, PermissionLevelAndSpecifics}, + permission::{ + Permission, PermissionLevel, PermissionLevelAndSpecifics, + }, procedure::Procedure, repo::Repo, resource::Resource, @@ -176,16 +178,34 @@ pub fn get_user_permission_on_resource<'a, T: KomodoResource>( }) } -pub async fn list_resources_for_user( - filters: impl Into>, - limit: impl Into>, - skip: impl Into>, +/// Precomputed user permissions for listing resources of a type. +/// Load with [load_list_permits], then check visibility of +/// each resource with [ListPermits::permitted]. +pub enum ListPermits { + /// The user can see all resources of the type, + /// eg. admin, transparent mode, or 'all' access on the variant. + Unrestricted, + /// Visibility must be checked per-resource + /// against the permissions table. + Fine(FineListPermits), +} + +pub struct FineListPermits { + required: PermissionLevelAndSpecifics, + base: PermissionLevelAndSpecifics, + permission_by_resource_id: HashMap, + additional_specific_cache: + HashMap>, + user: User, +} + +pub async fn load_list_permits( user: &User, - permission: PermissionLevelAndSpecifics, -) -> anyhow::Result>> { + required: PermissionLevelAndSpecifics, +) -> anyhow::Result { // Check admin if user.admin { - return list_all_resources::(filters, limit, skip).await; + return Ok(ListPermits::Unrestricted); } let mut base = PermissionLevelAndSpecifics { @@ -198,8 +218,8 @@ pub async fn list_resources_for_user( }; // 'transparent_mode' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters, limit, skip).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } let resource_type = T::resource_type(); @@ -208,8 +228,8 @@ pub async fn list_resources_for_user( if let Some(all_permission) = user.all.get(&resource_type) { base.elevate(all_permission); // 'user.all' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters, limit, skip).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } } @@ -219,25 +239,23 @@ pub async fn list_resources_for_user( if let Some(all_permission) = group.all.get(&resource_type) { base.elevate(all_permission); // 'group.all' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters, limit, skip).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } } } - let (all, permissions) = tokio::try_join!( - list_all_resources::(filters, None, None), - // And any ids using the permissions table - find_collect( - &db_client().permissions, - doc! { - "$or": user_target_query(&user.id, &groups)?, - "resource_target.type": resource_type.as_ref(), - }, - None, - ) - .map(|res| res.context("failed to query permissions on db")) - )?; + // Pull any permissions on the variant using the permissions table + let permissions = find_collect( + &db_client().permissions, + doc! { + "$or": user_target_query(&user.id, &groups)?, + "resource_target.type": resource_type.as_ref(), + }, + None, + ) + .await + .context("failed to query permissions on db")?; let permission_by_resource_id = permissions .into_iter() @@ -249,34 +267,48 @@ pub async fn list_resources_for_user( }) .collect::>(); - let mut resources = Vec::new(); - let mut additional_specific_cache = - HashMap::>::new(); + Ok(ListPermits::Fine(FineListPermits { + required, + base, + permission_by_resource_id, + additional_specific_cache: Default::default(), + user: user.clone(), + })) +} + +impl ListPermits { + /// Check whether the user can see the given resource. + pub async fn permitted( + &mut self, + resource: &Resource, + ) -> anyhow::Result { + let ListPermits::Fine(fine) = self else { + return Ok(true); + }; - for resource in all { let mut perm = if let Some(perm) = - permission_by_resource_id.get(&resource.id) + fine.permission_by_resource_id.get(&resource.id) { - base.join(perm) + fine.base.join(perm) } else { - base.clone() + fine.base.clone() }; // Check if already fulfils - if perm.fulfills(&permission) { - resources.push(resource); - continue; + if perm.fulfills(&fine.required) { + return Ok(true); } // Also check if fulfills with inherited specific let additional_target = if let Some(additional_target) = - T::inherit_specific_permissions_from(&resource) + T::inherit_specific_permissions_from(resource) && !additional_target.is_empty() { additional_target } else { - continue; + return Ok(false); }; - let additional_specific = match additional_specific_cache + let additional_specific = match fine + .additional_specific_cache .get(&additional_target) .cloned() { @@ -285,21 +317,87 @@ pub async fn list_resources_for_user( let specific = GetPermission { target: additional_target.clone(), } - .resolve(&ReadArgs { user: user.clone() }) + .resolve(&ReadArgs { + user: fine.user.clone(), + }) .await .map_err(|e| e.error) .context( "failed to get user permission on additional target", )? .specific; - additional_specific_cache + fine + .additional_specific_cache .insert(additional_target, specific.clone()); specific } }; perm.specific.extend(additional_specific); - if perm.fulfills(&permission) { - resources.push(resource); + Ok(perm.fulfills(&fine.required)) + } +} + +pub async fn list_resources_for_user( + filters: impl Into>, + limit: impl Into>, + skip: impl Into>, + user: &User, + permission: PermissionLevelAndSpecifics, +) -> anyhow::Result>> { + let mut permits = load_list_permits::(user, permission).await?; + + if let ListPermits::Unrestricted = permits { + return list_all_resources::(filters, limit, skip).await; + } + + list_resources_with_permits::( + &mut permits, + filters.into(), + limit.into(), + skip.into(), + ) + .await +} + +/// Fine grained permissions: drive the cursor directly, checking +/// each resource against the user permissions, with limit / skip +/// applied in memory after the permission filter. Stops pulling +/// from the cursor as soon as the limit is reached, and avoids +/// collecting resources the user cannot see. +async fn list_resources_with_permits( + permits: &mut ListPermits, + filters: Option, + limit: Option, + skip: Option, +) -> anyhow::Result>> { + let mut cursor = T::coll() + .find(filters.unwrap_or_default()) + .sort(doc! { "name": 1 }) + .await + .with_context(|| { + format!("Failed to query db for {}s", T::resource_type()) + })?; + + let limit = limit.unwrap_or_default().max(0) as usize; + let skip = skip.unwrap_or_default(); + let mut skipped = 0; + let mut resources = Vec::new(); + + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + if skipped < skip { + skipped += 1; + continue; + } + resources.push(resource); + if limit != 0 && resources.len() >= limit { + break; } } @@ -314,49 +412,17 @@ pub async fn list_resource_ids_for_user( user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result>> { - // Check admin - if user.admin { - return Ok(None); - } + let mut permits = load_list_permits::(user, permission).await?; - let mut base = PermissionLevelAndSpecifics { - level: if core_config().transparent_mode { - PermissionLevel::Read - } else { - PermissionLevel::None - }, - specific: Default::default(), - }; - - // 'transparent_mode' early return. - if base.fulfills(&permission) { + if let ListPermits::Unrestricted = permits { return Ok(None); } - let resource_type = T::resource_type(); - - if let Some(all) = user.all.get(&resource_type) { - base.elevate(all); - // 'user.all' early return. - if base.fulfills(&permission) { - return Ok(None); - } - } - - // Check user groups 'all' on variant - let groups = get_user_user_groups(&user.id).await?; - for group in &groups { - if let Some(all) = group.all.get(&resource_type) { - base.elevate(all); - // 'group.all' early return. - if base.fulfills(&permission) { - return Ok(None); - } - } - } - - let ids = list_resources_for_user::( - filters, limit, skip, user, permission, + let ids = list_resources_with_permits::( + &mut permits, + filters, + limit.into(), + skip.into(), ) .await? .into_iter() diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 11b1bc5bda..60aea550ed 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -17,7 +17,7 @@ use database::{ }, }; use formatting::format_serror; -use futures_util::future::join_all; +use futures_util::{TryStreamExt, future::join_all}; use indexmap::IndexSet; use komodo_client::{ api::{read::ExportResourcesToToml, write::CreateTag}, @@ -276,28 +276,67 @@ pub async fn list_all_resources( }) } -/// Some list item filters (eg. state, update available) are computed -/// from in-memory caches rather than stored on the database, and can -/// only be applied after the db query converts to list items. -/// Callers using these filters should pass `(0, 0)` limit / skip to -/// the db level query, and apply the equivalent pagination here -/// after filtering. -pub fn filter_list_items_paginated( - items: Vec, - filter: impl Fn(&T) -> bool, +/// List item pagination for the `List` apis, driving the +/// mongo cursor directly instead of collecting the full resource +/// list in memory. Each resource pulled from the cursor is checked +/// against the user permissions and converted to its list item, and +/// `filter` is applied before the item counts toward `limit` / `page`. +/// This is required because some list item fields (eg. state, +/// update available) are computed from in-memory caches rather than +/// stored on the database, so they cannot be part of the db query. +/// Stops pulling from the cursor as soon as the page is full. +pub async fn list_items_for_user( + mut query: ResourceQuery, limit: u64, page: u64, -) -> Vec { - items - .into_iter() - .filter(|item| filter(item)) - .skip((page * limit) as usize) - .take(if limit == 0 { - usize::MAX - } else { - limit as usize - }) - .collect() + user: &User, + permission: PermissionLevelAndSpecifics, + all_tags: &[Tag], + filter: impl Fn(&T::ListItem) -> bool, +) -> anyhow::Result> { + validate_resource_query_tags(&mut query, all_tags)?; + let mut filters = Document::new(); + query.add_filters(&mut filters); + + let mut permits = + crate::permission::load_list_permits::(user, permission) + .await?; + + let mut cursor = T::coll() + .find(filters) + .sort(doc! { "name": 1 }) + .await + .with_context(|| { + format!("Failed to query db for {}s", T::resource_type()) + })?; + + let skip = page * limit; + let mut skipped = 0; + let mut items = Vec::new(); + + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let item = T::to_list_item(resource).await; + if !filter(&item) { + continue; + } + if skipped < skip { + skipped += 1; + continue; + } + items.push(item); + if limit != 0 && items.len() as u64 >= limit { + break; + } + } + + Ok(items) } pub async fn list_for_user( From 6513cf297b1d0fc2d95518cdd9841c8c7723b5da Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 19:13:21 -0700 Subject: [PATCH 115/203] fix ListFull not filtering by query states --- bin/core/src/api/read/action.rs | 21 +++++++++++--- bin/core/src/api/read/build.rs | 20 +++++++++++-- bin/core/src/api/read/deployment.rs | 27 +++++++++++++++--- bin/core/src/api/read/procedure.rs | 22 ++++++++++++--- bin/core/src/api/read/repo.rs | 20 +++++++++++-- bin/core/src/api/read/server.rs | 25 +++++++++++++--- bin/core/src/api/read/stack.rs | 24 +++++++++++++--- bin/core/src/helpers/query.rs | 34 +++++++++++++++++++++- bin/core/src/permission.rs | 33 +++++++++++++++------- bin/core/src/resource/build.rs | 4 ++- bin/core/src/resource/deployment.rs | 15 ++-------- bin/core/src/resource/mod.rs | 44 +++++++++++++++++++++++++++-- bin/core/src/resource/repo.rs | 4 ++- bin/core/src/resource/stack.rs | 19 +++---------- 14 files changed, 243 insertions(+), 69 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 3ef20317a4..2dcd1aad98 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -11,7 +11,7 @@ use komodo_client::{ use mogh_resolver::Resolve; use crate::{ - helpers::query::get_all_tags, + helpers::query::{get_action_state, get_all_tags}, permission::get_check_permissions, resource, state::{action_state_cache, action_states}, @@ -73,15 +73,28 @@ impl Resolve for ListFullActions { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |action| { + let states = states.clone(); + async move { + if states.is_empty() + || states.contains(&get_action_state(&action.id).await) + { + Some(action) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index a2e3ef246e..87476ff470 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -79,15 +79,29 @@ impl Resolve for ListFullBuilds { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |build| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&resource::get_build_state(&build.id).await) + { + Some(build) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 7c9faf193e..c85cf6f6cd 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -25,7 +25,9 @@ use reqwest::StatusCode; use crate::{ helpers::{ - periphery_client, query::get_all_tags, swarm::swarm_request, + periphery_client, + query::{get_all_tags, get_deployment_state}, + swarm::swarm_request, }, permission::get_check_permissions, resource::{self, setup_deployment_execution}, @@ -93,15 +95,32 @@ impl Resolve for ListFullDeployments { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |deployment| { + let states = states.clone(); + async move { + if states.is_empty() + || states.contains( + &get_deployment_state(&deployment.id) + .await + .unwrap_or_default(), + ) + { + Some(deployment) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index a378279bc4..192f02bd35 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -9,7 +9,7 @@ use komodo_client::{ use mogh_resolver::Resolve; use crate::{ - helpers::query::get_all_tags, + helpers::query::{get_all_tags, get_procedure_state}, permission::get_check_permissions, resource, state::{action_states, procedure_state_cache}, @@ -71,15 +71,29 @@ impl Resolve for ListFullProcedures { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |procedure| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_procedure_state(&procedure.id).await) + { + Some(procedure) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index aed63671dd..1018b49711 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -69,15 +69,29 @@ impl Resolve for ListFullRepos { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |repo| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&resource::get_repo_state(&repo.id).await) + { + Some(repo) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index c5fa48788c..7bf89f0315 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -28,7 +28,10 @@ use reqwest::StatusCode; use tokio::sync::Mutex; use crate::{ - helpers::{periphery_client, query::get_all_tags}, + helpers::{ + periphery_client, + query::{get_all_tags, get_cached_server_state}, + }, permission::get_check_permissions, resource, state::{action_states, db_client, server_status_cache}, @@ -134,15 +137,29 @@ impl Resolve for ListFullServers { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |server| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_cached_server_state(&server.id).await) + { + Some(server) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 2c6b740d7d..77bdd32716 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -25,7 +25,9 @@ use reqwest::StatusCode; use crate::{ helpers::{ - periphery_client, query::get_all_tags, swarm::swarm_request, + periphery_client, + query::{get_all_tags, get_cached_stack_state}, + swarm::swarm_request, }, permission::get_check_permissions, resource, @@ -502,15 +504,29 @@ impl Resolve for ListFullStacks { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, - limit as i64, - self.page * limit, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |stack| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_cached_stack_state(&stack.id).await) + { + Some(stack) + } else { + None + } + } + }, ) .await?, ) diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs index 98d7ca4780..536aaee320 100644 --- a/bin/core/src/helpers/query.rs +++ b/bin/core/src/helpers/query.rs @@ -45,7 +45,7 @@ use crate::{ state::{ action_state_cache, action_states, db_client, deployment_status_cache, procedure_state_cache, - stack_status_cache, + server_status_cache, stack_status_cache, }, }; @@ -97,6 +97,38 @@ pub async fn get_server_state(server: &Server) -> ServerState { } } +/// The Server state as computed for the server list items, +/// from the in memory status cache. +pub async fn get_cached_server_state(id: &String) -> ServerState { + server_status_cache() + .get(id) + .await + .map(|status| status.state) + .unwrap_or_default() +} + +/// The Stack state as computed for the stack list items, +/// from the in memory action states / status cache. +pub async fn get_cached_stack_state(id: &String) -> StackState { + if action_states() + .stack + .get(id) + .await + .map(|s| s.get().map(|s| s.deploying)) + .transpose() + .ok() + .flatten() + .unwrap_or_default() + { + return StackState::Deploying; + } + stack_status_cache() + .get(id) + .await + .map(|status| status.curr.state) + .unwrap_or_default() +} + pub async fn get_deployment_state( id: &String, ) -> anyhow::Result { diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index 6f1a3d810c..ac893b027a 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, future::Future}; use anyhow::{Context, anyhow}; use database::{ @@ -350,26 +350,35 @@ pub async fn list_resources_for_user( return list_all_resources::(filters, limit, skip).await; } - list_resources_with_permits::( + list_resources_with_permits::( &mut permits, filters.into(), limit.into(), skip.into(), + |resource| async move { Some(resource) }, ) .await } -/// Fine grained permissions: drive the cursor directly, checking -/// each resource against the user permissions, with limit / skip -/// applied in memory after the permission filter. Stops pulling -/// from the cursor as soon as the limit is reached, and avoids -/// collecting resources the user cannot see. -async fn list_resources_with_permits( +/// Drive the cursor directly, checking each resource against the +/// user permissions and the additional `filter` (eg. by state +/// computed from the in memory caches), with limit / skip applied +/// in memory after the filters. Stops pulling from the cursor as +/// soon as the limit is reached, and avoids collecting resources +/// the user cannot see. +/// +/// `filter` receives each resource by value, and returns +/// `Some(resource)` to keep it in the results, or `None` to drop it. +pub async fn list_resources_with_permits( permits: &mut ListPermits, filters: Option, limit: Option, skip: Option, -) -> anyhow::Result>> { + filter: impl Fn(Resource) -> F, +) -> anyhow::Result>> +where + F: Future>> + Send, +{ let mut cursor = T::coll() .find(filters.unwrap_or_default()) .sort(doc! { "name": 1 }) @@ -391,6 +400,9 @@ async fn list_resources_with_permits( if !permits.permitted::(&resource).await? { continue; } + let Some(resource) = filter(resource).await else { + continue; + }; if skipped < skip { skipped += 1; continue; @@ -418,11 +430,12 @@ pub async fn list_resource_ids_for_user( return Ok(None); } - let ids = list_resources_with_permits::( + let ids = list_resources_with_permits::( &mut permits, filters, limit.into(), skip.into(), + |resource| async move { Some(resource) }, ) .await? .into_iter() diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index 2319761c7d..d06b0b75f7 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -297,7 +297,9 @@ async fn validate_config( Ok(()) } -async fn get_build_state(id: &String) -> BuildState { +/// The Build state as computed for the build list items, +/// from the in memory action states / state cache. +pub async fn get_build_state(id: &String) -> BuildState { if action_states() .build .get(id) diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index ecf18cd56f..8a196864ad 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -93,20 +93,9 @@ impl super::KomodoResource for Deployment { deployment: Resource, ) -> Self::ListItem { let status = deployment_status_cache().get(&deployment.id).await; - let state = if action_states() - .deployment - .get(&deployment.id) + let state = get_deployment_state(&deployment.id) .await - .map(|s| s.get().map(|s| s.deploying)) - .transpose() - .ok() - .flatten() - .unwrap_or_default() - { - DeploymentState::Deploying - } else { - status.as_ref().map(|s| s.curr.state).unwrap_or_default() - }; + .unwrap_or_default(); let all = all_resources_cache().load(); let server_name = all .servers diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 60aea550ed..45487f2839 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + future::Future, str::FromStr, }; @@ -70,7 +71,8 @@ pub use action::{ refresh_action_state_cache, spawn_action_state_refresh_loop, }; pub use build::{ - refresh_build_state_cache, spawn_build_state_refresh_loop, + get_build_state, refresh_build_state_cache, + spawn_build_state_refresh_loop, }; pub use deployment::setup_deployment_execution; pub use procedure::{ @@ -82,7 +84,8 @@ pub use refresh::{ spawn_resource_refresh_loop, }; pub use repo::{ - refresh_repo_state_cache, spawn_repo_state_refresh_loop, + get_repo_state, refresh_repo_state_cache, + spawn_repo_state_refresh_loop, }; pub use server::{rotate_server_keys, update_server_public_key}; @@ -445,6 +448,43 @@ pub async fn list_full_for_user_using_pattern( ) } +/// Same as [list_full_for_user], but applies an additional in-memory +/// `filter` (eg. by state computed from the in memory caches), with +/// limit / page applied after the filter. Required because such +/// filters cannot be expressed in the db level query. +/// +/// `filter` receives each resource by value, and returns +/// `Some(resource)` to keep it in the results, or `None` to drop it. +pub async fn list_full_for_user_filtered( + mut query: ResourceQuery, + limit: u64, + page: u64, + user: &User, + permission: PermissionLevelAndSpecifics, + all_tags: &[Tag], + filter: impl Fn(Resource) -> F, +) -> anyhow::Result>> +where + F: Future< + Output = Option>, + > + Send, +{ + validate_resource_query_tags(&mut query, all_tags)?; + let mut filters = Document::new(); + query.add_filters(&mut filters); + let mut permits = + crate::permission::load_list_permits::(user, permission) + .await?; + crate::permission::list_resources_with_permits::( + &mut permits, + Some(filters), + Some(limit as i64), + Some(page.saturating_mul(limit)), + filter, + ) + .await +} + pub async fn list_full_for_user( mut query: ResourceQuery, limit: impl Into>, diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 2774e3c105..8949730f14 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -271,7 +271,9 @@ async fn validate_config( Ok(()) } -async fn get_repo_state(id: &String) -> RepoState { +/// The Repo state as computed for the repo list items, +/// from the in memory action states / state cache. +pub async fn get_repo_state(id: &String) -> RepoState { if let Some(state) = action_states() .repo .get(id) diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index 2f6abe2b73..8668c35464 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -31,7 +31,9 @@ use crate::{ config::core_config, helpers::{ periphery_client, - query::{get_stack_state, get_swarm_or_server}, + query::{ + get_cached_stack_state, get_stack_state, get_swarm_or_server, + }, repo_link, swarm::swarm_request, }, @@ -95,20 +97,7 @@ impl super::KomodoResource for Stack { stack: Resource, ) -> Self::ListItem { let status = stack_status_cache().get(&stack.id).await; - let state = if action_states() - .stack - .get(&stack.id) - .await - .map(|s| s.get().map(|s| s.deploying)) - .transpose() - .ok() - .flatten() - .unwrap_or_default() - { - StackState::Deploying - } else { - status.as_ref().map(|s| s.curr.state).unwrap_or_default() - }; + let state = get_cached_stack_state(&stack.id).await; let project_name = stack.project_name(false); let services = status .as_ref() From ef2b6ebc46ea670e4e0e459608a6a97245239a80 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 19:15:53 -0700 Subject: [PATCH 116/203] fix the list permits respect resource.base_permission --- bin/core/src/permission.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index ac893b027a..148db07ac9 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -293,6 +293,9 @@ impl ListPermits { } else { fine.base.clone() }; + // Add in the resource level base permissions, + // matching [get_user_permission_on_resource]. + perm.elevate(&resource.base_permission); // Check if already fulfils if perm.fulfills(&fine.required) { return Ok(true); From 42367c530b0c458dc7b26a62823b17d05e8a0628 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 19:20:56 -0700 Subject: [PATCH 117/203] use saturating mul in skip computations --- bin/core/src/api/read/alert.rs | 2 +- bin/core/src/api/read/alerter.rs | 2 +- bin/core/src/api/read/build.rs | 3 ++- bin/core/src/api/read/builder.rs | 2 +- bin/core/src/api/read/server.rs | 2 +- bin/core/src/api/read/swarm.rs | 2 +- bin/core/src/api/read/sync.rs | 2 +- bin/core/src/api/read/update.rs | 2 +- bin/core/src/resource/mod.rs | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs index a543fadf8f..8f04e78854 100644 --- a/bin/core/src/api/read/alert.rs +++ b/bin/core/src/api/read/alert.rs @@ -40,7 +40,7 @@ impl Resolve for ListAlerts { FindOptions::builder() .sort(doc! { "ts": -1 }) .limit(NUM_ALERTS_PER_PAGE as i64) - .skip(self.page * NUM_ALERTS_PER_PAGE) + .skip(self.page.saturating_mul(NUM_ALERTS_PER_PAGE)) .build(), ) .await diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index f988d8213a..3db5c647e7 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -76,7 +76,7 @@ impl Resolve for ListFullAlerters { resource::list_full_for_user::( self.query, limit as i64, - self.page * limit, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 87476ff470..6b78f83ce2 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -187,7 +187,8 @@ impl Resolve for GetBuildMonthlyStats { let curr_ts = unix_timestamp_ms() as i64; let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; - let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS; + let close_ts = next_day + - (self.page as i64).saturating_mul(30 * ONE_DAY_MS); let open_ts = close_ts - 30 * ONE_DAY_MS; let mut build_updates = db_client() diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index a7cf154bb7..7ffc407c58 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -76,7 +76,7 @@ impl Resolve for ListFullBuilders { resource::list_full_for_user::( self.query, limit as i64, - self.page * limit, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 7bf89f0315..cf54a64b88 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -360,7 +360,7 @@ impl Resolve for GetHistoricalServerStats { }, FindOptions::builder() .sort(doc! { "ts": -1 }) - .skip(page as u64 * STATS_PER_PAGE as u64) + .skip((page as u64).saturating_mul(STATS_PER_PAGE as u64)) .limit(STATS_PER_PAGE) .build(), ) diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 5c93e6bcd8..93ee798e87 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -74,7 +74,7 @@ impl Resolve for ListFullSwarms { resource::list_full_for_user::( self.query, limit as i64, - self.page * limit, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 09f94d316e..b4f58cbbf9 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -74,7 +74,7 @@ impl Resolve for ListFullResourceSyncs { resource::list_full_for_user::( self.query, limit as i64, - self.page * limit, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs index a24e642910..0fa9502c34 100644 --- a/bin/core/src/api/read/update.rs +++ b/bin/core/src/api/read/update.rs @@ -47,7 +47,7 @@ impl Resolve for ListUpdates { query, FindOptions::builder() .sort(doc! { "start_ts": -1 }) - .skip(self.page as u64 * UPDATES_PER_PAGE as u64) + .skip((self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64)) .limit(UPDATES_PER_PAGE) .build(), ) diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 45487f2839..1c5e233404 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -313,7 +313,7 @@ pub async fn list_items_for_user( format!("Failed to query db for {}s", T::resource_type()) })?; - let skip = page * limit; + let skip = page.saturating_mul(limit); let mut skipped = 0; let mut items = Vec::new(); From 00f77c249c17dbeb740f3810b97d0f323930962f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 19:21:40 -0700 Subject: [PATCH 118/203] Fix list all containers / stack services - tags query, terms case insensitive --- bin/core/src/api/read/docker.rs | 27 +++++++++++++++++++++------ bin/core/src/api/read/stack.rs | 25 ++++++++++++++++++++----- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 80ea89c7d5..4086e3b098 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -26,7 +26,7 @@ use periphery_client::api as periphery; use crate::{ api::read::ReadArgs, - helpers::periphery_client, + helpers::{periphery_client, query::get_all_tags}, permission::{get_check_permissions, list_resources_for_user}, resource, stack::compose_container_match_regex, @@ -80,6 +80,11 @@ impl Resolve for ListAllContainers { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; let servers = resource::list_for_user::( ServerQuery::builder() .names(self.servers.clone()) @@ -89,7 +94,7 @@ impl Resolve for ListAllContainers { None, user, PermissionLevel::Read.into(), - &[], + &all_tags, ) .await?; @@ -97,6 +102,14 @@ impl Resolve for ListAllContainers { let mut skipped = 0; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let limit_usize = limit as usize; + // Eg. page 1 skips until after 100 containers, page 2 after 200. + let skip = limit.saturating_mul(self.page); + // Match terms case insensitively. + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); for server in servers { let cache = server_status_cache() @@ -112,13 +125,15 @@ impl Resolve for ListAllContainers { // Apply state filter if defined. (self.state.is_empty() || self.state.contains(&container.state)) && // Apply terms filter if defined - (self.terms.is_empty() + (terms.is_empty() // Match when all terms contained within a name. - || self.terms.iter().all(|term| container.name.contains(term))) + || { + let name = container.name.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }) }); for container in more { - if skipped < limit * self.page { - // Eg. page 1 skips until after 100 containers, page 2 after 200. + if skipped < skip { skipped += 1; } else { // push and maybe early return diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 77bdd32716..59746fe546 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -82,6 +82,11 @@ impl Resolve for ListAllStackServices { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; let stacks = resource::list_for_user::( StackQuery::builder() .names(self.stacks.clone()) @@ -91,7 +96,7 @@ impl Resolve for ListAllStackServices { None, user, PermissionLevel::Read.into(), - &[], + &all_tags, ) .await?; @@ -99,6 +104,14 @@ impl Resolve for ListAllStackServices { let mut skipped = 0; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); let limit_usize = limit as usize; + // Eg. page 1 skips until after 100 services, page 2 after 200. + let skip = limit.saturating_mul(self.page); + // Match terms case insensitively. + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); for stack in stacks { let cache = @@ -109,13 +122,15 @@ impl Resolve for ListAllStackServices { // Apply state filter if defined. (self.state.is_empty() || self.state.contains(&service.state)) && // Apply terms filter if defined - (self.terms.is_empty() + (terms.is_empty() // Match when all terms contained within a name. - || self.terms.iter().all(|term| service.service.contains(term))) + || { + let name = service.service.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }) }); for service in more { - if skipped < limit * self.page { - // Eg. page 1 skips until after 100 services, page 2 after 200. + if skipped < skip { skipped += 1; } else { // push and maybe early return From 52f5a4bbfb825c78a0b8fff5dfead1b91919ea5d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 19:22:14 -0700 Subject: [PATCH 119/203] deploy 2.3.0-dev-31 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 058beadbfd..1ee6493f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 270e14711e..25b21e9eee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-30" +version = "2.3.0-dev-31" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From a4b4b9bfce97fc652726891d5f1f041df8b78274 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 22:39:57 -0700 Subject: [PATCH 120/203] api based list resource sorting which works with pagination --- bin/core/src/api/listener/resources.rs | 2 +- bin/core/src/api/read/action.rs | 19 +++- bin/core/src/api/read/alerter.rs | 14 ++- bin/core/src/api/read/build.rs | 24 ++++- bin/core/src/api/read/builder.rs | 16 +++- bin/core/src/api/read/deployment.rs | 33 ++++++- bin/core/src/api/read/procedure.rs | 20 +++- bin/core/src/api/read/repo.rs | 21 +++- bin/core/src/api/read/server.rs | 22 ++++- bin/core/src/api/read/stack.rs | 36 ++++++- bin/core/src/api/read/swarm.rs | 15 ++- bin/core/src/api/read/sync.rs | 22 +++++ bin/core/src/api/read/update.rs | 4 +- bin/core/src/resource/mod.rs | 111 ++++++++++++++++------ client/core/rs/src/api/read/action.rs | 15 ++- client/core/rs/src/api/read/alerter.rs | 12 ++- client/core/rs/src/api/read/build.rs | 14 ++- client/core/rs/src/api/read/builder.rs | 12 ++- client/core/rs/src/api/read/deployment.rs | 12 ++- client/core/rs/src/api/read/docker.rs | 2 +- client/core/rs/src/api/read/procedure.rs | 12 ++- client/core/rs/src/api/read/repo.rs | 14 ++- client/core/rs/src/api/read/server.rs | 12 ++- client/core/rs/src/api/read/stack.rs | 12 ++- client/core/rs/src/api/read/swarm.rs | 14 ++- client/core/rs/src/api/read/sync.rs | 12 ++- client/core/rs/src/api/write/provider.rs | 2 +- client/core/rs/src/entities/action.rs | 15 +++ client/core/rs/src/entities/alerter.rs | 15 +++ client/core/rs/src/entities/build.rs | 15 +++ client/core/rs/src/entities/builder.rs | 15 +++ client/core/rs/src/entities/deployment.rs | 17 ++++ client/core/rs/src/entities/procedure.rs | 15 +++ client/core/rs/src/entities/repo.rs | 17 ++++ client/core/rs/src/entities/server.rs | 17 ++++ client/core/rs/src/entities/stack.rs | 17 ++++ client/core/rs/src/entities/swarm.rs | 13 +++ client/core/rs/src/entities/sync.rs | 17 ++++ 38 files changed, 620 insertions(+), 57 deletions(-) diff --git a/bin/core/src/api/listener/resources.rs b/bin/core/src/api/listener/resources.rs index c6be345cc4..385dc58a06 100644 --- a/bin/core/src/api/listener/resources.rs +++ b/bin/core/src/api/listener/resources.rs @@ -86,7 +86,7 @@ pub async fn handle_build_webhook( }) .await .ok(); - + poll_build_until_cancelled(&build.id).await?; } diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 2dcd1aad98..40065fb328 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -3,7 +3,8 @@ use komodo_client::{ api::read::*, entities::{ action::{ - Action, ActionActionState, ActionListItem, ActionState, + Action, ActionActionState, ActionListItem, ActionSortBy, + ActionState, }, permission::PermissionLevel, }, @@ -47,10 +48,26 @@ impl Resolve for ListActions { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + ActionSortBy::Name => resource::ListItemSort::Name, + ActionSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + ActionSortBy::NextRun => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.next_scheduled_run.cmp(&b.info.next_scheduled_run) + })) + } + }; let actions = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 3db5c647e7..932497144a 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc; use komodo_client::{ api::read::*, entities::{ - alerter::{Alerter, AlerterListItem}, + alerter::{Alerter, AlerterListItem, AlerterSortBy}, permission::PermissionLevel, }, }; @@ -46,11 +46,23 @@ impl Resolve for ListAlerters { get_all_tags(None).await? }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + AlerterSortBy::Name => resource::ListItemSort::Name, + AlerterSortBy::Type => { + resource::ListItemSort::DbField("config.endpoint.type") + } + AlerterSortBy::Enabled => { + resource::ListItemSort::DbField("config.enabled") + } + }; Ok( resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 6b78f83ce2..5415401dae 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -11,7 +11,9 @@ use komodo_client::{ api::read::*, entities::{ Operation, - build::{Build, BuildActionState, BuildListItem, BuildState}, + build::{ + Build, BuildActionState, BuildListItem, BuildSortBy, BuildState, + }, permission::PermissionLevel, update::UpdateStatus, }, @@ -55,10 +57,26 @@ impl Resolve for ListBuilds { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + BuildSortBy::Name => resource::ListItemSort::Name, + BuildSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.repo.cmp(&b.info.repo) + })) + } + BuildSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; let builds = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, @@ -187,8 +205,8 @@ impl Resolve for GetBuildMonthlyStats { let curr_ts = unix_timestamp_ms() as i64; let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; - let close_ts = next_day - - (self.page as i64).saturating_mul(30 * ONE_DAY_MS); + let close_ts = + next_day - (self.page as i64).saturating_mul(30 * ONE_DAY_MS); let open_ts = close_ts - 30 * ONE_DAY_MS; let mut build_updates = db_client() diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 7ffc407c58..7564048dad 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc; use komodo_client::{ api::read::*, entities::{ - builder::{Builder, BuilderListItem}, + builder::{Builder, BuilderListItem, BuilderSortBy}, permission::PermissionLevel, }, }; @@ -46,11 +46,25 @@ impl Resolve for ListBuilders { get_all_tags(None).await? }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + BuilderSortBy::Name => resource::ListItemSort::Name, + BuilderSortBy::Provider => { + resource::ListItemSort::DbField("config.type") + } + BuilderSortBy::InstanceType => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.instance_type.cmp(&b.info.instance_type) + })) + } + }; Ok( resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index c85cf6f6cd..022258ebc0 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -7,7 +7,7 @@ use komodo_client::{ SwarmOrServer, deployment::{ Deployment, DeploymentActionState, DeploymentConfig, - DeploymentListItem, DeploymentState, + DeploymentListItem, DeploymentSortBy, DeploymentState, }, docker::{ container::{Container, ContainerStats}, @@ -67,10 +67,41 @@ impl Resolve for ListDeployments { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + DeploymentSortBy::Name => resource::ListItemSort::Name, + DeploymentSortBy::Image => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.image.cmp(&b.info.image) + })) + } + DeploymentSortBy::Host => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + let host_a = if a.info.swarm_id.is_empty() { + &a.info.server_name + } else { + &a.info.swarm_name + }; + let host_b = if b.info.swarm_id.is_empty() { + &b.info.server_name + } else { + &b.info.swarm_name + }; + host_a.cmp(host_b) + })) + } + DeploymentSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; let deployments = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 192f02bd35..627e86addd 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -3,7 +3,9 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - procedure::{Procedure, ProcedureState}, + procedure::{ + Procedure, ProcedureListItem, ProcedureSortBy, ProcedureState, + }, }, }; use mogh_resolver::Resolve; @@ -45,10 +47,26 @@ impl Resolve for ListProcedures { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + ProcedureSortBy::Name => resource::ListItemSort::Name, + ProcedureSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + ProcedureSortBy::NextRun => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.next_scheduled_run.cmp(&b.info.next_scheduled_run) + })) + } + }; let procedures = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 1018b49711..e958836f99 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -3,7 +3,9 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - repo::{Repo, RepoActionState, RepoListItem, RepoState}, + repo::{ + Repo, RepoActionState, RepoListItem, RepoSortBy, RepoState, + }, }, }; use mogh_resolver::Resolve; @@ -45,10 +47,27 @@ impl Resolve for ListRepos { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + RepoSortBy::Name => resource::ListItemSort::Name, + RepoSortBy::Repo => { + resource::ListItemSort::DbField("config.repo") + } + RepoSortBy::Branch => { + resource::ListItemSort::DbField("config.branch") + } + RepoSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; let repos = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index cf54a64b88..f98480674d 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -16,7 +16,8 @@ use komodo_client::{ entities::{ permission::PermissionLevel, server::{ - Server, ServerActionState, ServerListItem, ServerState, + Server, ServerActionState, ServerListItem, ServerSortBy, + ServerState, }, stats::{SystemInformation, SystemProcess}, }, @@ -111,10 +112,29 @@ impl Resolve for ListServers { }; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + ServerSortBy::Name => resource::ListItemSort::Name, + ServerSortBy::Region => { + resource::ListItemSort::DbField("config.region") + } + ServerSortBy::Version => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.version.cmp(&b.info.version) + })) + } + ServerSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; let servers = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 59746fe546..ddba7f4503 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -11,7 +11,7 @@ use komodo_client::{ permission::PermissionLevel, stack::{ Stack, StackActionState, StackListItem, StackQuery, - StackService, StackState, + StackService, StackSortBy, StackState, }, }, }; @@ -486,10 +486,41 @@ impl Resolve for ListStacks { let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + StackSortBy::Name => resource::ListItemSort::Name, + StackSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.repo.cmp(&b.info.repo) + })) + } + StackSortBy::Host => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + let host_a = if a.info.swarm_id.is_empty() { + &a.info.server_name + } else { + &a.info.swarm_name + }; + let host_b = if b.info.swarm_id.is_empty() { + &b.info.server_name + } else { + &b.info.swarm_name + }; + host_a.cmp(host_b) + })) + } + StackSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; let stacks = resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, @@ -500,8 +531,7 @@ impl Resolve for ListStacks { .services .iter() .any(|service| service.update_available)) - && (states.is_empty() - || states.contains(&stack.info.state)) + && (states.is_empty() || states.contains(&stack.info.state)) }, ) .await?; diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 93ee798e87..b4229fa268 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -3,7 +3,9 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState}, + swarm::{ + Swarm, SwarmActionState, SwarmListItem, SwarmSortBy, SwarmState, + }, }, }; use mogh_resolver::Resolve; @@ -44,11 +46,22 @@ impl Resolve for ListSwarms { get_all_tags(None).await? }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + SwarmSortBy::Name => resource::ListItemSort::Name, + SwarmSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; Ok( resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index b4f58cbbf9..b258108dbf 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -5,6 +5,7 @@ use komodo_client::{ permission::PermissionLevel, sync::{ ResourceSync, ResourceSyncActionState, ResourceSyncListItem, + ResourceSyncSortBy, }, }, }; @@ -44,11 +45,32 @@ impl Resolve for ListResourceSyncs { get_all_tags(None).await? }; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + ResourceSyncSortBy::Name => resource::ListItemSort::Name, + ResourceSyncSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.repo.cmp(&b.info.repo) + })) + } + ResourceSyncSortBy::Branch => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.branch.cmp(&b.info.branch) + })) + } + ResourceSyncSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.to_string().cmp(&b.info.state.to_string()) + })) + } + }; Ok( resource::list_items_for_user::( self.query, limit, self.page, + self.sort_desc, + sort_by, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs index 0fa9502c34..52373f677f 100644 --- a/bin/core/src/api/read/update.rs +++ b/bin/core/src/api/read/update.rs @@ -47,7 +47,9 @@ impl Resolve for ListUpdates { query, FindOptions::builder() .sort(doc! { "start_ts": -1 }) - .skip((self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64)) + .skip( + (self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64), + ) .limit(UPDATES_PER_PAGE) .build(), ) diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 1c5e233404..7419946afb 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -292,6 +292,8 @@ pub async fn list_items_for_user( mut query: ResourceQuery, limit: u64, page: u64, + sort_desc: bool, + sort_by: ListItemSort, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], @@ -305,41 +307,96 @@ pub async fn list_items_for_user( crate::permission::load_list_permits::(user, permission) .await?; - let mut cursor = T::coll() - .find(filters) - .sort(doc! { "name": 1 }) - .await - .with_context(|| { + let direction = if sort_desc { -1 } else { 1 }; + let sort = match &sort_by { + ListItemSort::Name => doc! { "name": direction }, + // Db field sorts use ascending name as the secondary sort. + ListItemSort::DbField(field) => { + doc! { *field: direction, "name": 1 } + } + // In-memory sorts pull items in ascending name db order, + // keeping name as the stable tiebreak for equal sort keys. + ListItemSort::InMemory(_) => doc! { "name": 1 }, + }; + + let mut cursor = + T::coll().find(filters).sort(sort).await.with_context(|| { format!("Failed to query db for {}s", T::resource_type()) })?; - let skip = page.saturating_mul(limit); - let mut skipped = 0; + let skip = page.saturating_mul(limit) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; let mut items = Vec::new(); - while let Some(resource) = cursor - .try_next() - .await - .context("Failed to pull next resource from db cursor")? - { - if !permits.permitted::(&resource).await? { - continue; - } - let item = T::to_list_item(resource).await; - if !filter(&item) { - continue; + if let ListItemSort::InMemory(compare) = sort_by { + // The sort can only be applied after all matching items + // are collected, so pagination also happens after the sort. + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let item = T::to_list_item(resource).await; + if !filter(&item) { + continue; + } + items.push(item); } - if skipped < skip { - skipped += 1; - continue; + // Stable sort keeps equal sort keys in ascending + // name order for both directions. + if sort_desc { + items.sort_by(|a, b| compare(b, a)); + } else { + items.sort_by(|a, b| compare(a, b)); } - items.push(item); - if limit != 0 && items.len() as u64 >= limit { - break; + Ok(items.into_iter().skip(skip).take(take).collect()) + } else { + let mut skipped = 0; + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let item = T::to_list_item(resource).await; + if !filter(&item) { + continue; + } + if skipped < skip { + skipped += 1; + continue; + } + items.push(item); + if take != usize::MAX && items.len() >= take { + break; + } } + Ok(items) } +} - Ok(items) +/// How the `List` apis sort the list items. +pub enum ListItemSort { + /// Sort by name at the db level. Default. + Name, + /// Sort on a db stored field, keeping db level sort + /// and streaming pagination. Only usable when the db field + /// exactly matches the displayed list item field. + DbField(&'static str), + /// Compare list items in memory, required for fields computed + /// from the in memory caches (eg. state), or which diverge + /// from the db field (eg. linked repo sources). + /// Collects all matching items before applying pagination. + InMemory(Box std::cmp::Ordering + Send>), } pub async fn list_for_user( @@ -465,9 +522,7 @@ pub async fn list_full_for_user_filtered( filter: impl Fn(Resource) -> F, ) -> anyhow::Result>> where - F: Future< - Output = Option>, - > + Send, + F: Future>> + Send, { validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); diff --git a/client/core/rs/src/api/read/action.rs b/client/core/rs/src/api/read/action.rs index 805fe981f9..8d59db871f 100644 --- a/client/core/rs/src/api/read/action.rs +++ b/client/core/rs/src/api/read/action.rs @@ -4,7 +4,10 @@ use typeshare::typeshare; use crate::entities::{ U64, - action::{Action, ActionActionState, ActionListItem, ActionQuery}, + action::{ + Action, ActionActionState, ActionListItem, ActionQuery, + ActionSortBy, + }, }; use super::KomodoReadRequest; @@ -78,6 +81,16 @@ pub struct ListActions { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ActionSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs index 6af14acdc7..4b27583f2e 100644 --- a/client/core/rs/src/api/read/alerter.rs +++ b/client/core/rs/src/api/read/alerter.rs @@ -4,7 +4,7 @@ use typeshare::typeshare; use crate::entities::{ U64, - alerter::{Alerter, AlerterListItem, AlerterQuery}, + alerter::{Alerter, AlerterListItem, AlerterQuery, AlerterSortBy}, }; use super::KomodoReadRequest; @@ -78,6 +78,16 @@ pub struct ListAlerters { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: AlerterSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs index d59d324b40..2a2992420b 100644 --- a/client/core/rs/src/api/read/build.rs +++ b/client/core/rs/src/api/read/build.rs @@ -6,7 +6,9 @@ use typeshare::typeshare; use crate::entities::{ I64, U64, Version, - build::{Build, BuildActionState, BuildListItem, BuildQuery}, + build::{ + Build, BuildActionState, BuildListItem, BuildQuery, BuildSortBy, + }, }; use super::KomodoReadRequest; @@ -80,6 +82,16 @@ pub struct ListBuilds { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: BuildSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs index c4698e2555..db98323fb9 100644 --- a/client/core/rs/src/api/read/builder.rs +++ b/client/core/rs/src/api/read/builder.rs @@ -4,7 +4,7 @@ use typeshare::typeshare; use crate::entities::{ U64, - builder::{Builder, BuilderListItem, BuilderQuery}, + builder::{Builder, BuilderListItem, BuilderQuery, BuilderSortBy}, }; use super::KomodoReadRequest; @@ -77,6 +77,16 @@ pub struct ListBuilders { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: BuilderSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index 43689a8551..fe3e773ca4 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -6,7 +6,7 @@ use crate::entities::{ I64, SearchCombinator, U64, deployment::{ Deployment, DeploymentActionState, DeploymentListItem, - DeploymentQuery, DeploymentState, + DeploymentQuery, DeploymentSortBy, DeploymentState, }, docker::{ container::{Container, ContainerListItem, ContainerStats}, @@ -87,6 +87,16 @@ pub struct ListDeployments { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: DeploymentSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 3111cdf69f..b97663987a 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -112,7 +112,7 @@ pub struct ListAllContainers { /// `limit: 100` is default. /// /// Passing `limit: 0` returns all results (unlimited). - /// + /// /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs index fd41b5a8d3..ab6272cb1b 100644 --- a/client/core/rs/src/api/read/procedure.rs +++ b/client/core/rs/src/api/read/procedure.rs @@ -6,7 +6,7 @@ use crate::entities::{ U64, procedure::{ Procedure, ProcedureActionState, ProcedureListItem, - ProcedureQuery, + ProcedureQuery, ProcedureSortBy, }, }; @@ -81,6 +81,16 @@ pub struct ListProcedures { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ProcedureSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs index 35070813ee..8ffe9c6a52 100644 --- a/client/core/rs/src/api/read/repo.rs +++ b/client/core/rs/src/api/read/repo.rs @@ -4,7 +4,9 @@ use typeshare::typeshare; use crate::entities::{ U64, - repo::{Repo, RepoActionState, RepoListItem, RepoQuery}, + repo::{ + Repo, RepoActionState, RepoListItem, RepoQuery, RepoSortBy, + }, }; use super::KomodoReadRequest; @@ -78,6 +80,16 @@ pub struct ListRepos { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: RepoSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index 9dec2fb7b3..c9ca29b9c1 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -6,7 +6,7 @@ use crate::entities::{ I64, Timelength, U64, server::{ PeripheryInformation, Server, ServerActionState, ServerListItem, - ServerQuery, ServerState, + ServerQuery, ServerSortBy, ServerState, }, stats::{ SystemInformation, SystemProcess, SystemStats, SystemStatsRecord, @@ -84,6 +84,16 @@ pub struct ListServers { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ServerSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 205aced31c..a1062a48b0 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -9,7 +9,7 @@ use crate::entities::{ }, stack::{ Stack, StackActionState, StackListItem, StackQuery, StackService, - StackServiceState, + StackServiceState, StackSortBy, }, update::Log, }; @@ -429,6 +429,16 @@ pub struct ListStacks { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: StackSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index 1475c2bfaf..cdba961a91 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -14,7 +14,9 @@ use crate::entities::{ swarm::SwarmInspectInfo, task::{SwarmTask, SwarmTaskListItem}, }, - swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmQuery}, + swarm::{ + Swarm, SwarmActionState, SwarmListItem, SwarmQuery, SwarmSortBy, + }, update::Log, }; @@ -89,6 +91,16 @@ pub struct ListSwarms { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: SwarmSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/sync.rs b/client/core/rs/src/api/read/sync.rs index 3a971559c2..b8521c6a3f 100644 --- a/client/core/rs/src/api/read/sync.rs +++ b/client/core/rs/src/api/read/sync.rs @@ -6,7 +6,7 @@ use crate::entities::{ U64, sync::{ ResourceSync, ResourceSyncActionState, ResourceSyncListItem, - ResourceSyncQuery, + ResourceSyncQuery, ResourceSyncSortBy, }, }; @@ -81,6 +81,16 @@ pub struct ListResourceSyncs { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ResourceSyncSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/write/provider.rs b/client/core/rs/src/api/write/provider.rs index 96a9fab1f2..54980eb85f 100644 --- a/client/core/rs/src/api/write/provider.rs +++ b/client/core/rs/src/api/write/provider.rs @@ -180,7 +180,7 @@ pub fn delete_image_registry_account() {} /// **Admin only.** Delete an image registry account. /// Response: [ImageRegistryAccount]. -/// +/// /// Pre v2.3.0, called `DeleteDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] diff --git a/client/core/rs/src/entities/action.rs b/client/core/rs/src/entities/action.rs index cc983cdc45..548af0a9bc 100644 --- a/client/core/rs/src/entities/action.rs +++ b/client/core/rs/src/entities/action.rs @@ -256,6 +256,21 @@ pub struct ActionActionState { #[typeshare] pub type ActionQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ActionSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, + /// Sort by next scheduled run. + NextRun, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/alerter.rs b/client/core/rs/src/entities/alerter.rs index 7a4d9c2d6f..2bb578a467 100644 --- a/client/core/rs/src/entities/alerter.rs +++ b/client/core/rs/src/entities/alerter.rs @@ -323,6 +323,21 @@ fn default_pushover_url() -> String { #[typeshare] pub type AlerterQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum AlerterSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by endpoint type. + Type, + /// Sort by enabled. + Enabled, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index cde42303ac..ff2e6bb2c0 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -708,6 +708,21 @@ pub struct BuildActionState { #[typeshare] pub type BuildQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum BuildSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by state. + State, +} + #[typeshare] #[derive( Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index f048b98e00..2897abeeba 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -676,6 +676,21 @@ impl utoipa::ToSchema for PartialAwsBuilderConfig {} #[typeshare] pub type BuilderQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum BuilderSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by builder provider type. + Provider, + /// Sort by instance type. + InstanceType, +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index 11ac1c922a..4d385eb52a 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -594,6 +594,23 @@ pub struct DeploymentActionState { #[typeshare] pub type DeploymentQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum DeploymentSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by image. + Image, + /// Sort by host Server / Swarm name. + Host, + /// Sort by state. + State, +} + #[typeshare] #[derive( Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, diff --git a/client/core/rs/src/entities/procedure.rs b/client/core/rs/src/entities/procedure.rs index e787b17a98..ce85bdbf06 100644 --- a/client/core/rs/src/entities/procedure.rs +++ b/client/core/rs/src/entities/procedure.rs @@ -256,6 +256,21 @@ pub struct ProcedureActionState { #[typeshare] pub type ProcedureQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ProcedureSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, + /// Sort by next scheduled run. + NextRun, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs index 6d4cefceab..1a01bdcd1c 100644 --- a/client/core/rs/src/entities/repo.rs +++ b/client/core/rs/src/entities/repo.rs @@ -342,6 +342,23 @@ pub struct RepoActionState { #[typeshare] pub type RepoQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum RepoSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by the git repo. + Repo, + /// Sort by branch. + Branch, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index 4e4a3fe116..641b187ec6 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -459,6 +459,23 @@ pub enum ServerState { #[typeshare] pub type ServerQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ServerSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by region. + Region, + /// Sort by periphery version. + Version, + /// Sort by state. + State, +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 7a3b3aa550..6248442110 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -933,6 +933,23 @@ pub struct StackActionState { #[typeshare] pub type StackQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum StackSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by host Server / Swarm name. + Host, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/swarm.rs b/client/core/rs/src/entities/swarm.rs index 966d64cc03..1a55e933bd 100644 --- a/client/core/rs/src/entities/swarm.rs +++ b/client/core/rs/src/entities/swarm.rs @@ -145,6 +145,19 @@ pub struct SwarmActionState {} #[typeshare] pub type SwarmQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum SwarmSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/sync.rs b/client/core/rs/src/entities/sync.rs index 0c720e0477..f6cd6ba223 100644 --- a/client/core/rs/src/entities/sync.rs +++ b/client/core/rs/src/entities/sync.rs @@ -430,6 +430,23 @@ pub struct ResourceSyncActionState { pub type ResourceSyncQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ResourceSyncSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by branch. + Branch, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, From 47287fadb6558c61affc340a8044f2a5058ca475 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 22:42:57 -0700 Subject: [PATCH 121/203] ui implement server side table sort --- client/core/ts/src/types.ts | 195 +++++++++++++++++++++ ui/package.json | 2 +- ui/public/client/types.d.ts | 184 +++++++++++++++++++ ui/public/client/types.js | 107 +++++++++++ ui/src/pages/dashboard/tables.tsx | 19 +- ui/src/pages/resources.tsx | 47 ++++- ui/src/resources/action/table.tsx | 22 +++ ui/src/resources/alerter/table.tsx | 26 ++- ui/src/resources/build/table.tsx | 22 +++ ui/src/resources/builder/table.tsx | 22 +++ ui/src/resources/deployment/table.tsx | 23 +++ ui/src/resources/index.ts | 6 + ui/src/resources/procedure/table.tsx | 22 +++ ui/src/resources/repo/table.tsx | 27 ++- ui/src/resources/server/table/standard.tsx | 26 +++ ui/src/resources/stack/table.tsx | 23 +++ ui/src/resources/swarm/table.tsx | 21 +++ ui/src/resources/sync/table.tsx | 23 +++ ui/yarn.lock | 8 +- 19 files changed, 806 insertions(+), 19 deletions(-) diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 427ac43957..a32c1e5083 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -8720,6 +8720,15 @@ export interface LatestCommit { message: string; } +export enum ActionSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun", +} + /** List actions matching optional query. Response: [ListActionsResponse]. */ export interface ListActions { /** optional structured query to filter actions. */ @@ -8739,6 +8748,23 @@ export interface ListActions { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ActionSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum AlerterSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by endpoint type. */ + Type = "Type", + /** Sort by enabled. */ + Enabled = "Enabled", } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ @@ -8760,6 +8786,14 @@ export interface ListAlerters { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: AlerterSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -8915,6 +8949,15 @@ export interface ListBuildVersions { limit?: I64; } +export enum BuilderSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by builder provider type. */ + Provider = "Provider", + /** Sort by instance type. */ + InstanceType = "InstanceType", +} + /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; @@ -8933,6 +8976,23 @@ export interface ListBuilders { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuilderSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum BuildSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by state. */ + State = "State", } /** List builds matching optional query. Response: [ListBuildsResponse]. */ @@ -8954,6 +9014,14 @@ export interface ListBuilds { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuildSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -9012,6 +9080,17 @@ export interface ListContainers { server: string; } +export enum DeploymentSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by image. */ + Image = "Image", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State", +} + /** * List deployments matching optional query. * Response: [ListDeploymentsResponse]. @@ -9034,6 +9113,14 @@ export interface ListDeployments { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: DeploymentSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ @@ -9377,6 +9464,15 @@ export interface ListOnboardingKeys { export interface ListPermissions { } +export enum ProcedureSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun", +} + /** List procedures matching optional query. Response: [ListProceduresResponse]. */ export interface ListProcedures { /** optional structured query to filter procedures. */ @@ -9396,6 +9492,25 @@ export interface ListProcedures { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ProcedureSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum RepoSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by the git repo. */ + Repo = "Repo", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State", } /** List repos matching optional query. Response: [ListReposResponse]. */ @@ -9417,6 +9532,25 @@ export interface ListRepos { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: RepoSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum ResourceSyncSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State", } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ @@ -9438,6 +9572,14 @@ export interface ListResourceSyncs { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ResourceSyncSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -9463,6 +9605,17 @@ export interface ListSecrets { target?: ResourceTarget; } +export enum ServerSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by region. */ + Region = "Region", + /** Sort by periphery version. */ + Version = "Version", + /** Sort by state. */ + State = "State", +} + /** List servers matching optional query. Response: [ListServersResponse]. */ export interface ListServers { /** optional structured query to filter servers. */ @@ -9482,6 +9635,14 @@ export interface ListServers { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ServerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ @@ -9490,6 +9651,17 @@ export interface ListStackServices { stack: string; } +export enum StackSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State", +} + /** List stacks matching optional query. Response: [ListStacksResponse]. */ export interface ListStacks { /** optional structured query to filter stacks. */ @@ -9509,6 +9681,14 @@ export interface ListStacks { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: StackSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -9576,6 +9756,13 @@ export interface ListSwarmTasks { swarm: string; } +export enum SwarmSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", +} + /** List Swarms matching optional query. Response: [ListSwarmsResponse]. */ export interface ListSwarms { /** Optional structured query to filter Swarms. */ @@ -9595,6 +9782,14 @@ export interface ListSwarms { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: SwarmSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** diff --git a/ui/package.json b/ui/package.json index ed8c52d1ec..e72652fb55 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,7 @@ "jotai-location": "^0.6.2", "lucide-react": "^1.22.0", "mogh_auth_client": "^1.7.0", - "mogh_ui": "^0.7.0", + "mogh_ui": "^0.7.1", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.5.1", "react": "^19.2.7", diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 396ea70479..d9d1b3e1ba 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -8314,6 +8314,14 @@ export interface LatestCommit { hash: string; message: string; } +export declare enum ActionSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun" +} /** List actions matching optional query. Response: [ListActionsResponse]. */ export interface ListActions { /** optional structured query to filter actions. */ @@ -8333,6 +8341,22 @@ export interface ListActions { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ActionSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum AlerterSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by endpoint type. */ + Type = "Type", + /** Sort by enabled. */ + Enabled = "Enabled" } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { @@ -8353,6 +8377,14 @@ export interface ListAlerters { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: AlerterSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Get a paginated list of alerts sorted by timestamp descending. @@ -8500,6 +8532,14 @@ export interface ListBuildVersions { /** Limit the number of included results. Default is no limit. */ limit?: I64; } +export declare enum BuilderSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by builder provider type. */ + Provider = "Provider", + /** Sort by instance type. */ + InstanceType = "InstanceType" +} /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; @@ -8518,6 +8558,22 @@ export interface ListBuilders { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuilderSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum BuildSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by state. */ + State = "State" } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { @@ -8538,6 +8594,14 @@ export interface ListBuilds { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuildSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Gets a list of existing values used as extra args across other builds. @@ -8589,6 +8653,16 @@ export interface ListContainers { /** Id or name */ server: string; } +export declare enum DeploymentSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by image. */ + Image = "Image", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State" +} /** * List deployments matching optional query. * Response: [ListDeploymentsResponse]. @@ -8611,6 +8685,14 @@ export interface ListDeployments { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: DeploymentSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { @@ -8933,6 +9015,14 @@ export interface ListOnboardingKeys { */ export interface ListPermissions { } +export declare enum ProcedureSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun" +} /** List procedures matching optional query. Response: [ListProceduresResponse]. */ export interface ListProcedures { /** optional structured query to filter procedures. */ @@ -8952,6 +9042,24 @@ export interface ListProcedures { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ProcedureSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum RepoSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by the git repo. */ + Repo = "Repo", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State" } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { @@ -8972,6 +9080,24 @@ export interface ListRepos { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: RepoSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum ResourceSyncSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State" } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { @@ -8992,6 +9118,14 @@ export interface ListResourceSyncs { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ResourceSyncSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List configured schedules. @@ -9014,6 +9148,16 @@ export interface ListSecrets { */ target?: ResourceTarget; } +export declare enum ServerSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by region. */ + Region = "Region", + /** Sort by periphery version. */ + Version = "Version", + /** Sort by state. */ + State = "State" +} /** List servers matching optional query. Response: [ListServersResponse]. */ export interface ListServers { /** optional structured query to filter servers. */ @@ -9033,12 +9177,30 @@ export interface ListServers { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ServerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ export interface ListStackServices { /** Id or name */ stack: string; } +export declare enum StackSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State" +} /** List stacks matching optional query. Response: [ListStacksResponse]. */ export interface ListStacks { /** optional structured query to filter stacks. */ @@ -9058,6 +9220,14 @@ export interface ListStacks { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: StackSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List configs on the target Swarm. @@ -9117,6 +9287,12 @@ export interface ListSwarmTasks { /** Id or name */ swarm: string; } +export declare enum SwarmSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State" +} /** List Swarms matching optional query. Response: [ListSwarmsResponse]. */ export interface ListSwarms { /** Optional structured query to filter Swarms. */ @@ -9136,6 +9312,14 @@ export interface ListSwarms { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: SwarmSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List the processes running on the target server. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index c663d15412..21e06f1780 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -819,6 +819,113 @@ export var ContainerTerminalMode; ContainerTerminalMode["Exec"] = "exec"; ContainerTerminalMode["Attach"] = "attach"; })(ContainerTerminalMode || (ContainerTerminalMode = {})); +export var ActionSortBy; +(function (ActionSortBy) { + /** Sort by name. Default. */ + ActionSortBy["Name"] = "Name"; + /** Sort by state. */ + ActionSortBy["State"] = "State"; + /** Sort by next scheduled run. */ + ActionSortBy["NextRun"] = "NextRun"; +})(ActionSortBy || (ActionSortBy = {})); +export var AlerterSortBy; +(function (AlerterSortBy) { + /** Sort by name. Default. */ + AlerterSortBy["Name"] = "Name"; + /** Sort by endpoint type. */ + AlerterSortBy["Type"] = "Type"; + /** Sort by enabled. */ + AlerterSortBy["Enabled"] = "Enabled"; +})(AlerterSortBy || (AlerterSortBy = {})); +export var BuilderSortBy; +(function (BuilderSortBy) { + /** Sort by name. Default. */ + BuilderSortBy["Name"] = "Name"; + /** Sort by builder provider type. */ + BuilderSortBy["Provider"] = "Provider"; + /** Sort by instance type. */ + BuilderSortBy["InstanceType"] = "InstanceType"; +})(BuilderSortBy || (BuilderSortBy = {})); +export var BuildSortBy; +(function (BuildSortBy) { + /** Sort by name. Default. */ + BuildSortBy["Name"] = "Name"; + /** Sort by source repo. */ + BuildSortBy["Source"] = "Source"; + /** Sort by state. */ + BuildSortBy["State"] = "State"; +})(BuildSortBy || (BuildSortBy = {})); +export var DeploymentSortBy; +(function (DeploymentSortBy) { + /** Sort by name. Default. */ + DeploymentSortBy["Name"] = "Name"; + /** Sort by image. */ + DeploymentSortBy["Image"] = "Image"; + /** Sort by host Server / Swarm name. */ + DeploymentSortBy["Host"] = "Host"; + /** Sort by state. */ + DeploymentSortBy["State"] = "State"; +})(DeploymentSortBy || (DeploymentSortBy = {})); +export var ProcedureSortBy; +(function (ProcedureSortBy) { + /** Sort by name. Default. */ + ProcedureSortBy["Name"] = "Name"; + /** Sort by state. */ + ProcedureSortBy["State"] = "State"; + /** Sort by next scheduled run. */ + ProcedureSortBy["NextRun"] = "NextRun"; +})(ProcedureSortBy || (ProcedureSortBy = {})); +export var RepoSortBy; +(function (RepoSortBy) { + /** Sort by name. Default. */ + RepoSortBy["Name"] = "Name"; + /** Sort by the git repo. */ + RepoSortBy["Repo"] = "Repo"; + /** Sort by branch. */ + RepoSortBy["Branch"] = "Branch"; + /** Sort by state. */ + RepoSortBy["State"] = "State"; +})(RepoSortBy || (RepoSortBy = {})); +export var ResourceSyncSortBy; +(function (ResourceSyncSortBy) { + /** Sort by name. Default. */ + ResourceSyncSortBy["Name"] = "Name"; + /** Sort by source repo. */ + ResourceSyncSortBy["Source"] = "Source"; + /** Sort by branch. */ + ResourceSyncSortBy["Branch"] = "Branch"; + /** Sort by state. */ + ResourceSyncSortBy["State"] = "State"; +})(ResourceSyncSortBy || (ResourceSyncSortBy = {})); +export var ServerSortBy; +(function (ServerSortBy) { + /** Sort by name. Default. */ + ServerSortBy["Name"] = "Name"; + /** Sort by region. */ + ServerSortBy["Region"] = "Region"; + /** Sort by periphery version. */ + ServerSortBy["Version"] = "Version"; + /** Sort by state. */ + ServerSortBy["State"] = "State"; +})(ServerSortBy || (ServerSortBy = {})); +export var StackSortBy; +(function (StackSortBy) { + /** Sort by name. Default. */ + StackSortBy["Name"] = "Name"; + /** Sort by source repo. */ + StackSortBy["Source"] = "Source"; + /** Sort by host Server / Swarm name. */ + StackSortBy["Host"] = "Host"; + /** Sort by state. */ + StackSortBy["State"] = "State"; +})(StackSortBy || (StackSortBy = {})); +export var SwarmSortBy; +(function (SwarmSortBy) { + /** Sort by name. Default. */ + SwarmSortBy["Name"] = "Name"; + /** Sort by state. */ + SwarmSortBy["State"] = "State"; +})(SwarmSortBy || (SwarmSortBy = {})); export var ServiceUserQueryBehavior; (function (ServiceUserQueryBehavior) { /** Include service users in results. Default. */ diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 5359ee9267..9ca4c4b9b8 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -67,20 +67,29 @@ function TableSection({ const [templates] = useTemplatesQueryBehavior(); const [page, setPage] = useState(0); - // Set to page 0 whenever any filter changes, + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: string; + sort_desc?: boolean; + }>({}); + // Set to page 0 whenever any filter or the sort changes, // otherwise the query can point past the last page and come back empty. useEffect(() => { setPage(0); - }, [terms, tags, templates]); + }, [terms, tags, templates, sort.sort_by, sort.sort_desc]); const _resources = - useRead(`List${type}s`, { query: { terms, tags, templates }, page }).data ?? - []; + useRead(`List${type}s`, { + query: { terms, tags, templates }, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }).data ?? []; // Prevent flashing when typing / fetching const resources = useDebounce(_resources, 200); const Table = useMemo( - () => show && , + () => show && , [resources, show], ); diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 4ed49af945..9638ce7738 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useFilterByUpdateAvailable, useRead, @@ -52,11 +52,34 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const tags = useTagsFilter(); const [templates] = useTemplatesQueryBehavior(); - // Set to page 0 whenever the resource type or any filter changes, - // otherwise the query can point past the last page and come back empty. + // Server side sort, passed up from the table. + // The sort keys are resource type specific, so ignore + // any sort captured on a previously selected type. + const [_sort, _setSort] = useState<{ + type: UsableResource; + sort_by?: string; + sort_desc?: boolean; + }>({ type }); + const sort = _sort.type === type ? _sort : { type }; + const setSort = useCallback( + (sort: { sort_by?: string; sort_desc?: boolean }) => + _setSort({ type, ...sort }), + [type], + ); + + // Set to page 0 whenever the resource type, any filter, + // or the sort changes, otherwise the query can point past + // the last page and come back empty. useEffect(() => { setPage(0); - }, [type, tags, templates, filterUpdateAvailable]); + }, [ + type, + tags, + templates, + filterUpdateAvailable, + sort.sort_by, + sort.sort_desc, + ]); const query: Types.ResourceQuery = { terms, @@ -67,7 +90,13 @@ export default function Resources({ _type }: { _type?: UsableResource }) { ? { update_available: filterUpdateAvailable } : undefined, }; - const _resources = useRead(`List${type}s`, { query, page }).data ?? []; + const _resources = + useRead(`List${type}s`, { + query, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }).data ?? []; // Debounce: prevents flashing when typing / fetching. // Must also set immediately upon change of resource type @@ -84,8 +113,12 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const Table = useMemo( () => - resources && RC ? : , - [resources], + resources && RC ? ( + + ) : ( + + ), + [resources, setSort], ); if (!RC) { diff --git a/ui/src/resources/action/table.tsx b/ui/src/resources/action/table.tsx index 91fa2d9193..291d5670ef 100644 --- a/ui/src/resources/action/table.tsx +++ b/ui/src/resources/action/table.tsx @@ -6,16 +6,35 @@ import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import ResourceLink from "@/resources/link"; +const SORT_KEYS = ["Name", "State", "NextRun"]; + export default function ActionTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ActionListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Action"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="actions-table" data={resources} selectOptions={{ @@ -24,6 +43,7 @@ export default function ActionTable({ }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -33,6 +53,7 @@ export default function ActionTable({ ), }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( @@ -40,6 +61,7 @@ export default function ActionTable({ cell: ({ row }) => , }, { + id: "NextRun", accessorKey: "info.next_scheduled_run", header: ({ column }) => ( diff --git a/ui/src/resources/alerter/table.tsx b/ui/src/resources/alerter/table.tsx index 392ed43d01..248427eb7e 100644 --- a/ui/src/resources/alerter/table.tsx +++ b/ui/src/resources/alerter/table.tsx @@ -6,14 +6,35 @@ import ResourceLink from "@/resources/link"; import TableTags from "@/components/tags/table"; import { StatusBadge } from "mogh_ui"; +const SORT_KEYS = ["Name", "Type", "Enabled"]; + export default function AlerterTable({ resources, + onServerSort, ...boxProps -}: { resources: Types.AlerterListItem[] } & BoxProps) { +}: { + resources: Types.AlerterListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; +} & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Alerter"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="alerters" data={resources} selectOptions={{ @@ -22,6 +43,7 @@ export default function AlerterTable({ }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -31,6 +53,7 @@ export default function AlerterTable({ ), }, { + id: "Type", accessorKey: "info.endpoint_type", header: ({ column }) => ( @@ -40,6 +63,7 @@ export default function AlerterTable({ ), }, { + id: "Enabled", accessorKey: "info.enabled", header: ({ column }) => ( diff --git a/ui/src/resources/build/table.tsx b/ui/src/resources/build/table.tsx index 106bbb8213..233fe5f859 100644 --- a/ui/src/resources/build/table.tsx +++ b/ui/src/resources/build/table.tsx @@ -8,17 +8,36 @@ import { BoxProps } from "@mantine/core"; import ResourceLink from "@/resources/link"; import FileSource from "@/components/file-source"; +const SORT_KEYS = ["Name", "Source", "State"]; + export default function BuildTable({ resources, + onServerSort, ...boxProps }: { resources: Types.BuildListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Build"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="builds" data={resources} selectOptions={{ @@ -30,6 +49,7 @@ export default function BuildTable({ header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -38,6 +58,7 @@ export default function BuildTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -48,6 +69,7 @@ export default function BuildTable({ size: 120, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/builder/table.tsx b/ui/src/resources/builder/table.tsx index 641cd4cf81..0268f3d73e 100644 --- a/ui/src/resources/builder/table.tsx +++ b/ui/src/resources/builder/table.tsx @@ -6,16 +6,35 @@ import ResourceLink from "@/resources/link"; import TableTags from "@/components/tags/table"; import BuilderInstanceType from "./instance-type"; +const SORT_KEYS = ["Name", "Provider", "InstanceType"]; + export default function BuilderTable({ resources, + onServerSort, ...boxProps }: { resources: Types.BuilderListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Builder"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="builders" data={resources} selectOptions={{ @@ -24,6 +43,7 @@ export default function BuilderTable({ }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -33,12 +53,14 @@ export default function BuilderTable({ ), }, { + id: "Provider", accessorKey: "info.builder_type", header: ({ column }) => ( ), }, { + id: "InstanceType", accessorKey: "info.instance_type", header: ({ column }) => ( diff --git a/ui/src/resources/deployment/table.tsx b/ui/src/resources/deployment/table.tsx index 8f6533fd3e..0cbc0b9765 100644 --- a/ui/src/resources/deployment/table.tsx +++ b/ui/src/resources/deployment/table.tsx @@ -8,17 +8,36 @@ import { DeploymentComponents } from "."; import ResourceLink from "@/resources/link"; import DeploymentUpdateAvailable from "./update-available"; +const SORT_KEYS = ["Name", "Image", "Host", "State"]; + export default function DeploymentTable({ resources, + onServerSort, ...boxProps }: { resources: Types.DeploymentListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Deployment"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="deployments" data={resources} selectOptions={{ @@ -27,6 +46,7 @@ export default function DeploymentTable({ }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -40,6 +60,7 @@ export default function DeploymentTable({ size: 200, }, { + id: "Image", accessorKey: "info.image", header: ({ column }) => ( @@ -57,6 +78,7 @@ export default function DeploymentTable({ header: ({ column }) => ( ), + id: "Host", accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id @@ -83,6 +105,7 @@ export default function DeploymentTable({ size: 200, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/index.ts b/ui/src/resources/index.ts index 66e2b02635..8e1173f05c 100644 --- a/ui/src/resources/index.ts +++ b/ui/src/resources/index.ts @@ -99,6 +99,12 @@ export interface RequiredResourceComponents< Table: React.FC< { resources: Types.ResourceListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; tableProps?: TableProps; } & BoxProps >; diff --git a/ui/src/resources/procedure/table.tsx b/ui/src/resources/procedure/table.tsx index 4f13ac92eb..a9798d818b 100644 --- a/ui/src/resources/procedure/table.tsx +++ b/ui/src/resources/procedure/table.tsx @@ -6,17 +6,36 @@ import { ProcedureComponents } from "."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "State", "NextRun"]; + export default function ProcedureTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ProcedureListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Procedure"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="procedures" data={resources} selectOptions={{ @@ -25,6 +44,7 @@ export default function ProcedureTable({ }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -34,6 +54,7 @@ export default function ProcedureTable({ ), }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( @@ -41,6 +62,7 @@ export default function ProcedureTable({ cell: ({ row }) => , }, { + id: "NextRun", accessorKey: "info.next_scheduled_run", header: ({ column }) => ( diff --git a/ui/src/resources/repo/table.tsx b/ui/src/resources/repo/table.tsx index ce2d5ee60a..a024fb631f 100644 --- a/ui/src/resources/repo/table.tsx +++ b/ui/src/resources/repo/table.tsx @@ -7,15 +7,36 @@ import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import RepoLink from "@/components/repo-link"; +const SORT_KEYS = ["Name", "Repo", "Branch", "State"]; + export default function RepoTable({ resources, + onServerSort, ...boxProps -}: { resources: Types.RepoListItem[] } & BoxProps) { +}: { + resources: Types.RepoListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; +} & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Repo"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="repo-table" data={resources} selectOptions={{ @@ -27,6 +48,7 @@ export default function RepoTable({ header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -35,6 +57,7 @@ export default function RepoTable({ header: ({ column }) => ( ), + id: "Repo", accessorKey: "info.repo", cell: ({ row }) => ( ( ), + id: "Branch", accessorKey: "info.branch", size: 200, }, @@ -55,6 +79,7 @@ export default function RepoTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => , size: 120, diff --git a/ui/src/resources/server/table/standard.tsx b/ui/src/resources/server/table/standard.tsx index 92a98422c5..a6df9fbe27 100644 --- a/ui/src/resources/server/table/standard.tsx +++ b/ui/src/resources/server/table/standard.tsx @@ -7,11 +7,20 @@ import { ServerComponents } from ".."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "Region", "Version", "State"]; + export default function StandardServerTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ServerListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Server"); const deployments = useRead("ListDeployments", { limit: 0 }).data; @@ -31,6 +40,16 @@ export default function StandardServerTable({ return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="standard-server-table" data={resources} selectOptions={{ @@ -40,6 +59,7 @@ export default function StandardServerTable({ columns={[ { size: 250, + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -51,6 +71,9 @@ export default function StandardServerTable({ { size: 100, accessorKey: "id", + // The resource count is computed on the client, + // it cannot be sorted server side. + enableSorting: !onServerSort, sortingFn: (a, b) => { const sa = resourcesCount(a.original.id); const sb = resourcesCount(b.original.id); @@ -72,6 +95,7 @@ export default function StandardServerTable({ }, { size: 200, + id: "Region", accessorKey: "info.region", header: ({ column }) => ( @@ -79,6 +103,7 @@ export default function StandardServerTable({ }, { size: 150, + id: "Version", accessorKey: "info.version", header: ({ column }) => ( @@ -87,6 +112,7 @@ export default function StandardServerTable({ }, { size: 150, + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/stack/table.tsx b/ui/src/resources/stack/table.tsx index dc99f3d459..a904ef0d04 100644 --- a/ui/src/resources/stack/table.tsx +++ b/ui/src/resources/stack/table.tsx @@ -8,17 +8,36 @@ import TableTags from "@/components/tags/table"; import FileSource from "@/components/file-source"; import StackUpdateAvailable from "./update-available"; +const SORT_KEYS = ["Name", "Source", "Host", "State"]; + export default function StackTable({ resources, + onServerSort, ...boxProps }: { resources: Types.StackListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Stack"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="stack-table" data={resources} selectOptions={{ @@ -30,6 +49,7 @@ export default function StackTable({ header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => { return ( @@ -45,6 +65,7 @@ export default function StackTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -53,6 +74,7 @@ export default function StackTable({ header: ({ column }) => ( ), + id: "Host", accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id @@ -79,6 +101,7 @@ export default function StackTable({ size: 200, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/swarm/table.tsx b/ui/src/resources/swarm/table.tsx index 9bdf8a883e..752309e3f7 100644 --- a/ui/src/resources/swarm/table.tsx +++ b/ui/src/resources/swarm/table.tsx @@ -6,17 +6,36 @@ import { SwarmComponents } from "."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "State"]; + export default function SwarmTable({ resources, + onServerSort, ...boxProps }: { resources: Types.SwarmListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Swarm"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="swarm-table" data={resources} selectOptions={{ @@ -28,6 +47,7 @@ export default function SwarmTable({ header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -36,6 +56,7 @@ export default function SwarmTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => , size: 120, diff --git a/ui/src/resources/sync/table.tsx b/ui/src/resources/sync/table.tsx index a351829da9..8c50c3e9a9 100644 --- a/ui/src/resources/sync/table.tsx +++ b/ui/src/resources/sync/table.tsx @@ -7,16 +7,35 @@ import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import FileSource from "@/components/file-source"; +const SORT_KEYS = ["Name", "Source", "Branch", "State"]; + export default function ResourceSyncTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ResourceSyncListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("ResourceSync"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="syncs" data={resources} selectOptions={{ @@ -28,6 +47,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => ( @@ -38,6 +58,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -46,6 +67,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "Branch", accessorKey: "info.branch", size: 200, }, @@ -53,6 +75,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => ( diff --git a/ui/yarn.lock b/ui/yarn.lock index 304604f4bb..094ac354d6 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -913,10 +913,10 @@ mogh_auth_client@^1.7.0: dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.0.tgz#a2049bd0b33cf9e76c1cf9afb08062e0d9074806" - integrity sha512-epMZtg+wLBsIJ8vm048nD1qfvEau106asNbMjT6/Qv6ebF0lqvynk7K3vGbcAhOvVEFK0tde51CmfY/UcXt2+Q== +mogh_ui@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.1.tgz#09ab30985de93c883ae5f23b3658e62907c0fcea" + integrity sha512-McG0H/4mCCvLfpJiRCwODDLBKxhj+o7qK4Cx0KXxz8uYVBvkNiI8loNPxv/dPP/nIAghDA5WETtJfMLosJfQ3Q== monaco-editor@^0.55.1: version "0.55.1" From 247af73c095e18c9d0210adba9cc825ae2c21c9b Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 22:46:19 -0700 Subject: [PATCH 122/203] fix cli usage --- bin/cli/src/command/list.rs | 22 ++++++++++++++++++++++ ui/src/pages/containers.tsx | 2 +- ui/src/pages/dashboard/tables.tsx | 2 +- ui/src/pages/resources.tsx | 2 +- ui/src/resources/multi-selector.tsx | 2 +- ui/src/resources/selector.tsx | 8 ++++---- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index 777d9ef4b9..78eb999c74 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -327,6 +327,8 @@ impl ListResources for ServerListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await?; let names = parse_wildcards(&filters.names); @@ -373,6 +375,8 @@ impl ListResources for SwarmListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await?; let names = parse_wildcards(&filters.names); @@ -429,6 +433,8 @@ impl ListResources for StackListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; stacks.iter_mut().for_each(|stack| { @@ -506,6 +512,8 @@ impl ListResources for DeploymentListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; deployments.iter_mut().for_each(|deployment| { @@ -584,6 +592,8 @@ impl ListResources for BuildListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; builds.iter_mut().for_each(|build| { @@ -648,6 +658,8 @@ impl ListResources for RepoListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -699,6 +711,8 @@ impl ListResources for ProcedureListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -750,6 +764,8 @@ impl ListResources for ActionListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -801,6 +817,8 @@ impl ListResources for ResourceSyncListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -849,6 +867,8 @@ impl ListResources for BuilderListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -884,6 +904,8 @@ impl ListResources for AlerterListItem { .build(), limit: Some(100), page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index ae97afabf9..59c11031b3 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -51,7 +51,7 @@ export default function Containers() { }).data ?? []; // Prevent flashes during loading - const containers = useDebounce(_containers, 200); + const containers = useDebounce(_containers, 300); const Table = useMemo( () => ( diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 9ca4c4b9b8..059d94e423 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -86,7 +86,7 @@ function TableSection({ sort_desc: sort.sort_desc, }).data ?? []; // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 200); + const resources = useDebounce(_resources, 300); const Table = useMemo( () => show && , diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 9638ce7738..a0d377b7ed 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -103,7 +103,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) { const [resources, setResources] = useState(_resources); useEffect(() => setResources(_resources), [type]); useEffect(() => { - const handler = setTimeout(() => setResources(_resources), 200); + const handler = setTimeout(() => setResources(_resources), 300); return () => { clearTimeout(handler); }; diff --git a/ui/src/resources/multi-selector.tsx b/ui/src/resources/multi-selector.tsx index c901a3dce6..ae4d477340 100644 --- a/ui/src/resources/multi-selector.tsx +++ b/ui/src/resources/multi-selector.tsx @@ -40,7 +40,7 @@ export default function ResourceMultiSelector({ useRead(`List${type}s`, { query: { templates, terms }, limit: 10 }).data ?? []; // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 200); + const resources = useDebounce(_resources, 300); // Keep the selected names in the options, // even when they don't match the current search. diff --git a/ui/src/resources/selector.tsx b/ui/src/resources/selector.tsx index e43d377378..5fe3bbf09d 100644 --- a/ui/src/resources/selector.tsx +++ b/ui/src/resources/selector.tsx @@ -49,14 +49,14 @@ export default function ResourceSelector({ ...comboboxProps }: ResourceSelectorProps) { const { search, setSearch, combobox } = useSearchCombobox(); - + const debouncedSearch = useDebounce(search, 200); const terms = useMemo( () => - search + debouncedSearch .split(" ") .map((item) => item.trim()) .filter((item) => !!item), - [search], + [debouncedSearch], ); const Components = ResourceComponents[type]; @@ -78,7 +78,7 @@ export default function ResourceSelector({ ); // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 100); + const resources = useDebounce(_resources, 300); const name = selectedResource?.name; From caa125d17a6b0e1b83509fac1efe9fc9e94ccb44 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 22:47:04 -0700 Subject: [PATCH 123/203] deploy 2.3.0-dev-32 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ee6493f1d..bcda7388d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 25b21e9eee..99aea26813 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-31" +version = "2.3.0-dev-32" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From efa7e5c169aba7bc2863723718b8c06a09023cb2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 23:09:44 -0700 Subject: [PATCH 124/203] ensure required refetch intervals are still passed for data freshness --- ui/package.json | 2 +- ui/src/lib/hooks.ts | 10 +++++- ui/src/pages/containers.tsx | 18 ++++++---- ui/src/pages/dashboard/active.tsx | 48 ++++++++++++++++++++------- ui/src/pages/dashboard/tables.tsx | 16 +++++---- ui/src/pages/resources.tsx | 16 +++++---- ui/src/pages/stack-service/index.tsx | 6 +++- ui/src/resources/action/index.tsx | 8 +++-- ui/src/resources/alerter/index.tsx | 8 +++-- ui/src/resources/build/index.tsx | 8 +++-- ui/src/resources/builder/index.tsx | 18 +++++++--- ui/src/resources/deployment/index.tsx | 8 +++-- ui/src/resources/procedure/index.tsx | 8 +++-- ui/src/resources/repo/index.tsx | 8 +++-- ui/src/resources/server/index.tsx | 8 +++-- ui/src/resources/stack/index.tsx | 8 +++-- ui/src/resources/swarm/index.tsx | 8 +++-- ui/src/resources/sync/index.tsx | 8 +++-- ui/yarn.lock | 8 ++--- 19 files changed, 159 insertions(+), 63 deletions(-) diff --git a/ui/package.json b/ui/package.json index e72652fb55..586652da13 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,7 @@ "jotai-location": "^0.6.2", "lucide-react": "^1.22.0", "mogh_auth_client": "^1.7.0", - "mogh_ui": "^0.7.1", + "mogh_ui": "^0.7.2", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.5.1", "react": "^19.2.7", diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 62ed6070d0..5983c7e217 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -252,16 +252,23 @@ function batchedListItemFetch( * plain `useRead("Lists", { query: { names: [id] } })` call, so cache * entries are shared and `useInvalidate(["Lists"])` style prefix * invalidations reach these too. + * + * Polls by default so state colors etc. stay current for passive + * state changes (which don't come in over the update websocket). + * The refetches re-batch, so this costs one small request + * per resource type per interval. Pass `false` to disable. */ export function useListItemQuery( type: UsableResource, id: string | undefined, + refetchInterval: number | false = 15_000, ) { const hasJwt = !!MoghAuth.LOGIN_TOKENS.jwt(); return useQuery({ queryKey: [`List${type}s`, { query: { names: id ? [id] : [] } }], queryFn: () => batchedListItemFetch(type, id!), enabled: hasJwt && !!id, + refetchInterval, }); } @@ -274,8 +281,9 @@ export function useListItem( type: T, id: string | undefined, useName?: boolean, + refetchInterval?: number | false, ): ReadResponses[`List${T}s`][number] | undefined { - const { data } = useListItemQuery(type, id); + const { data } = useListItemQuery(type, id, refetchInterval); return (data as Types.ResourceListItem[] | undefined)?.find((r) => useName ? r.name === id : r.id === id, ) as ReadResponses[`List${T}s`][number] | undefined; diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 59c11031b3..c7214bf019 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -42,13 +42,17 @@ export default function Containers() { }, [selectedServers, tags]); const _containers = - useRead("ListAllContainers", { - terms: searchTerms, - servers: selectedServers, - tags, - page, - limit: RESOURCE_PAGE_SIZE, - }).data ?? []; + useRead( + "ListAllContainers", + { + terms: searchTerms, + servers: selectedServers, + tags, + page, + limit: RESOURCE_PAGE_SIZE, + }, + { refetchInterval: 15_000 }, + ).data ?? []; // Prevent flashes during loading const containers = useDebounce(_containers, 300); diff --git a/ui/src/pages/dashboard/active.tsx b/ui/src/pages/dashboard/active.tsx index afb13aa582..bb5dfcde87 100644 --- a/ui/src/pages/dashboard/active.tsx +++ b/ui/src/pages/dashboard/active.tsx @@ -19,7 +19,9 @@ import { ReactNode } from "react"; export default function DashboardActiveResources() { const stacks = - useRead("ListStacks", { + useRead( + "ListStacks", + { query: { specific: { states: [ @@ -30,9 +32,13 @@ export default function DashboardActiveResources() { }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const deployments = - useRead("ListDeployments", { + useRead( + "ListDeployments", + { query: { specific: { states: [ @@ -43,16 +49,24 @@ export default function DashboardActiveResources() { }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const builds = - useRead("ListBuilds", { + useRead( + "ListBuilds", + { query: { specific: { states: [Types.BuildState.Building] }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const repos = - useRead("ListRepos", { + useRead( + "ListRepos", + { query: { specific: { states: [ @@ -63,21 +77,31 @@ export default function DashboardActiveResources() { }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const procedures = - useRead("ListProcedures", { + useRead( + "ListProcedures", + { query: { specific: { states: [Types.ProcedureState.Running] }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const actions = - useRead("ListActions", { + useRead( + "ListActions", + { query: { specific: { states: [Types.ActionState.Running] }, }, limit: 0, - }).data ?? []; + }, + { refetchInterval: 15_000 }, + ).data ?? []; const globalAutoUpdates = useRead("ListUpdates", { query: { diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 059d94e423..60d4a8fc42 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -79,12 +79,16 @@ function TableSection({ }, [terms, tags, templates, sort.sort_by, sort.sort_desc]); const _resources = - useRead(`List${type}s`, { - query: { terms, tags, templates }, - page, - sort_by: sort.sort_by as any, - sort_desc: sort.sort_desc, - }).data ?? []; + useRead( + `List${type}s`, + { + query: { terms, tags, templates }, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }, + { refetchInterval: 15_000 }, + ).data ?? []; // Prevent flashing when typing / fetching const resources = useDebounce(_resources, 300); diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index a0d377b7ed..02345f8e29 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -91,12 +91,16 @@ export default function Resources({ _type }: { _type?: UsableResource }) { : undefined, }; const _resources = - useRead(`List${type}s`, { - query, - page, - sort_by: sort.sort_by as any, - sort_desc: sort.sort_desc, - }).data ?? []; + useRead( + `List${type}s`, + { + query, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }, + { refetchInterval: 15_000 }, + ).data ?? []; // Debounce: prevents flashing when typing / fetching. // Must also set immediately upon change of resource type diff --git a/ui/src/pages/stack-service/index.tsx b/ui/src/pages/stack-service/index.tsx index 25a47b8b63..74d26a7451 100644 --- a/ui/src/pages/stack-service/index.tsx +++ b/ui/src/pages/stack-service/index.tsx @@ -60,7 +60,11 @@ function StackServiceInner({ }) { const stack = useStack(stackId); useSetTitle(`${stack?.name} | ${serviceName}`); - const services = useRead("ListStackServices", { stack: stackId }).data; + const services = useRead( + "ListStackServices", + { stack: stackId }, + { refetchInterval: 10_000 }, + ).data; const service = services?.find((s) => s.service === serviceName); const container = service?.container; diff --git a/ui/src/resources/action/index.tsx b/ui/src/resources/action/index.tsx index 1d687f55ac..dc8200114c 100644 --- a/ui/src/resources/action/index.tsx +++ b/ui/src/resources/action/index.tsx @@ -15,8 +15,12 @@ import ResourceHeader from "../header"; import BatchExecutions from "@/components/batch-executions"; import { RunAction } from "./executions"; -export function useAction(id: string | undefined, useName?: boolean) { - return useListItem("Action", id, useName); +export function useAction( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Action", id, useName, refetchInterval); } export function useFullAction(id: string) { diff --git a/ui/src/resources/alerter/index.tsx b/ui/src/resources/alerter/index.tsx index 3efcaa7f23..87b11a2ee4 100644 --- a/ui/src/resources/alerter/index.tsx +++ b/ui/src/resources/alerter/index.tsx @@ -10,8 +10,12 @@ import { hexColorByIntention } from "mogh_ui"; import BatchExecutions from "@/components/batch-executions"; import { ConfirmButton } from "mogh_ui"; -export function useAlerter(id: string | undefined, useName?: boolean) { - return useListItem("Alerter", id, useName); +export function useAlerter( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Alerter", id, useName, refetchInterval); } export function useFullAlerter(id: string) { diff --git a/ui/src/resources/build/index.tsx b/ui/src/resources/build/index.tsx index f4d78db933..be76d0b758 100644 --- a/ui/src/resources/build/index.tsx +++ b/ui/src/resources/build/index.tsx @@ -26,8 +26,12 @@ import { useState } from "react"; import ResourceSelector from "../selector"; import { hexColorByIntention } from "mogh_ui"; -export function useBuild(id: string | undefined, useName?: boolean) { - return useListItem("Build", id, useName); +export function useBuild( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Build", id, useName, refetchInterval); } export function useFullBuild(id: string) { diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index ef89116d9a..436051cfbd 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -12,8 +12,12 @@ import BatchExecutions from "@/components/batch-executions"; import { useState } from "react"; import { Group, Select } from "@mantine/core"; -export function useBuilder(id: string | undefined, useName?: boolean) { - return useListItem("Builder", id, useName); +export function useBuilder( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Builder", id, useName, refetchInterval); } export function useFullBuilder(id: string) { @@ -80,9 +84,13 @@ export const BuilderComponents: RequiredResourceComponents< (builder?.info.builder_type === "Server" && builder.info.instance_type?.split(",").map((id) => id.trim())) || []; - const servers = useRead("ListServers", { - query: { names: configured }, - }).data?.filter((s) => configured.includes(s.id)); + const servers = useRead( + "ListServers", + { + query: { names: configured }, + }, + { refetchInterval: 15_000 }, + ).data?.filter((s) => configured.includes(s.id)); const coreVersion = useRead("GetVersion", {}).data?.version; const intent = !servers?.length ? "Neutral" diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index d35d773e57..6e1558e0ad 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -28,8 +28,12 @@ import BatchExecutions from "@/components/batch-executions"; import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; -export function useDeployment(id: string | undefined, useName?: boolean) { - return useListItem("Deployment", id, useName); +export function useDeployment( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Deployment", id, useName, refetchInterval); } export function useFullDeployment(id: string) { diff --git a/ui/src/resources/procedure/index.tsx b/ui/src/resources/procedure/index.tsx index 330903007e..e643299c99 100644 --- a/ui/src/resources/procedure/index.tsx +++ b/ui/src/resources/procedure/index.tsx @@ -16,8 +16,12 @@ import { useDisclosure } from "@mantine/hooks"; import { updateLogToHtml } from "@/lib/utils"; import { hexColorByIntention } from "mogh_ui"; -export function useProcedure(id: string | undefined, useName?: boolean) { - return useListItem("Procedure", id, useName); +export function useProcedure( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Procedure", id, useName, refetchInterval); } export function useFullProcedure(id: string) { diff --git a/ui/src/resources/repo/index.tsx b/ui/src/resources/repo/index.tsx index 06bea3382e..644c6a407b 100644 --- a/ui/src/resources/repo/index.tsx +++ b/ui/src/resources/repo/index.tsx @@ -17,8 +17,12 @@ import ResourceLink from "../link"; import RepoLink from "@/components/repo-link"; import { hexColorByIntention } from "mogh_ui"; -export function useRepo(id: string | undefined, useName?: boolean) { - return useListItem("Repo", id, useName); +export function useRepo( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Repo", id, useName, refetchInterval); } export function useFullRepo(id: string) { diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 119ac4eec3..455c4a8486 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -24,8 +24,12 @@ import ServerDiskUsage from "./diskUsage"; import ServerCpuUsage from "./stats/current/cpu"; import { HoverError } from "mogh_ui"; -export function useServer(id: string | undefined, useName?: boolean) { - return useListItem("Server", id, useName); +export function useServer( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Server", id, useName, refetchInterval); } export function useFullServer(id: string | undefined) { diff --git a/ui/src/resources/stack/index.tsx b/ui/src/resources/stack/index.tsx index 555d757f66..b438b190a6 100644 --- a/ui/src/resources/stack/index.tsx +++ b/ui/src/resources/stack/index.tsx @@ -42,8 +42,12 @@ import BatchExecutions from "@/components/batch-executions"; import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; -export function useStack(id: string | undefined, useName?: boolean) { - return useListItem("Stack", id, useName); +export function useStack( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Stack", id, useName, refetchInterval); } export function useFullStack(id: string) { diff --git a/ui/src/resources/swarm/index.tsx b/ui/src/resources/swarm/index.tsx index 725204ae5d..9c05680407 100644 --- a/ui/src/resources/swarm/index.tsx +++ b/ui/src/resources/swarm/index.tsx @@ -20,8 +20,12 @@ import SwarmHeaderInfo from "./header-info"; import { HoverError } from "mogh_ui"; import { hexColorByIntention } from "mogh_ui"; -export function useSwarm(id: string | undefined, useName?: boolean) { - return useListItem("Swarm", id, useName); +export function useSwarm( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Swarm", id, useName, refetchInterval); } export function useFullSwarm(id: string) { diff --git a/ui/src/resources/sync/index.tsx b/ui/src/resources/sync/index.tsx index e169f5399a..4272a902fa 100644 --- a/ui/src/resources/sync/index.tsx +++ b/ui/src/resources/sync/index.tsx @@ -16,8 +16,12 @@ import ResourceHeader from "../header"; import BatchExecutions from "@/components/batch-executions"; import { hexColorByIntention } from "mogh_ui"; -export function useResourceSync(id: string | undefined, useName?: boolean) { - return useListItem("ResourceSync", id, useName); +export function useResourceSync( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("ResourceSync", id, useName, refetchInterval); } export function useFullResourceSync(id: string) { diff --git a/ui/yarn.lock b/ui/yarn.lock index 094ac354d6..496921ea59 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -913,10 +913,10 @@ mogh_auth_client@^1.7.0: dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.1.tgz#09ab30985de93c883ae5f23b3658e62907c0fcea" - integrity sha512-McG0H/4mCCvLfpJiRCwODDLBKxhj+o7qK4Cx0KXxz8uYVBvkNiI8loNPxv/dPP/nIAghDA5WETtJfMLosJfQ3Q== +mogh_ui@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.2.tgz#d6d7e42aaa2256679277994bf519eef79c0eec11" + integrity sha512-kRQ/9g2E+gSZzxOc1wwHwiKpldf+mtaAq1tEMAQSF2tTNI59OIZIuJSZHvbcJPQ0IfB7e7meW8Be+3aEmnvHyQ== monaco-editor@^0.55.1: version "0.55.1" From a53b2edbb0fd90c1d3b4b4ca2252a94642737be7 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 9 Jul 2026 23:13:56 -0700 Subject: [PATCH 125/203] omni search services before containers --- ui/src/app/topbar/omni-search/hooks.tsx | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 7c2ebf2ad9..a15150f53e 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -226,6 +226,25 @@ export function useOmniSearch(opened: boolean): { }; }), + { + group: "Services", + actions: + services?.map((service) => { + const intention = service?.swarm_service?.State + ? swarmStateIntention(service?.swarm_service?.State) + : containerStateIntention(service?.container?.state); + const color = hexColorByIntention(intention); + return { + id: service.stack_id + " " + service.service, + label: service.service, + description: "Stack: " + service.stack_name, + onClick: () => + nav(`/stacks/${service.stack_id}/service/${service.service}`), + leftSection: , + }; + }) ?? [], + }, + { group: "Containers", actions: @@ -247,25 +266,6 @@ export function useOmniSearch(opened: boolean): { })) ?? [], }, - { - group: "Services", - actions: - services?.map((service) => { - const intention = service?.swarm_service?.State - ? swarmStateIntention(service?.swarm_service?.State) - : containerStateIntention(service?.container?.state); - const color = hexColorByIntention(intention); - return { - id: service.stack_id + " " + service.service, - label: service.service, - description: "Stack: " + service.stack_name, - onClick: () => - nav(`/stacks/${service.stack_id}/service/${service.service}`), - leftSection: , - }; - }) ?? [], - }, - { group: "Terminals", actions: From 95a040c1eb72ead32ed64e88329729b4d69a907c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 10 Jul 2026 13:09:36 -0700 Subject: [PATCH 126/203] add Deployment custom_name configuration to set specific container name --- bin/core/src/api/execute/deployment.rs | 14 ++-- bin/core/src/api/read/deployment.rs | 33 ++++----- bin/core/src/api/read/docker.rs | 17 ++++- bin/core/src/api/write/deployment.rs | 46 +++++++++--- bin/core/src/helpers/terminal.rs | 2 +- bin/core/src/monitor/resources.rs | 4 +- bin/core/src/resource/deployment.rs | 78 ++++++++++++++++++-- bin/periphery/src/api/container/run.rs | 31 ++++---- bin/periphery/src/api/swarm/service.rs | 29 ++++---- client/core/rs/src/entities/deployment.rs | 36 +++++++-- client/core/ts/src/types.ts | 10 +++ ui/public/client/types.d.ts | 10 +++ ui/src/resources/deployment/config/index.tsx | 12 +++ ui/src/resources/deployment/index.tsx | 21 ++++-- ui/src/resources/deployment/tasks.tsx | 6 +- 15 files changed, 255 insertions(+), 94 deletions(-) diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index 1ca4df4db0..c20782cfa9 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -517,7 +517,7 @@ impl Resolve for StartDeployment { let log = match periphery_client(&server) .await? .request(api::container::StartContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), }) .await { @@ -589,7 +589,7 @@ impl Resolve for RestartDeployment { let log = match periphery_client(&server) .await? .request(api::container::RestartContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), }) .await { @@ -663,7 +663,7 @@ impl Resolve for PauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::PauseContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), }) .await { @@ -735,7 +735,7 @@ impl Resolve for UnpauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::UnpauseContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), }) .await { @@ -811,7 +811,7 @@ impl Resolve for StopDeployment { let log = match periphery_client(&server) .await? .request(api::container::StopContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) @@ -926,7 +926,7 @@ impl Resolve for DestroyDeployment { match swarm_request( &swarm.config.server_ids, api::swarm::RemoveSwarmServices { - services: vec![deployment.name], + services: vec![deployment.custom_name().to_string()], }, ) .await @@ -947,7 +947,7 @@ impl Resolve for DestroyDeployment { match periphery_client(&server) .await? .request(api::container::RemoveContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 022258ebc0..d2e3f80a47 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -6,8 +6,8 @@ use komodo_client::{ entities::{ SwarmOrServer, deployment::{ - Deployment, DeploymentActionState, DeploymentConfig, - DeploymentListItem, DeploymentSortBy, DeploymentState, + Deployment, DeploymentActionState, DeploymentListItem, + DeploymentSortBy, DeploymentState, }, docker::{ container::{Container, ContainerStats}, @@ -208,7 +208,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLog { - service: deployment.name, + service: deployment.custom_name().to_string(), tail, timestamps, no_task_ids: false, @@ -221,7 +221,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLog { - name: deployment.name, + name: deployment.custom_name().to_string(), tail: cmp::min(tail, MAX_LOG_LENGTH), timestamps, }) @@ -260,7 +260,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLogSearch { - service: deployment.name, + service: deployment.custom_name().to_string(), terms, combinator, invert, @@ -275,7 +275,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLogSearch { - name: deployment.name, + name: deployment.custom_name().to_string(), terms, combinator, invert, @@ -328,7 +328,7 @@ impl Resolve for InspectDeploymentContainer { periphery_client(&server) .await? .request(InspectContainer { - name: deployment.name, + name: deployment.custom_name().to_string(), }) .await .context("Failed to inspect container on server") @@ -361,7 +361,7 @@ impl Resolve for InspectDeploymentSwarmService { swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::InspectSwarmService { - service: deployment.name, + service: deployment.custom_name().to_string(), }, ) .await @@ -375,25 +375,24 @@ impl Resolve for GetDeploymentStats { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { - let Deployment { - name, - config: DeploymentConfig { server_id, .. }, - .. - } = get_check_permissions::( + let deployment = get_check_permissions::( &self.deployment, user, PermissionLevel::Read.into(), ) .await?; - if server_id.is_empty() { + if deployment.config.server_id.is_empty() { return Err( - anyhow!("deployment has no server attached").into(), + anyhow!("Deployment has no Server attached").into(), ); } - let server = resource::get::(&server_id).await?; + let server = + resource::get::(&deployment.config.server_id).await?; let res = periphery_client(&server) .await? - .request(api::container::GetContainerStats { name }) + .request(api::container::GetContainerStats { + name: deployment.custom_name().to_string(), + }) .await .context("failed to get stats from periphery")?; Ok(res) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 4086e3b098..efba9d9822 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -30,7 +30,7 @@ use crate::{ permission::{get_check_permissions, list_resources_for_user}, resource, stack::compose_container_match_regex, - state::server_status_cache, + state::{db_client, server_status_cache}, }; impl Resolve for GetContainersSummary { @@ -215,9 +215,22 @@ impl Resolve for GetResourceMatchingContainer { PermissionLevel::Read.into(), ) .await?; - // first check deployments + + // First check deployments with a matching custom container name + if let Ok(Some(deployment)) = db_client() + .deployments + .find_one(doc! { "config.custom_name": &self.container }) + .await + { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Deployment(deployment.id).into(), + }); + } + + // Then check deployments matching by name if let Ok(deployment) = resource::get::(&self.container).await + && deployment.custom_name() == self.container { return Ok(GetResourceMatchingContainerResponse { resource: ResourceTarget::Deployment(deployment.id).into(), diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index 97cae36e23..1ef0ea004d 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -300,17 +300,39 @@ impl Resolve for RenameDeployment { let name = to_container_compatible_name(&self.name); - let container_state = - get_deployment_state(&deployment.id).await?; - - if container_state == DeploymentState::Unknown { - return Err( - anyhow!( - "Cannot rename Deployment when container status is unknown" - ) - .into(), - ); - } + let state = get_deployment_state(&deployment.id).await?; + + // When no custom name is configured, the container / service + // follows the Deployment name, and must be kept matched + // by renaming the container to the new name (Server mode). + // Swarm services cannot be renamed, so the rename fails + // if the service is deployed. + // A configured custom name is unaffected by Deployment rename. + let rename_container = if deployment.config.custom_name.is_empty() + && state != DeploymentState::NotDeployed + { + if !deployment.config.swarm_id.is_empty() { + return Err( + anyhow!( + "Cannot rename Deployment while the Swarm service is deployed, as services cannot be renamed. Destroy the Deployment first, or configure a custom service name to keep the service name unchanged." + ) + .into(), + ); + } + if state == DeploymentState::Unknown + && !deployment.config.server_id.is_empty() + { + return Err( + anyhow!( + "Cannot rename Deployment when container / service status is unknown" + ) + .into(), + ); + } + !deployment.config.server_id.is_empty() + } else { + false + }; let mut update = make_update(&deployment, Operation::RenameDeployment, user); @@ -326,7 +348,7 @@ impl Resolve for RenameDeployment { .await .context("Failed to update Deployment name on db")?; - if container_state != DeploymentState::NotDeployed { + if rename_container { let server = resource::get::(&deployment.config.server_id).await?; let log = periphery_client(&server) diff --git a/bin/core/src/helpers/terminal.rs b/bin/core/src/helpers/terminal.rs index 7ef0787396..363f6dc080 100644 --- a/bin/core/src/helpers/terminal.rs +++ b/bin/core/src/helpers/terminal.rs @@ -332,7 +332,7 @@ pub async fn get_deployment_periphery_container( let periphery = periphery_client(&server).await?; - let container = deployment.name.clone(); + let container = deployment.custom_name().to_string(); Ok(( TerminalTarget::Deployment { diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 5baab23c00..994444528a 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -247,7 +247,7 @@ pub async fn update_swarm_deployment_cache( service .name .as_ref() - .map(|name| name == &deployment.name) + .map(|name| name == deployment.custom_name()) .unwrap_or_default() }) .cloned(); @@ -301,7 +301,7 @@ pub async fn update_server_deployment_cache( for deployment in deployments { let container = containers .iter() - .find(|container| container.name == deployment.name) + .find(|container| container.name == deployment.custom_name()) .cloned(); let image_digests = container diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index 8a196864ad..c53c721d81 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -1,4 +1,4 @@ -use anyhow::Context; +use anyhow::{Context, anyhow}; use database::mungos::mongodb::Collection; use formatting::format_serror; use indexmap::IndexSet; @@ -23,7 +23,8 @@ use komodo_client::entities::{ user::User, }; use periphery_client::api::{ - container::RemoveContainer, swarm::RemoveSwarmServices, + container::{RemoveContainer, RenameContainer}, + swarm::RemoveSwarmServices, }; use crate::{ @@ -171,6 +172,7 @@ impl super::KomodoResource for Deployment { status: status.as_ref().and_then(|s| { s.curr.container.as_ref().and_then(|c| c.status.to_owned()) }), + custom_name: deployment.config.custom_name, image, update_available, swarm_id: deployment.config.swarm_id, @@ -249,11 +251,12 @@ impl super::KomodoResource for Deployment { } async fn validate_update_config( - _id: &str, + id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { - validate_config(config, user).await + validate_config(config, user).await?; + handle_custom_name_update(id, config).await } async fn post_update( @@ -319,7 +322,7 @@ impl super::KomodoResource for Deployment { SwarmOrServer::Swarm(swarm) => match swarm_request( &swarm.config.server_ids, RemoveSwarmServices { - services: vec![deployment.name.clone()], + services: vec![deployment.custom_name().to_string()], }, ) .await @@ -357,7 +360,7 @@ impl super::KomodoResource for Deployment { }; match periphery .request(RemoveContainer { - name: deployment.name.clone(), + name: deployment.custom_name().to_string(), signal: deployment.config.termination_signal.into(), time: deployment.config.termination_timeout.into(), }) @@ -441,12 +444,75 @@ async fn validate_config( environment_vars_from_str(environment) .context("Invalid environment")?; } + if let Some(custom_name) = &config.custom_name + && !custom_name.is_empty() + { + config.custom_name = + Some(to_container_compatible_name(custom_name)); + } if let Some(extra_args) = &mut config.extra_args { extra_args.retain(|v| !empty_or_only_spaces(v)) } Ok(()) } +/// If a config update changes the custom container name, +/// the container is renamed to keep the Deployment matched to it. +/// This runs before the update is written to the database, +/// so a failed rename rejects the update and nothing is left mismatched. +/// Swarm services cannot be renamed, so the update is rejected +/// while the service is deployed. +async fn handle_custom_name_update( + id: &str, + config: &PartialDeploymentConfig, +) -> anyhow::Result<()> { + let Some(custom_name) = &config.custom_name else { + return Ok(()); + }; + let deployment = super::get::(id).await?; + // custom_name is already made container compatible by validate_config. + let new_container_name = if custom_name.is_empty() { + deployment.name.as_str() + } else { + custom_name.as_str() + }; + if deployment.custom_name() == new_container_name { + return Ok(()); + } + if !deployment.config.server_id.is_empty() { + let container_state = + get_deployment_state(&deployment.id).await?; + if container_state == DeploymentState::Unknown { + return Err(anyhow!( + "Cannot change custom container name when container status is unknown" + )); + } + if container_state == DeploymentState::NotDeployed { + return Ok(()); + } + let server = + super::get::(&deployment.config.server_id).await?; + periphery_client(&server) + .await? + .request(RenameContainer { + curr_name: deployment.custom_name().to_string(), + new_name: new_container_name.to_string(), + }) + .await + .context("Failed to rename container to the new custom name")?; + } else if !deployment.config.swarm_id.is_empty() + && get_deployment_state(&deployment.id).await? + != DeploymentState::NotDeployed + { + // Also rejects Unknown state, as it cannot be verified + // that no service is left behind under the old name. + return Err(anyhow!( + "Cannot change custom service name while the service is deployed. Destroy the Deployment first." + )); + } + Ok(()) +} + pub async fn setup_deployment_execution( deployment: &str, user: &User, diff --git a/bin/periphery/src/api/container/run.rs b/bin/periphery/src/api/container/run.rs index 3a00201083..7de73d2499 100644 --- a/bin/periphery/src/api/container/run.rs +++ b/bin/periphery/src/api/container/run.rs @@ -94,7 +94,7 @@ impl Resolve for RunContainer { debug!("image pulled"); let _ = (RemoveContainer { - name: deployment.name.clone(), + name: deployment.custom_name().to_string(), signal: stop_signal, time: stop_time, }) @@ -126,24 +126,21 @@ impl Resolve for RunContainer { } fn docker_run_command( - Deployment { - name, - config: - DeploymentConfig { - volumes, - ports, - network, - command, - restart, - environment, - labels, - extra_args, - .. - }, - .. - }: &Deployment, + deployment: &Deployment, image: &str, ) -> anyhow::Result { + let name = deployment.custom_name(); + let DeploymentConfig { + volumes, + ports, + network, + command, + restart, + environment, + labels, + extra_args, + .. + } = &deployment.config; let mut res = format!("docker run -d --name {name} --network {network}"); diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index d7e20dcd76..0454715a81 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -269,7 +269,7 @@ impl Resolve for CreateSwarmService { }; let log = (RemoveSwarmServices { - services: vec![deployment.name.clone()], + services: vec![deployment.custom_name().to_string()], }) .resolve(args) .await; @@ -310,24 +310,21 @@ impl Resolve for CreateSwarmService { } fn docker_service_create_command( - Deployment { - name, - config: - DeploymentConfig { - volumes, - ports, - network, - command, - environment, - labels, - extra_args, - .. - }, - .. - }: &Deployment, + deployment: &Deployment, image: &str, use_with_registry_auth: bool, ) -> anyhow::Result { + let name = deployment.custom_name(); + let DeploymentConfig { + volumes, + ports, + network, + command, + environment, + labels, + extra_args, + .. + } = &deployment.config; let mut res = format!( "docker service create --name {name} --network {network}" ); diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index 4d385eb52a..b8e2390ba5 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -8,13 +8,7 @@ use strum::{AsRefStr, Display, EnumDiscriminants, EnumString}; use typeshare::typeshare; use crate::{ - deserializers::{ - conversions_deserializer, env_vars_deserializer, - labels_deserializer, option_conversions_deserializer, - option_env_vars_deserializer, option_labels_deserializer, - option_string_list_deserializer, option_term_labels_deserializer, - string_list_deserializer, term_labels_deserializer, - }, + deserializers::*, entities::{ EnvironmentVar, ImageDigest, environment_vars_from_str, optional_str, @@ -39,10 +33,28 @@ pub struct DeploymentSchema( #[typeshare] pub type Deployment = Resource; +impl Deployment { + /// Configured custom container / service name, + /// falling back to deployment name. + pub fn custom_name(&self) -> &str { + optional_str(self.config.custom_name.trim()) + .unwrap_or(self.name.trim()) + } +} + #[typeshare] pub type DeploymentListItem = ResourceListItem; +impl DeploymentListItem { + /// Configured custom container / service name, + /// falling back to deployment name. + pub fn custom_name(&self) -> &str { + optional_str(self.info.custom_name.trim()) + .unwrap_or(self.name.trim()) + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -51,6 +63,9 @@ pub struct DeploymentListItemInfo { pub state: DeploymentState, /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) pub status: Option, + /// Custom container / service name, if different + /// than deployment name + pub custom_name: String, /// The image attached to the deployment. pub image: String, /// Whether there is a newer image available at the same tag. @@ -119,6 +134,12 @@ pub struct DeploymentConfig { #[builder(default)] pub server_id: String, + /// Specify a custom container / service name, + /// if different from Deployment name. + #[serde(default)] + #[builder(default)] + pub custom_name: String, + /// The image which the deployment deploys. /// Can either be a user inputted image, or a Komodo Build. #[serde(default)] @@ -305,6 +326,7 @@ impl Default for DeploymentConfig { Self { swarm_id: Default::default(), server_id: Default::default(), + custom_name: Default::default(), image: Default::default(), image_registry_account: Default::default(), skip_secret_interp: Default::default(), diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index a32c1e5083..6d4c3b0a71 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1328,6 +1328,11 @@ export interface DeploymentConfig { * swarm_id overrides server_id and the Deployment will be in Swarm mode. */ server_id?: string; + /** + * Specify a custom container / service name, + * if different from Deployment name. + */ + custom_name?: string; /** * The image which the deployment deploys. * Can either be a user inputted image, or a Komodo Build. @@ -1471,6 +1476,11 @@ export interface DeploymentListItemInfo { state: DeploymentState; /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; + /** + * Custom container / service name, if different + * than deployment name + */ + custom_name: string; /** The image attached to the deployment. */ image: string; /** Whether there is a newer image available at the same tag. */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index d9d1b3e1ba..e19c5f8127 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -1477,6 +1477,11 @@ export interface DeploymentConfig { * swarm_id overrides server_id and the Deployment will be in Swarm mode. */ server_id?: string; + /** + * Specify a custom container / service name, + * if different from Deployment name. + */ + custom_name?: string; /** * The image which the deployment deploys. * Can either be a user inputted image, or a Komodo Build. @@ -1615,6 +1620,11 @@ export interface DeploymentListItemInfo { state: DeploymentState; /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; + /** + * Custom container / service name, if different + * than deployment name + */ + custom_name: string; /** The image attached to the deployment. */ image: string; /** Whether there is a newer image available at the same tag. */ diff --git a/ui/src/resources/deployment/config/index.tsx b/ui/src/resources/deployment/config/index.tsx index 9c5023934d..ef3c49e8c8 100644 --- a/ui/src/resources/deployment/config/index.tsx +++ b/ui/src/resources/deployment/config/index.tsx @@ -298,6 +298,18 @@ export default function DeploymentConfig({ }, ], advanced: [ + { + label: "Custom Name", + labelHidden: true, + fields: { + custom_name: { + placeholder: `Custom ${currServerId ? "service" : "container"} name`, + description: currSwarmId + ? "Optionally set a custom service name, if different from the Deployment name." + : "Optionally set a custom container name, if different from the Deployment name.", + }, + }, + }, { label: "Command", labelHidden: true, diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index 6e1558e0ad..ccb11cb093 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -44,6 +44,15 @@ export function useFullDeployment(id: string) { ).data; } +/** The container / service name for the deployment, + * ie the configured custom name if set, otherwise the deployment name. */ +export function deploymentContainerName( + deployment: Types.DeploymentListItem | undefined, +) { + if (!deployment) return undefined; + return deployment.info.custom_name || deployment.name; +} + export const DeploymentComponents: RequiredResourceComponents< Types.DeploymentConfig, Types.DeploymentInfo, @@ -193,11 +202,12 @@ export const DeploymentComponents: RequiredResourceComponents< }, DockerResource: ({ id }) => { const deployment = useDeployment(id); + const containerName = deploymentContainerName(deployment); const service = useRead( "ListSwarmServices", { swarm: deployment?.info.swarm_id! }, { enabled: !!deployment?.info.swarm_id }, - ).data?.find((service) => service.Name === deployment?.name); + ).data?.find((service) => service.Name === containerName); if ( !deployment || [ @@ -213,8 +223,8 @@ export const DeploymentComponents: RequiredResourceComponents< {service?.Configs.map((config) => ( ); @@ -248,13 +258,14 @@ export const DeploymentComponents: RequiredResourceComponents< }, Ports: ({ id }) => { const deployment = useDeployment(id); + const containerName = deploymentContainerName(deployment); const container = useRead( "ListContainers", { server: deployment?.info.server_id!, }, { refetchInterval: 10_000, enabled: !!deployment?.info.server_id }, - ).data?.find((container) => container.name === deployment?.name); + ).data?.find((container) => container.name === containerName); if (!container) return null; return ( service.Name === deployment?.name); + ).data?.find( + (service) => service.Name === deploymentContainerName(deployment), + ); const _search = useState(""); if (!swarmId || !service) { From 32417a3f9a11cf36a71c869424d1d8ef73d5b76e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 10 Jul 2026 13:19:14 -0700 Subject: [PATCH 127/203] GetResourceMatchingContainer fix empty container passed check --- bin/core/src/api/read/docker.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index efba9d9822..9d5663f2ad 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -216,11 +216,14 @@ impl Resolve for GetResourceMatchingContainer { ) .await?; - // First check deployments with a matching custom container name - if let Ok(Some(deployment)) = db_client() - .deployments - .find_one(doc! { "config.custom_name": &self.container }) - .await + // First check deployments with a matching custom container name. + // The empty check is required to avoid matching + // deployments with no custom name configured. + if !self.container.is_empty() + && let Ok(Some(deployment)) = db_client() + .deployments + .find_one(doc! { "config.custom_name": &self.container }) + .await { return Ok(GetResourceMatchingContainerResponse { resource: ResourceTarget::Deployment(deployment.id).into(), From 6b4c455b648f62739651bebb522e1ad7aeb486c6 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 10 Jul 2026 13:19:45 -0700 Subject: [PATCH 128/203] validate conatiner / docker compatible names protect against injection --- client/core/rs/src/entities/mod.rs | 45 ++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index e159b41bf3..17472230cf 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -156,13 +156,34 @@ pub fn to_path_compatible_name(name: &str) -> String { name.trim().replace([' ', '\n'], "_").to_string() } -/// Enforce common container naming rules. -/// [a-zA-Z0-9_.-] +/// Enforce docker container / service naming rules: +/// [a-zA-Z0-9][a-zA-Z0-9_.-]* +/// Any other characters are replaced with '_', +/// and leading non-alphanumeric characters are stripped. +/// This also guards against shell injection through the name, +/// as it is passed to shell commands like 'docker stop {name}'. pub fn to_container_compatible_name(name: &str) -> String { - name.trim().replace([' ', ',', '\n', '&'], "_").to_string() + name + .trim() + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') { + c + } else { + '_' + } + }) + .collect::() + .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) + .to_string() } -/// Enforce common docker naming rules, such as only lowercase, and no '.'. +/// Enforce common docker naming rules, such as only lowercase, and no '.': +/// [a-z0-9][a-z0-9_-]* +/// Any other characters are replaced with '_', +/// and leading non-alphanumeric characters are stripped. +/// This also guards against shell injection through the name, +/// as it is passed to shell commands like 'docker compose -p {name}'. /// These apply to: /// - Stacks (docker project name) /// - Builds (docker image name) @@ -170,9 +191,21 @@ pub fn to_container_compatible_name(name: &str) -> String { /// - Volumes pub fn to_docker_compatible_name(name: &str) -> String { name - .to_lowercase() - .replace([' ', '.', ',', '\n', '&'], "_") .trim() + .to_lowercase() + .chars() + .map(|c| { + if c.is_ascii_lowercase() + || c.is_ascii_digit() + || matches!(c, '_' | '-') + { + c + } else { + '_' + } + }) + .collect::() + .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) .to_string() } From f585f614ab322ec556310ac07ad94a744c6ff9e9 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 10 Jul 2026 13:52:41 -0700 Subject: [PATCH 129/203] track deployment deployed name for smooth swarm service renaming behavior --- bin/core/src/api/execute/deployment.rs | 48 +++++++++++-- bin/core/src/api/read/deployment.rs | 14 ++-- bin/core/src/api/read/docker.rs | 9 ++- bin/core/src/api/write/deployment.rs | 83 ++++++++++++++--------- bin/core/src/helpers/terminal.rs | 2 +- bin/core/src/monitor/resources.rs | 4 +- bin/core/src/resource/deployment.rs | 66 ++++++++++++------ bin/periphery/src/api/container/run.rs | 4 +- bin/periphery/src/api/swarm/service.rs | 4 +- client/core/rs/src/entities/deployment.rs | 25 ++++++- client/core/ts/src/types.ts | 11 ++- ui/public/client/types.d.ts | 11 ++- 12 files changed, 202 insertions(+), 79 deletions(-) diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index c20782cfa9..d23e3dc884 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -201,6 +201,12 @@ impl Resolve for Deploy { update_update(update.clone()).await?; let deployment_id = deployment.id.clone(); + // Track the name the container / service is deployed under, + // so the Deployment stays matched to it even if the + // name configuration changes before the next deploy. + let fresh_name = deployment.custom_name().to_string(); + let prev_deployed_name = deployment.info.deployed_name.clone(); + let mut deployed = false; match swarm_or_server { SwarmOrServer::None => unreachable!(), @@ -217,6 +223,7 @@ impl Resolve for Deploy { { Ok(logs) => { refresh_swarm_cache(&swarm, true).await; + deployed = logs.iter().all(|log| log.success); update.logs.extend(logs) } Err(e) => { @@ -241,6 +248,7 @@ impl Resolve for Deploy { { Ok(log) => { refresh_server_cache(&server, true).await; + deployed = log.success; update.logs.push(log) } Err(e) => { @@ -257,6 +265,11 @@ impl Resolve for Deploy { &deployment_id, &DeploymentInfo { latest_image_digest: Default::default(), + deployed_name: if deployed { + fresh_name + } else { + prev_deployed_name + }, }, ) .await @@ -517,7 +530,7 @@ impl Resolve for StartDeployment { let log = match periphery_client(&server) .await? .request(api::container::StartContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await { @@ -589,7 +602,7 @@ impl Resolve for RestartDeployment { let log = match periphery_client(&server) .await? .request(api::container::RestartContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await { @@ -663,7 +676,7 @@ impl Resolve for PauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::PauseContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await { @@ -735,7 +748,7 @@ impl Resolve for UnpauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::UnpauseContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await { @@ -811,7 +824,7 @@ impl Resolve for StopDeployment { let log = match periphery_client(&server) .await? .request(api::container::StopContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) @@ -926,7 +939,7 @@ impl Resolve for DestroyDeployment { match swarm_request( &swarm.config.server_ids, api::swarm::RemoveSwarmServices { - services: vec![deployment.custom_name().to_string()], + services: vec![deployment.deployed_name().to_string()], }, ) .await @@ -947,7 +960,7 @@ impl Resolve for DestroyDeployment { match periphery_client(&server) .await? .request(api::container::RemoveContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) @@ -973,6 +986,27 @@ impl Resolve for DestroyDeployment { } }; + // Clear the tracked deployed name once the container / service + // is confirmed removed, so the next deploy uses the fresh name. + if log.success + && let Err(e) = resource::update_info::( + &deployment.id, + &DeploymentInfo { + latest_image_digest: deployment + .info + .latest_image_digest + .clone(), + deployed_name: Default::default(), + }, + ) + .await + { + warn!( + "Failed to clear deployment {} ({}) deployed name after destroy | {e:#}", + deployment.name, deployment.id, + ); + } + update.logs.push(log); update.finalize(); diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index d2e3f80a47..147e7cb2ae 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -208,7 +208,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLog { - service: deployment.custom_name().to_string(), + service: deployment.deployed_name().to_string(), tail, timestamps, no_task_ids: false, @@ -221,7 +221,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLog { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), tail: cmp::min(tail, MAX_LOG_LENGTH), timestamps, }) @@ -260,7 +260,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLogSearch { - service: deployment.custom_name().to_string(), + service: deployment.deployed_name().to_string(), terms, combinator, invert, @@ -275,7 +275,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLogSearch { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), terms, combinator, invert, @@ -328,7 +328,7 @@ impl Resolve for InspectDeploymentContainer { periphery_client(&server) .await? .request(InspectContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await .context("Failed to inspect container on server") @@ -361,7 +361,7 @@ impl Resolve for InspectDeploymentSwarmService { swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::InspectSwarmService { - service: deployment.custom_name().to_string(), + service: deployment.deployed_name().to_string(), }, ) .await @@ -391,7 +391,7 @@ impl Resolve for GetDeploymentStats { let res = periphery_client(&server) .await? .request(api::container::GetContainerStats { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), }) .await .context("failed to get stats from periphery")?; diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 9d5663f2ad..f2efb31d46 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -216,13 +216,18 @@ impl Resolve for GetResourceMatchingContainer { ) .await?; - // First check deployments with a matching custom container name. + // First check deployments with a matching deployed / custom container name. // The empty check is required to avoid matching // deployments with no custom name configured. if !self.container.is_empty() && let Ok(Some(deployment)) = db_client() .deployments - .find_one(doc! { "config.custom_name": &self.container }) + .find_one(doc! { + "$or": [ + { "info.deployed_name": &self.container }, + { "config.custom_name": &self.container }, + ] + }) .await { return Ok(GetResourceMatchingContainerResponse { diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index 1ef0ea004d..c73018db0c 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -303,58 +303,74 @@ impl Resolve for RenameDeployment { let state = get_deployment_state(&deployment.id).await?; // When no custom name is configured, the container / service - // follows the Deployment name, and must be kept matched - // by renaming the container to the new name (Server mode). - // Swarm services cannot be renamed, so the rename fails - // if the service is deployed. + // follows the Deployment name, and must be kept matched: + // - Server mode: rename the container to the new name. + // - Swarm mode: services cannot be renamed, so the current + // service name is pinned in info.deployed_name, and stays + // in use until the next deploy recreates the service. // A configured custom name is unaffected by Deployment rename. - let rename_container = if deployment.config.custom_name.is_empty() - && state != DeploymentState::NotDeployed - { - if !deployment.config.swarm_id.is_empty() { - return Err( - anyhow!( - "Cannot rename Deployment while the Swarm service is deployed, as services cannot be renamed. Destroy the Deployment first, or configure a custom service name to keep the service name unchanged." - ) - .into(), - ); - } - if state == DeploymentState::Unknown - && !deployment.config.server_id.is_empty() - { - return Err( - anyhow!( - "Cannot rename Deployment when container / service status is unknown" - ) - .into(), - ); - } - !deployment.config.server_id.is_empty() - } else { - false - }; + let follows_name = + deployment.config.custom_name.trim().is_empty(); + let deployed = state != DeploymentState::NotDeployed; + + let rename_container = follows_name + && deployed + && !deployment.config.server_id.is_empty(); + + if rename_container && state == DeploymentState::Unknown { + return Err( + anyhow!( + "Cannot rename Deployment when container status is unknown" + ) + .into(), + ); + } + + // Includes the Unknown state, in case a service + // is still running under the current name. + let pin_deployed_name = follows_name + && deployed + && !deployment.config.swarm_id.is_empty() + && deployment.info.deployed_name.is_empty(); let mut update = make_update(&deployment, Operation::RenameDeployment, user); + let mut set = + doc! { "name": &name, "updated_at": komodo_timestamp() }; + if rename_container { + // Container is renamed to the new name below. + set.insert("info.deployed_name", name.as_str()); + } else if pin_deployed_name { + set.insert("info.deployed_name", deployment.deployed_name()); + } + update_one_by_id( &db_client().deployments, &deployment.id, - database::mungos::update::Update::Set( - doc! { "name": &name, "updated_at": komodo_timestamp() }, - ), + database::mungos::update::Update::Set(set), None, ) .await .context("Failed to update Deployment name on db")?; + if pin_deployed_name { + update.push_simple_log( + "Pin Service Name", + format!( + "Swarm services cannot be renamed, so the current service name '{}' stays in use until the next deploy recreates the service under the new name.", + deployment.deployed_name() + ), + ); + } + if rename_container { let server = resource::get::(&deployment.config.server_id).await?; let log = periphery_client(&server) .await? .request(api::container::RenameContainer { - curr_name: deployment.name.clone(), + curr_name: deployment.deployed_name().to_string(), new_name: name.clone(), }) .await @@ -487,6 +503,7 @@ pub async fn check_deployment_for_update_inner( &deployment.id, &DeploymentInfo { latest_image_digest: latest_digest.clone(), + deployed_name: deployment.info.deployed_name.clone(), }, ) .await?; diff --git a/bin/core/src/helpers/terminal.rs b/bin/core/src/helpers/terminal.rs index 363f6dc080..e4755a0d00 100644 --- a/bin/core/src/helpers/terminal.rs +++ b/bin/core/src/helpers/terminal.rs @@ -332,7 +332,7 @@ pub async fn get_deployment_periphery_container( let periphery = periphery_client(&server).await?; - let container = deployment.custom_name().to_string(); + let container = deployment.deployed_name().to_string(); Ok(( TerminalTarget::Deployment { diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 994444528a..a812e75627 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -247,7 +247,7 @@ pub async fn update_swarm_deployment_cache( service .name .as_ref() - .map(|name| name == deployment.custom_name()) + .map(|name| name == deployment.deployed_name()) .unwrap_or_default() }) .cloned(); @@ -301,7 +301,7 @@ pub async fn update_server_deployment_cache( for deployment in deployments { let container = containers .iter() - .find(|container| container.name == deployment.custom_name()) + .find(|container| container.name == deployment.deployed_name()) .cloned(); let image_digests = container diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index c53c721d81..be3cd2facc 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -1,5 +1,7 @@ use anyhow::{Context, anyhow}; -use database::mungos::mongodb::Collection; +use database::mungos::{ + by_id::update_one_by_id, mongodb::Collection, mongodb::bson::doc, +}; use formatting::format_serror; use indexmap::IndexSet; use komodo_client::entities::{ @@ -11,7 +13,7 @@ use komodo_client::entities::{ DeploymentListItemInfo, DeploymentQuerySpecifics, DeploymentState, PartialDeploymentConfig, conversions_from_str, }, - environment_vars_from_str, + environment_vars_from_str, optional_string, permission::{ PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission, }, @@ -172,7 +174,8 @@ impl super::KomodoResource for Deployment { status: status.as_ref().and_then(|s| { s.curr.container.as_ref().and_then(|c| c.status.to_owned()) }), - custom_name: deployment.config.custom_name, + custom_name: optional_string(deployment.info.deployed_name) + .unwrap_or(deployment.config.custom_name), image, update_available, swarm_id: deployment.config.swarm_id, @@ -322,7 +325,7 @@ impl super::KomodoResource for Deployment { SwarmOrServer::Swarm(swarm) => match swarm_request( &swarm.config.server_ids, RemoveSwarmServices { - services: vec![deployment.custom_name().to_string()], + services: vec![deployment.deployed_name().to_string()], }, ) .await @@ -360,7 +363,7 @@ impl super::KomodoResource for Deployment { }; match periphery .request(RemoveContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), signal: deployment.config.termination_signal.into(), time: deployment.config.termination_timeout.into(), }) @@ -445,7 +448,7 @@ async fn validate_config( .context("Invalid environment")?; } if let Some(custom_name) = &config.custom_name - && !custom_name.is_empty() + && !custom_name.trim().is_empty() { config.custom_name = Some(to_container_compatible_name(custom_name)); @@ -456,12 +459,14 @@ async fn validate_config( Ok(()) } -/// If a config update changes the custom container name, -/// the container is renamed to keep the Deployment matched to it. -/// This runs before the update is written to the database, -/// so a failed rename rejects the update and nothing is left mismatched. -/// Swarm services cannot be renamed, so the update is rejected -/// while the service is deployed. +/// Handles a config update which changes the custom container name: +/// - Server mode: the container is renamed to keep the Deployment +/// matched to it. This runs before the update is written to the +/// database, so a failed rename rejects the update and nothing +/// is left mismatched. +/// - Swarm mode: services cannot be renamed, so the current +/// service name is pinned in info.deployed_name, and stays +/// in use until the next deploy recreates the service. async fn handle_custom_name_update( id: &str, config: &PartialDeploymentConfig, @@ -479,7 +484,9 @@ async fn handle_custom_name_update( if deployment.custom_name() == new_container_name { return Ok(()); } - if !deployment.config.server_id.is_empty() { + if deployment.config.swarm_id.is_empty() + && !deployment.config.server_id.is_empty() + { let container_state = get_deployment_state(&deployment.id).await?; if container_state == DeploymentState::Unknown { @@ -495,20 +502,41 @@ async fn handle_custom_name_update( periphery_client(&server) .await? .request(RenameContainer { - curr_name: deployment.custom_name().to_string(), + curr_name: deployment.deployed_name().to_string(), new_name: new_container_name.to_string(), }) .await .context("Failed to rename container to the new custom name")?; + // Keep the tracked deployed name matched to the renamed container. + update_one_by_id( + &db_client().deployments, + &deployment.id, + database::mungos::update::Update::Set( + doc! { "info.deployed_name": new_container_name }, + ), + None, + ) + .await + .context( + "Failed to update deployed name on db after container rename", + )?; } else if !deployment.config.swarm_id.is_empty() + && deployment.info.deployed_name.is_empty() + // Includes the Unknown state, in case a service + // is still running under the current name. && get_deployment_state(&deployment.id).await? != DeploymentState::NotDeployed { - // Also rejects Unknown state, as it cannot be verified - // that no service is left behind under the old name. - return Err(anyhow!( - "Cannot change custom service name while the service is deployed. Destroy the Deployment first." - )); + update_one_by_id( + &db_client().deployments, + &deployment.id, + database::mungos::update::Update::Set( + doc! { "info.deployed_name": deployment.deployed_name() }, + ), + None, + ) + .await + .context("Failed to pin deployed service name on db")?; } Ok(()) } diff --git a/bin/periphery/src/api/container/run.rs b/bin/periphery/src/api/container/run.rs index 7de73d2499..3b889b31d7 100644 --- a/bin/periphery/src/api/container/run.rs +++ b/bin/periphery/src/api/container/run.rs @@ -93,8 +93,10 @@ impl Resolve for RunContainer { let _ = pull_image(image).await; debug!("image pulled"); + // Remove the existing container under the previously deployed name, + // in case the name configuration changed since the last deploy. let _ = (RemoveContainer { - name: deployment.custom_name().to_string(), + name: deployment.deployed_name().to_string(), signal: stop_signal, time: stop_time, }) diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 0454715a81..75dcdec450 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -268,8 +268,10 @@ impl Resolve for CreateSwarmService { } }; + // Remove the existing service under the previously deployed name, + // in case the name configuration changed since the last deploy. let log = (RemoveSwarmServices { - services: vec![deployment.custom_name().to_string()], + services: vec![deployment.deployed_name().to_string()], }) .resolve(args) .await; diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index b8e2390ba5..c81a17b450 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -36,10 +36,25 @@ pub type Deployment = Resource; impl Deployment { /// Configured custom container / service name, /// falling back to deployment name. + /// This is the name the next deploy will use. pub fn custom_name(&self) -> &str { optional_str(self.config.custom_name.trim()) .unwrap_or(self.name.trim()) } + + /// The name of the currently deployed container / service, + /// falling back to [Deployment::custom_name]. + /// May be different than custom_name if the name configuration + /// changed since the last deploy. Use this to match the Deployment + /// against the existing container / service. + pub fn deployed_name(&self) -> &str { + let trimmed = self.info.deployed_name.trim(); + if !trimmed.is_empty() { + trimmed + } else { + self.custom_name() + } + } } #[typeshare] @@ -63,8 +78,9 @@ pub struct DeploymentListItemInfo { pub state: DeploymentState, /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) pub status: Option, - /// Custom container / service name, if different - /// than deployment name + /// The container / service name, if different than + /// the deployment name. Uses the currently deployed name + /// if deployed, else the configured custom name. pub custom_name: String, /// The image attached to the deployment. pub image: String, @@ -92,6 +108,11 @@ pub struct DeploymentInfo { /// This includes both the image name / tag, and the specific digest hash. #[serde(default)] pub latest_image_digest: ImageDigest, + /// The container / service name used at the time of the last deploy. + /// Kept to match the Deployment to its container / service + /// even if the name configuration changes before the next deploy. + #[serde(default)] + pub deployed_name: String, } #[typeshare(serialized_as = "Partial")] diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 6d4c3b0a71..c1e615aa8a 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1432,6 +1432,12 @@ export interface DeploymentInfo { * This includes both the image name / tag, and the specific digest hash. */ latest_image_digest?: ImageDigest; + /** + * The container / service name used at the time of the last deploy. + * Kept to match the Deployment to its container / service + * even if the name configuration changes before the next deploy. + */ + deployed_name?: string; } export type Deployment = Resource; @@ -1477,8 +1483,9 @@ export interface DeploymentListItemInfo { /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; /** - * Custom container / service name, if different - * than deployment name + * The container / service name, if different than + * the deployment name. Uses the currently deployed name + * if deployed, else the configured custom name. */ custom_name: string; /** The image attached to the deployment. */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index e19c5f8127..542124c7c9 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -1579,6 +1579,12 @@ export interface DeploymentInfo { * This includes both the image name / tag, and the specific digest hash. */ latest_image_digest?: ImageDigest; + /** + * The container / service name used at the time of the last deploy. + * Kept to match the Deployment to its container / service + * even if the name configuration changes before the next deploy. + */ + deployed_name?: string; } export type Deployment = Resource; /** @@ -1621,8 +1627,9 @@ export interface DeploymentListItemInfo { /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; /** - * Custom container / service name, if different - * than deployment name + * The container / service name, if different than + * the deployment name. Uses the currently deployed name + * if deployed, else the configured custom name. */ custom_name: string; /** The image attached to the deployment. */ From b3782699027436b6aa473a5fdfae4145061aab2f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 10 Jul 2026 13:53:07 -0700 Subject: [PATCH 130/203] deploy 2.3.0-dev-33 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcda7388d7..4fc760b617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 99aea26813..5f79fe120e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-32" +version = "2.3.0-dev-33" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 292bc1a941b6daf69d117ef8b9a5dc57e25fad4e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 00:33:18 -0700 Subject: [PATCH 131/203] batch executions support filtering by tag --- bin/core/src/api/execute/action.rs | 9 +- bin/core/src/api/execute/build.rs | 9 +- bin/core/src/api/execute/deployment.rs | 11 +- bin/core/src/api/execute/mod.rs | 20 +- bin/core/src/api/execute/procedure.rs | 8 +- bin/core/src/api/execute/repo.rs | 29 +- bin/core/src/api/execute/stack.rs | 31 +- bin/core/src/api/write/deployment.rs | 19 +- bin/core/src/api/write/stack.rs | 17 +- bin/core/src/permission.rs | 19 +- bin/core/src/resource/mod.rs | 5 +- client/core/rs/src/api/execute/action.rs | 5 + client/core/rs/src/api/execute/build.rs | 5 + client/core/rs/src/api/execute/deployment.rs | 10 + client/core/rs/src/api/execute/procedure.rs | 5 + client/core/rs/src/api/execute/repo.rs | 15 + client/core/rs/src/api/execute/stack.rs | 20 + client/core/rs/src/api/write/deployment.rs | 4 + client/core/rs/src/api/write/stack.rs | 4 + client/core/ts/src/types.ts | 70 +++ ui/public/client/types.d.ts | 70 +++ ui/src/components/tags/multi-selector.tsx | 77 ++++ .../resources/procedure/config/executions.tsx | 397 +++++++++++------- 23 files changed, 675 insertions(+), 184 deletions(-) create mode 100644 ui/src/components/tags/multi-selector.tsx diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 9b8ffdcca9..b3195a0638 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -73,6 +73,7 @@ impl Resolve for BatchRunAction { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -80,8 +81,12 @@ impl Resolve for BatchRunAction { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index bde3bee557..8ecaf99a5c 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -78,6 +78,7 @@ impl Resolve for BatchRunBuild { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -85,8 +86,12 @@ impl Resolve for BatchRunBuild { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index d23e3dc884..9e50cac9b7 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -58,6 +58,7 @@ impl Resolve for BatchDeploy { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -65,8 +66,12 @@ impl Resolve for BatchDeploy { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -871,6 +876,7 @@ impl Resolve for BatchDestroyDeployment { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -880,6 +886,7 @@ impl Resolve for BatchDestroyDeployment { Ok( super::batch_execute::( &self.pattern, + self.tags, user, ) .await?, diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index fdf8abbb20..cf4aa6d991 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -13,6 +13,7 @@ use komodo_client::{ entities::{ Operation, permission::PermissionLevel, + resource::ResourceQuery, update::{Log, Update}, user::User, }, @@ -29,7 +30,10 @@ use uuid::Uuid; use crate::{ auth::KomodoAuthImpl, - helpers::update::{init_execution_update, update_update}, + helpers::{ + query::get_all_tags, + update::{init_execution_update, update_update}, + }, resource::{KomodoResource, list_full_for_user_using_pattern}, state::db_client, }; @@ -358,16 +362,26 @@ trait BatchExecute { #[instrument("BatchExecute", skip(user))] async fn batch_execute( pattern: &str, + tags: Vec, user: &User, ) -> anyhow::Result { + let all_tags = if tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let resources = list_full_for_user_using_pattern::( pattern, - Default::default(), + ResourceQuery { + tags, + ..Default::default() + }, None, None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index ab66bdbf6c..4b73de96fc 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -51,8 +51,12 @@ impl Resolve for BatchRunProcedure { ExecuteArgs { user, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs index 8e2864bf73..214e5d5f53 100644 --- a/bin/core/src/api/execute/repo.rs +++ b/bin/core/src/api/execute/repo.rs @@ -58,6 +58,7 @@ impl Resolve for BatchCloneRepo { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -65,8 +66,12 @@ impl Resolve for BatchCloneRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -192,7 +197,8 @@ impl Resolve for BatchPullRepo { fields( task_id = task_id.to_string(), operator = user.id, - pattern = self.pattern + pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -200,8 +206,12 @@ impl Resolve for BatchPullRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -374,6 +384,7 @@ impl Resolve for BatchBuildRepo { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -381,8 +392,12 @@ impl Resolve for BatchBuildRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index 50f20b1d7f..27442ef5e4 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -72,6 +72,7 @@ impl Resolve for BatchDeployStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -79,8 +80,12 @@ impl Resolve for BatchDeployStack { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -349,6 +354,7 @@ impl Resolve for BatchDeployStackIfChanged { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -358,6 +364,7 @@ impl Resolve for BatchDeployStackIfChanged { Ok( super::batch_execute::( &self.pattern, + self.tags, user, ) .await?, @@ -777,6 +784,7 @@ impl Resolve for BatchPullStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -784,8 +792,12 @@ impl Resolve for BatchPullStack { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -1153,15 +1165,20 @@ impl Resolve for BatchDestroyStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( self, ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { - super::batch_execute::(&self.pattern, user) - .await - .map_err(Into::into) + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await + .map_err(Into::into) } } diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index c73018db0c..438b2f101d 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -15,6 +15,7 @@ use komodo_client::{ docker::container::RestartPolicyNameEnum, komodo_timestamp, optional_string, permission::PermissionLevel, + resource::ResourceQuery, server::{Server, ServerState}, to_container_compatible_name, update::Update, @@ -30,7 +31,9 @@ use crate::{ api::execute::{self, ExecuteRequest, ExecutionResult}, helpers::{ periphery_client, - query::{get_deployment_state, get_swarm_or_server}, + query::{ + get_all_tags, get_deployment_state, get_swarm_or_server, + }, registry_token, update::{add_update, make_update, poll_update_until_complete}, }, @@ -660,6 +663,7 @@ impl Resolve for BatchCheckDeploymentForUpdate { fields( operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), skip_auto_update = self.skip_auto_update, wait_for_auto_update = self.wait_for_auto_update, ) @@ -668,14 +672,23 @@ impl Resolve for BatchCheckDeploymentForUpdate { self, WriteArgs { user }: &WriteArgs, ) -> Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let deployments = list_full_for_user_using_pattern::( &self.pattern, - Default::default(), + ResourceQuery { + tags: self.tags, + ..Default::default() + }, None, None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index 02df13c5a2..78e0b3cfe6 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -19,6 +19,7 @@ use komodo_client::{ all_logs_success, komodo_timestamp, permission::PermissionLevel, repo::Repo, + resource::ResourceQuery, stack::{Stack, StackInfo, StackServiceWithUpdate, StackState}, update::Update, user::{auto_redeploy_user, stack_user, system_user}, @@ -36,7 +37,7 @@ use crate::{ api::execute::{self, ExecuteRequest, ExecutionResult}, config::core_config, helpers::{ - query::get_swarm_or_server, + query::{get_all_tags, get_swarm_or_server}, stack_git_token, swarm_or_server_request, update::{add_update, make_update, poll_update_until_complete}, }, @@ -1040,6 +1041,7 @@ impl Resolve for BatchCheckStackForUpdate { fields( operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), skip_auto_update = self.skip_auto_update, wait_for_auto_update = self.wait_for_auto_update, ) @@ -1048,14 +1050,23 @@ impl Resolve for BatchCheckStackForUpdate { self, WriteArgs { user }: &WriteArgs, ) -> Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let stacks = list_full_for_user_using_pattern::( &self.pattern, - Default::default(), + ResourceQuery { + tags: self.tags, + ..Default::default() + }, None, None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index 148db07ac9..c0c7d705f0 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -187,7 +187,7 @@ pub enum ListPermits { Unrestricted, /// Visibility must be checked per-resource /// against the permissions table. - Fine(FineListPermits), + Fine(Box), } pub struct FineListPermits { @@ -267,13 +267,16 @@ pub async fn load_list_permits( }) .collect::>(); - Ok(ListPermits::Fine(FineListPermits { - required, - base, - permission_by_resource_id, - additional_specific_cache: Default::default(), - user: user.clone(), - })) + Ok(ListPermits::Fine( + FineListPermits { + required, + base, + permission_by_resource_id, + additional_specific_cache: Default::default(), + user: user.clone(), + } + .into(), + )) } impl ListPermits { diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 7419946afb..ff743d3066 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -396,9 +396,12 @@ pub enum ListItemSort { /// from the in memory caches (eg. state), or which diverge /// from the db field (eg. linked repo sources). /// Collects all matching items before applying pagination. - InMemory(Box std::cmp::Ordering + Send>), + InMemory(InMemoryListItemSortFn), } +pub type InMemoryListItemSortFn = + Box std::cmp::Ordering + Send>; + pub async fn list_for_user( mut query: ResourceQuery, limit: impl Into>, diff --git a/client/core/rs/src/api/execute/action.rs b/client/core/rs/src/api/execute/action.rs index b94a0baa5f..964e4ad897 100644 --- a/client/core/rs/src/api/execute/action.rs +++ b/client/core/rs/src/api/execute/action.rs @@ -85,6 +85,11 @@ pub struct BatchRunAction { /// extra-action-1, extra-action-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/build.rs b/client/core/rs/src/api/execute/build.rs index a0bc4a6f50..939286d774 100644 --- a/client/core/rs/src/api/execute/build.rs +++ b/client/core/rs/src/api/execute/build.rs @@ -84,6 +84,11 @@ pub struct BatchRunBuild { /// extra-build-1, extra-build-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/deployment.rs b/client/core/rs/src/api/execute/deployment.rs index a4099685e0..57725450c1 100644 --- a/client/core/rs/src/api/execute/deployment.rs +++ b/client/core/rs/src/api/execute/deployment.rs @@ -85,6 +85,11 @@ pub struct BatchDeploy { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -352,4 +357,9 @@ pub struct BatchDestroyDeployment { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } diff --git a/client/core/rs/src/api/execute/procedure.rs b/client/core/rs/src/api/execute/procedure.rs index 2ea51d1e21..29d3781524 100644 --- a/client/core/rs/src/api/execute/procedure.rs +++ b/client/core/rs/src/api/execute/procedure.rs @@ -72,6 +72,11 @@ pub struct BatchRunProcedure { /// extra-procedure-1, extra-procedure-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/repo.rs b/client/core/rs/src/api/execute/repo.rs index 8af2a2f713..1b9316779f 100644 --- a/client/core/rs/src/api/execute/repo.rs +++ b/client/core/rs/src/api/execute/repo.rs @@ -80,6 +80,11 @@ pub struct BatchCloneRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -152,6 +157,11 @@ pub struct BatchPullRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -228,6 +238,11 @@ pub struct BatchBuildRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/stack.rs b/client/core/rs/src/api/execute/stack.rs index a3fbc6da49..fc8b416ac3 100644 --- a/client/core/rs/src/api/execute/stack.rs +++ b/client/core/rs/src/api/execute/stack.rs @@ -81,6 +81,11 @@ pub struct BatchDeployStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -153,6 +158,11 @@ pub struct BatchDeployStackIfChanged { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -224,6 +234,11 @@ pub struct BatchPullStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -534,4 +549,9 @@ pub struct BatchDestroyStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } diff --git a/client/core/rs/src/api/write/deployment.rs b/client/core/rs/src/api/write/deployment.rs index 8c62881b48..3f6935a84a 100644 --- a/client/core/rs/src/api/write/deployment.rs +++ b/client/core/rs/src/api/write/deployment.rs @@ -277,6 +277,10 @@ pub struct BatchCheckDeploymentForUpdate { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, /// Normally resources with 'auto_update' will be /// redeployed immediately if updates are found. /// With this enabled, convert this into an UpdateAvailable alert. diff --git a/client/core/rs/src/api/write/stack.rs b/client/core/rs/src/api/write/stack.rs index 8dc44cb0c4..5d249d3827 100644 --- a/client/core/rs/src/api/write/stack.rs +++ b/client/core/rs/src/api/write/stack.rs @@ -310,6 +310,10 @@ pub struct BatchCheckStackForUpdate { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, /// Normally resources with 'auto_update' will be /// redeployed immediately if updates are found. /// With this enabled, convert this into an UpdateAvailable alert. diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index c1e615aa8a..efbb468fc8 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -6057,6 +6057,11 @@ export interface BatchBuildRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */ @@ -6074,6 +6079,11 @@ export interface BatchCheckDeploymentForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -6103,6 +6113,11 @@ export interface BatchCheckStackForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -6137,6 +6152,11 @@ export interface BatchCloneRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** @@ -6163,6 +6183,11 @@ export interface BatchDeploy { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6180,6 +6205,11 @@ export interface BatchDeployStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6197,6 +6227,11 @@ export interface BatchDeployStackIfChanged { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6214,6 +6249,11 @@ export interface BatchDestroyDeployment { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6231,6 +6271,11 @@ export interface BatchDestroyStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } export interface BatchExecutionResponseItemErr { @@ -6253,6 +6298,11 @@ export interface BatchPullRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6270,6 +6320,11 @@ export interface BatchPullStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */ @@ -6287,6 +6342,11 @@ export interface BatchRunAction { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6304,6 +6364,11 @@ export interface BatchRunBuild { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6321,6 +6386,11 @@ export interface BatchRunProcedure { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 542124c7c9..e7e2337dba 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -5863,6 +5863,11 @@ export interface BatchBuildRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */ export interface BatchCheckDeploymentForUpdate { @@ -5879,6 +5884,11 @@ export interface BatchCheckDeploymentForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5907,6 +5917,11 @@ export interface BatchCheckStackForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5940,6 +5955,11 @@ export interface BatchCloneRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** * Delete all terminals on many or all Servers. @@ -5964,6 +5984,11 @@ export interface BatchDeploy { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDeployStack { @@ -5980,6 +6005,11 @@ export interface BatchDeployStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDeployStackIfChanged { @@ -5996,6 +6026,11 @@ export interface BatchDeployStackIfChanged { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDestroyDeployment { @@ -6012,6 +6047,11 @@ export interface BatchDestroyDeployment { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDestroyStack { @@ -6028,6 +6068,11 @@ export interface BatchDestroyStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } export interface BatchExecutionResponseItemErr { name: string; @@ -6048,6 +6093,11 @@ export interface BatchPullRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchPullStack { @@ -6064,6 +6114,11 @@ export interface BatchPullStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */ export interface BatchRunAction { @@ -6080,6 +6135,11 @@ export interface BatchRunAction { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchRunBuild { @@ -6096,6 +6156,11 @@ export interface BatchRunBuild { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchRunProcedure { @@ -6112,6 +6177,11 @@ export interface BatchRunProcedure { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** * Builds the target repo, using the attached builder. Response: [Update]. diff --git a/ui/src/components/tags/multi-selector.tsx b/ui/src/components/tags/multi-selector.tsx new file mode 100644 index 0000000000..ff9136e004 --- /dev/null +++ b/ui/src/components/tags/multi-selector.tsx @@ -0,0 +1,77 @@ +import { useRead } from "@/lib/hooks"; +import { filterBySplit } from "mogh_ui"; +import { + Box, + ComboboxItem, + Group, + MultiSelect, + MultiSelectProps, + Text, +} from "@mantine/core"; +import { useMemo } from "react"; + +export interface TagMultiSelectorProps extends Omit< + MultiSelectProps, + "data" | "value" | "onChange" +> { + /** The selected tag ids (or names with `useName`). */ + value: string[]; + onChange: (tags: string[]) => void; + /** Use tag names as values instead of ids. */ + useName?: boolean; +} + +export default function TagMultiSelector({ + value, + onChange, + useName, + ...props +}: TagMultiSelectorProps) { + const tags = useRead("ListTags", {}).data; + + const { data, colors } = useMemo(() => { + const colors: { [value: string]: string | undefined } = {}; + const data = + tags?.map((tag) => { + const value = useName ? tag.name : tag._id?.$oid!; + colors[value] = tag.color; + return { value, label: tag.name }; + }) ?? []; + return { data, colors }; + }, [tags, useName]); + + return ( + + filterBySplit( + options as ComboboxItem[], + search, + (option) => option.label, + ) + } + renderOption={({ option }) => ( + + {option.label} + + + )} + {...props} + /> + ); +} diff --git a/ui/src/resources/procedure/config/executions.tsx b/ui/src/resources/procedure/config/executions.tsx index c780806145..40fa6b77e5 100644 --- a/ui/src/resources/procedure/config/executions.tsx +++ b/ui/src/resources/procedure/config/executions.tsx @@ -24,6 +24,7 @@ import { Types } from "komodo_client"; import { CheckCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { quote as shellQuote, parse as shellParse } from "shell-quote"; +import TagMultiSelector from "@/components/tags/multi-selector"; export type ExecutionType = Types.Execution["type"]; @@ -84,19 +85,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunProcedure: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, // Action @@ -124,19 +134,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunAction: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, // Build @@ -152,19 +171,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunBuild: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, CancelBuild: { @@ -193,19 +221,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDeploy: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, PullDeployment: { @@ -286,19 +323,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchDestroyDeployment: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, // Stack @@ -335,19 +381,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDeployStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, DeployStackIfChanged: { @@ -362,19 +417,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchDeployStackIfChanged: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, PullStack: { @@ -410,19 +474,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchPullStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, StartStack: { @@ -618,19 +691,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDestroyStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, RunStackService: { @@ -909,19 +991,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchCloneRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, PullRepo: { @@ -936,19 +1027,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchPullRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, BuildRepo: { @@ -963,19 +1063,28 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchBuildRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams((params) => ({ ...params, pattern })) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams((params) => ({ ...params, tags }))} + useName + /> + ), }, CancelRepoBuild: { From 1bfb605a8e1ebd6e8499f3bfe2fec76763ecfd20 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 00:44:40 -0700 Subject: [PATCH 132/203] fix clippy lint --- bin/core/src/api/read/action.rs | 10 ++++---- bin/core/src/api/read/alerter.rs | 10 ++++---- bin/core/src/api/read/build.rs | 10 ++++---- bin/core/src/api/read/builder.rs | 10 ++++---- bin/core/src/api/read/deployment.rs | 10 ++++---- bin/core/src/api/read/procedure.rs | 10 ++++---- bin/core/src/api/read/repo.rs | 10 ++++---- bin/core/src/api/read/server.rs | 10 ++++---- bin/core/src/api/read/stack.rs | 10 ++++---- bin/core/src/api/read/swarm.rs | 10 ++++---- bin/core/src/api/read/sync.rs | 10 ++++---- bin/core/src/resource/mod.rs | 37 ++++++++++------------------- 12 files changed, 79 insertions(+), 68 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 40065fb328..9ff76bb13d 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -64,10 +64,12 @@ impl Resolve for ListActions { }; let actions = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 932497144a..25800c0636 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -59,10 +59,12 @@ impl Resolve for ListAlerters { Ok( resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 5415401dae..247da8387f 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -73,10 +73,12 @@ impl Resolve for ListBuilds { }; let builds = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 7564048dad..06d1d53e10 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -61,10 +61,12 @@ impl Resolve for ListBuilders { Ok( resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 147e7cb2ae..5c3176bd84 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -98,10 +98,12 @@ impl Resolve for ListDeployments { }; let deployments = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 627e86addd..b261bdc6d5 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -63,10 +63,12 @@ impl Resolve for ListProcedures { }; let procedures = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index e958836f99..b1d41ab689 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -64,10 +64,12 @@ impl Resolve for ListRepos { }; let repos = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index f98480674d..21ccc29a6f 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -131,10 +131,12 @@ impl Resolve for ListServers { }; let servers = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index ddba7f4503..4a59c03c70 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -517,10 +517,12 @@ impl Resolve for ListStacks { }; let stacks = resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index b4229fa268..39bb96298b 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -58,10 +58,12 @@ impl Resolve for ListSwarms { Ok( resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index b258108dbf..ae02a5c2b3 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -67,10 +67,12 @@ impl Resolve for ListResourceSyncs { Ok( resource::list_items_for_user::( self.query, - limit, - self.page, - self.sort_desc, - sort_by, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index ff743d3066..b17bcecc9c 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -279,6 +279,13 @@ pub async fn list_all_resources( }) } +pub struct ListItemsQueryOptions { + pub limit: u64, + pub page: u64, + pub sort_desc: bool, + pub sort_by: ListItemSort, +} + /// List item pagination for the `List` apis, driving the /// mongo cursor directly instead of collecting the full resource /// list in memory. Each resource pulled from the cursor is checked @@ -290,10 +297,12 @@ pub async fn list_all_resources( /// Stops pulling from the cursor as soon as the page is full. pub async fn list_items_for_user( mut query: ResourceQuery, - limit: u64, - page: u64, - sort_desc: bool, - sort_by: ListItemSort, + ListItemsQueryOptions { + limit, + page, + sort_desc, + sort_by, + }: ListItemsQueryOptions, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], @@ -419,26 +428,6 @@ pub async fn list_for_user( .await } -// // pub async fn list_for_user_using_pattern( -// pattern: &str, -// query: ResourceQuery, -// user: &User, -// permissions: PermissionLevelAndSpecifics, -// all_tags: &[Tag], -// ) -> anyhow::Result> { -// let list = list_full_for_user_using_pattern::( -// pattern, -// query, -// user, -// permissions, -// all_tags, -// ) -// .await? -// .into_iter() -// .map(|resource| T::to_list_item(resource)); -// Ok(join_all(list).await) -// } - pub async fn list_for_user_using_document( filters: Document, limit: impl Into>, From c65e2e9f00dfa0f85103ba71eed551a4c0d356f1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 01:15:02 -0700 Subject: [PATCH 133/203] improve tag multi selector matching resource sync config --- ui/src/components/tags/multi-selector.tsx | 100 ++++++++---------- .../resources/procedure/config/executions.tsx | 72 ++++++++----- ui/src/resources/sync/config.tsx | 42 ++------ 3 files changed, 100 insertions(+), 114 deletions(-) diff --git a/ui/src/components/tags/multi-selector.tsx b/ui/src/components/tags/multi-selector.tsx index ff9136e004..0a8223debc 100644 --- a/ui/src/components/tags/multi-selector.tsx +++ b/ui/src/components/tags/multi-selector.tsx @@ -1,77 +1,63 @@ import { useRead } from "@/lib/hooks"; -import { filterBySplit } from "mogh_ui"; -import { - Box, - ComboboxItem, - Group, - MultiSelect, - MultiSelectProps, - Text, -} from "@mantine/core"; -import { useMemo } from "react"; +import { ComboboxProps, Group } from "@mantine/core"; +import TagSelector from "./selector"; +import Tags from "."; +import { CircleMinus } from "lucide-react"; -export interface TagMultiSelectorProps extends Omit< - MultiSelectProps, - "data" | "value" | "onChange" -> { +export interface TagMultiSelectorProps extends ComboboxProps { + title?: string; /** The selected tag ids (or names with `useName`). */ value: string[]; onChange: (tags: string[]) => void; /** Use tag names as values instead of ids. */ useName?: boolean; + canCreate?: boolean; } export default function TagMultiSelector({ + title = "Select Tags", value, onChange, useName, + canCreate, + disabled, + position = "bottom-start", ...props }: TagMultiSelectorProps) { const tags = useRead("ListTags", {}).data; - - const { data, colors } = useMemo(() => { - const colors: { [value: string]: string | undefined } = {}; - const data = - tags?.map((tag) => { - const value = useName ? tag.name : tag._id?.$oid!; - colors[value] = tag.color; - return { value, label: tag.name }; - }) ?? []; - return { data, colors }; - }, [tags, useName]); + const otherTags = tags?.filter( + (tag) => !value.includes(useName ? tag.name : tag._id?.$oid!), + ); return ( - - filterBySplit( - options as ComboboxItem[], - search, - (option) => option.label, - ) - } - renderOption={({ option }) => ( - - {option.label} - - - )} - {...props} - /> + + onChange([...value, tag])} + disabled={disabled} + position={position} + useName={useName} + canCreate={canCreate} + {...props} + /> + + value.includes(tag.name)) + .map((tag) => tag.name) ?? [] + } + onBadgeClick={ + disabled + ? undefined + : (toRemove) => + onChange(value.filter((tagName) => tagName !== toRemove)) + } + icon={} + fz="1rem" + useName={useName} + /> + ); } diff --git a/ui/src/resources/procedure/config/executions.tsx b/ui/src/resources/procedure/config/executions.tsx index 40fa6b77e5..7f50216431 100644 --- a/ui/src/resources/procedure/config/executions.tsx +++ b/ui/src/resources/procedure/config/executions.tsx @@ -95,15 +95,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match procedures by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -144,15 +146,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match actions by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -181,15 +185,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match builds by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -231,15 +237,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match deployments by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -333,15 +341,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match deployments by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -391,15 +401,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match stacks by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -427,15 +439,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match stacks by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -484,15 +498,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match stacks by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -701,15 +717,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match stacks by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -1001,15 +1019,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match repos by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -1037,15 +1057,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match repos by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> @@ -1073,15 +1095,17 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { "# Match repos by name, id, wildcard, or \\regex\\.\n" } onUpdate={(pattern) => - setParams((params) => ({ ...params, pattern })) + setParams({ ...params, pattern }) } disabled={disabled} useMonaco monacoLanguage="string_list" /> setParams((params) => ({ ...params, tags }))} + onChange={(tags) => setParams({ ...params, tags })} + disabled={disabled} useName /> diff --git a/ui/src/resources/sync/config.tsx b/ui/src/resources/sync/config.tsx index 929b9787e3..9d60b929a6 100644 --- a/ui/src/resources/sync/config.tsx +++ b/ui/src/resources/sync/config.tsx @@ -2,8 +2,6 @@ import { AccountSelectorConfig } from "@/components/config/account-selector"; import LinkedRepo from "@/components/config/linked-repo"; import { ProviderSelectorConfig } from "@/components/config/provider-selector"; import { MonacoEditor } from "mogh_ui"; -import Tags from "@/components/tags"; -import TagSelector from "@/components/tags/selector"; import WebhookBuilder from "@/components/webhook/builder"; import CopyWebhookUrl from "@/components/webhook/copy-url"; import { @@ -22,12 +20,12 @@ import { ConfigSwitch, } from "mogh_ui"; import { ShowHideButton } from "mogh_ui"; -import { Group, Select, Text } from "@mantine/core"; +import { Select, Text } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; -import { CircleMinus } from "lucide-react"; import { ReactNode } from "react"; import { useFullResourceSync } from "."; +import TagMultiSelector from "@/components/tags/multi-selector"; type SyncMode = "UI Defined" | "Files On Server" | "Git Repo" | undefined; const SYNC_MODES = ["UI Defined", "Files On Server", "Git Repo"] as const; @@ -189,36 +187,14 @@ export default function ResourceSyncConfig({ description: "Only sync resources matching all of these tags.", fields: { match_tags: (values, set) => { - const tags = useRead("ListTags", {}).data; - const otherTags = tags?.filter((tag) => !values?.includes(tag.name)); return ( - - set({ match_tags: [...(values ?? []), tag] })} - disabled={disabled || !includeResources} - position="bottom-start" - useName - canCreate - /> - - values?.includes(tag.name)) - .map((tag) => tag.name) ?? [] - } - onBadgeClick={(toRemove) => - set({ - match_tags: values?.filter((tagName) => tagName !== toRemove), - }) - } - icon={} - fz="1rem" - useName - /> - + set({ match_tags })} + disabled={disabled || !includeResources} + useName + canCreate + /> ); }, }, From 78ab4f1b50a991eafc1a6b2c5d1554acbd7ca928 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 01:15:34 -0700 Subject: [PATCH 134/203] deploy 2.3.0-dev-34 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fc760b617..19482801f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5f79fe120e..bb4f942eda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-33" +version = "2.3.0-dev-34" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 50fc99cc9e0d74139aebfc9040eaff73a4ed42b2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 01:22:41 -0700 Subject: [PATCH 135/203] fix containers page networks / volumes wrapping --- ui/src/pages/containers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index c7214bf019..055ae3f81b 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -138,7 +138,7 @@ export default function Containers() { ), cell: ({ row }) => (row.original.networks?.length ?? 0) > 0 ? ( - + {row.original.networks?.map((network) => ( ), cell: ({ row }) => ( - + {row.original.volumes?.map((volume) => ( Date: Sat, 11 Jul 2026 12:19:53 -0700 Subject: [PATCH 136/203] use enum cmp for sorting by state --- bin/cli/src/command/list.rs | 11 ++++------- bin/core/src/api/read/action.rs | 2 +- bin/core/src/api/read/build.rs | 2 +- bin/core/src/api/read/deployment.rs | 2 +- bin/core/src/api/read/procedure.rs | 2 +- bin/core/src/api/read/repo.rs | 2 +- bin/core/src/api/read/server.rs | 2 +- bin/core/src/api/read/stack.rs | 2 +- bin/core/src/api/read/swarm.rs | 2 +- bin/core/src/api/read/sync.rs | 2 +- client/core/rs/src/entities/action.rs | 10 +++++----- client/core/rs/src/entities/repo.rs | 16 +++++++++------- 12 files changed, 27 insertions(+), 28 deletions(-) diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index 78eb999c74..0444527055 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -631,10 +631,7 @@ impl ListResources for BuildListItem { }) .collect::>(); builds.sort_by(|a, b| { - a.name - .cmp(&b.name) - .then(a.info.builder_id.cmp(&b.info.builder_id)) - .then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(builds) } @@ -740,7 +737,7 @@ impl ListResources for ProcedureListItem { (Some(a), Some(b)) => return a.cmp(&b), (None, None) => {} } - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(procedures) } @@ -793,7 +790,7 @@ impl ListResources for ActionListItem { (Some(a), Some(b)) => return a.cmp(&b), (None, None) => {} } - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(actions) } @@ -843,7 +840,7 @@ impl ListResources for ResourceSyncListItem { }) .collect::>(); syncs.sort_by(|a, b| { - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(syncs) } diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index 9ff76bb13d..a22955fb66 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -53,7 +53,7 @@ impl Resolve for ListActions { ActionSortBy::Name => resource::ListItemSort::Name, ActionSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } ActionSortBy::NextRun => { diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 247da8387f..04d83eb9fd 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -67,7 +67,7 @@ impl Resolve for ListBuilds { } BuildSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 5c3176bd84..1ece7e4bdf 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -92,7 +92,7 @@ impl Resolve for ListDeployments { } DeploymentSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index b261bdc6d5..90cfee5493 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -52,7 +52,7 @@ impl Resolve for ListProcedures { ProcedureSortBy::Name => resource::ListItemSort::Name, ProcedureSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } ProcedureSortBy::NextRun => { diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index b1d41ab689..5ea0585c11 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -58,7 +58,7 @@ impl Resolve for ListRepos { } RepoSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 21ccc29a6f..651a9f2cd3 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -125,7 +125,7 @@ impl Resolve for ListServers { } ServerSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 4a59c03c70..5530a020b8 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -511,7 +511,7 @@ impl Resolve for ListStacks { } StackSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index 39bb96298b..a9e44141a3 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -51,7 +51,7 @@ impl Resolve for ListSwarms { SwarmSortBy::Name => resource::ListItemSort::Name, SwarmSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index ae02a5c2b3..73a6fd58a2 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -60,7 +60,7 @@ impl Resolve for ListResourceSyncs { } ResourceSyncSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.to_string().cmp(&b.info.state.to_string()) + a.info.state.cmp(&b.info.state) })) } }; diff --git a/client/core/rs/src/entities/action.rs b/client/core/rs/src/entities/action.rs index 548af0a9bc..51eac1e0c7 100644 --- a/client/core/rs/src/entities/action.rs +++ b/client/core/rs/src/entities/action.rs @@ -53,15 +53,15 @@ pub struct ActionListItemInfo { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum ActionState { - /// Unknown case - #[default] - Unknown, + /// Currently running + Running, /// Last clone / pull successful (or never cloned) Ok, /// Last clone / pull failed Failed, - /// Currently running - Running, + /// Unknown case + #[default] + Unknown, } #[cfg(feature = "utoipa")] diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs index 1a01bdcd1c..01f3752311 100644 --- a/client/core/rs/src/entities/repo.rs +++ b/client/core/rs/src/entities/repo.rs @@ -66,25 +66,27 @@ pub struct RepoListItemInfo { Default, PartialEq, Eq, + PartialOrd, + Ord, Serialize, Deserialize, Display, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum RepoState { - /// Unknown case - #[default] - Unknown, - /// Last clone / pull successful (or never cloned) - Ok, - /// Last clone / pull failed - Failed, /// Currently cloning Cloning, /// Currently pulling Pulling, /// Currently building Building, + /// Last clone / pull successful (or never cloned) + Ok, + /// Last clone / pull failed + Failed, + /// Unknown case + #[default] + Unknown, } #[cfg(feature = "utoipa")] From 9ae703ca694993d15a97bf1b987e24419d287c38 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 12:42:56 -0700 Subject: [PATCH 137/203] container page uses api based sort --- bin/core/src/api/read/docker.rs | 88 ++++++++++++------- client/core/rs/src/api/read/docker.rs | 12 ++- .../core/rs/src/entities/docker/container.rs | 27 +++++- client/core/ts/src/types.ts | 48 +++++++--- ui/public/client/types.d.ts | 49 ++++++++--- ui/public/client/types.js | 41 ++++++--- ui/src/pages/containers.tsx | 36 +++++++- 7 files changed, 229 insertions(+), 72 deletions(-) diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index f2efb31d46..5d76a4c5f9 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -9,7 +9,8 @@ use komodo_client::{ deployment::Deployment, docker::{ container::{ - Container, ContainerListItem, ContainerStateStatusEnum, + Container, ContainerListItem, ContainerSortBy, + ContainerStateStatusEnum, }, image::{Image, ImageHistoryResponseItem}, network::Network, @@ -99,11 +100,7 @@ impl Resolve for ListAllContainers { .await?; let mut containers = Vec::::new(); - let mut skipped = 0; let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - let limit_usize = limit as usize; - // Eg. page 1 skips until after 100 containers, page 2 after 200. - let skip = limit.saturating_mul(self.page); // Match terms case insensitively. let terms = self .terms @@ -118,34 +115,65 @@ impl Resolve for ListAllContainers { let Some(docker) = &cache.docker else { continue; }; - let more = docker - .containers - .iter() - .filter(|container| { - // Apply state filter if defined. - (self.state.is_empty() || self.state.contains(&container.state)) && - // Apply terms filter if defined - (terms.is_empty() - // Match when all terms contained within a name. - || { - let name = container.name.to_lowercase(); - terms.iter().all(|term| name.contains(term)) - }) - }); - for container in more { - if skipped < skip { - skipped += 1; - } else { - // push and maybe early return - containers.push(container.clone()); - if limit > 0 && containers.len() >= limit_usize { - return Ok(containers); - } + containers.extend( + docker + .containers + .iter() + .filter(|container| { + // Apply state filter if defined. + (self.state.is_empty() || self.state.contains(&container.state)) && + // Apply terms filter if defined + (terms.is_empty() + // Match when all terms contained within a name. + || { + let name = container.name.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }) + }) + .cloned(), + ); + } + + // The containers all come from the in memory status cache, + // so all matching containers are collected and sorted + // before applying pagination. + let compare = + |a: &ContainerListItem, b: &ContainerListItem| match self + .sort_by + { + ContainerSortBy::Name => a.name.cmp(&b.name), + ContainerSortBy::Server => a.server_name.cmp(&b.server_name), + ContainerSortBy::State => a.state.cmp(&b.state), + ContainerSortBy::Image => a.image.cmp(&b.image), + ContainerSortBy::Networks => { + a.networks.first().cmp(&b.networks.first()) } - } + ContainerSortBy::Ports => a + .ports + .first() + .map(|port| port.private_port) + .cmp(&b.ports.first().map(|port| port.private_port)), + ContainerSortBy::Volumes => { + a.volumes.first().cmp(&b.volumes.first()) + } + }; + if self.sort_desc { + containers.sort_by(|a, b| { + compare(b, a).then_with(|| a.name.cmp(&b.name)) + }); + } else { + containers.sort_by(|a, b| { + compare(a, b).then_with(|| a.name.cmp(&b.name)) + }); } - Ok(containers) + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(containers.into_iter().skip(skip).take(take).collect()) } } diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index b97663987a..f70148e625 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -6,7 +6,8 @@ use crate::entities::{ ResourceTarget, SearchCombinator, U64, docker::{ container::{ - Container, ContainerListItem, ContainerStateStatusEnum, + Container, ContainerListItem, ContainerSortBy, + ContainerStateStatusEnum, }, image::{Image, ImageHistoryResponseItem, ImageListItem}, network::{Network, NetworkListItem}, @@ -116,6 +117,15 @@ pub struct ListAllContainers { /// Note: the page logic relies on this being consistent /// across queries for more pages. pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: ContainerSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/entities/docker/container.rs b/client/core/rs/src/entities/docker/container.rs index 719e921129..81231e4efe 100644 --- a/client/core/rs/src/entities/docker/container.rs +++ b/client/core/rs/src/entities/docker/container.rs @@ -274,6 +274,29 @@ pub struct ContainerState { pub health: Option, } +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ContainerSortBy { + /// Sort by container name. Default. + #[default] + Name, + /// Sort by host Server name. + Server, + /// Sort by container state. + State, + /// Sort by image. + Image, + /// Sort by first network. + Networks, + /// Sort by first port. + Ports, + /// Sort by first volume. + Volumes, +} + #[typeshare] #[derive( Debug, @@ -294,11 +317,11 @@ pub struct ContainerState { pub enum ContainerStateStatusEnum { Running, Created, - Paused, Restarting, - Exited, Stopping, Removing, + Paused, + Exited, Dead, #[default] #[serde(rename = "")] diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index efbb468fc8..1960a89f9c 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -156,14 +156,14 @@ export interface ResourceListItem { } export enum ActionState { - /** Unknown case */ - Unknown = "Unknown", + /** Currently running */ + Running = "Running", /** Last clone / pull successful (or never cloned) */ Ok = "Ok", /** Last clone / pull failed */ Failed = "Failed", - /** Currently running */ - Running = "Running", + /** Unknown case */ + Unknown = "Unknown", } export interface ActionListItemInfo { @@ -3037,11 +3037,11 @@ export type GetVariableResponse = Variable; export enum ContainerStateStatusEnum { Running = "running", Created = "created", - Paused = "paused", Restarting = "restarting", - Exited = "exited", Stopping = "stopping", Removing = "removing", + Paused = "paused", + Exited = "exited", Dead = "dead", Empty = "", } @@ -5324,18 +5324,18 @@ export type ProcedureListItem = ResourceListItem; export type ListProceduresResponse = ProcedureListItem[]; export enum RepoState { - /** Unknown case */ - Unknown = "Unknown", - /** Last clone / pull successful (or never cloned) */ - Ok = "Ok", - /** Last clone / pull failed */ - Failed = "Failed", /** Currently cloning */ Cloning = "Cloning", /** Currently pulling */ Pulling = "Pulling", /** Currently building */ Building = "Building", + /** Last clone / pull successful (or never cloned) */ + Ok = "Ok", + /** Last clone / pull failed */ + Failed = "Failed", + /** Unknown case */ + Unknown = "Unknown", } export interface RepoListItemInfo { @@ -8932,6 +8932,23 @@ export interface ListAlertsResponse { next_page?: I64; } +export enum ContainerSortBy { + /** Sort by container name. Default. */ + Name = "Name", + /** Sort by host Server name. */ + Server = "Server", + /** Sort by container state. */ + State = "State", + /** Sort by image. */ + Image = "Image", + /** Sort by first network. */ + Networks = "Networks", + /** Sort by first port. */ + Ports = "Ports", + /** Sort by first volume. */ + Volumes = "Volumes", +} + /** * List all containers on the target servers. * Response: [ListAllContainersResponse]. @@ -8965,6 +8982,13 @@ export interface ListAllContainers { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ContainerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index e7e2337dba..e50629f9d0 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -140,14 +140,14 @@ export interface ResourceListItem { info: Info; } export declare enum ActionState { - /** Unknown case */ - Unknown = "Unknown", + /** Currently running */ + Running = "Running", /** Last clone / pull successful (or never cloned) */ Ok = "Ok", /** Last clone / pull failed */ Failed = "Failed", - /** Currently running */ - Running = "Running" + /** Unknown case */ + Unknown = "Unknown" } export interface ActionListItemInfo { /** Whether last action run successful */ @@ -3158,11 +3158,11 @@ export type GetVariableResponse = Variable; export declare enum ContainerStateStatusEnum { Running = "running", Created = "created", - Paused = "paused", Restarting = "restarting", - Exited = "exited", Stopping = "stopping", Removing = "removing", + Paused = "paused", + Exited = "exited", Dead = "dead", Empty = "" } @@ -5222,18 +5222,18 @@ export interface ProcedureListItemInfo { export type ProcedureListItem = ResourceListItem; export type ListProceduresResponse = ProcedureListItem[]; export declare enum RepoState { - /** Unknown case */ - Unknown = "Unknown", - /** Last clone / pull successful (or never cloned) */ - Ok = "Ok", - /** Last clone / pull failed */ - Failed = "Failed", /** Currently cloning */ Cloning = "Cloning", /** Currently pulling */ Pulling = "Pulling", /** Currently building */ - Building = "Building" + Building = "Building", + /** Last clone / pull successful (or never cloned) */ + Ok = "Ok", + /** Last clone / pull failed */ + Failed = "Failed", + /** Unknown case */ + Unknown = "Unknown" } export interface RepoListItemInfo { /** The server that repo sits on. */ @@ -8520,6 +8520,22 @@ export interface ListAlertsResponse { */ next_page?: I64; } +export declare enum ContainerSortBy { + /** Sort by container name. Default. */ + Name = "Name", + /** Sort by host Server name. */ + Server = "Server", + /** Sort by container state. */ + State = "State", + /** Sort by image. */ + Image = "Image", + /** Sort by first network. */ + Networks = "Networks", + /** Sort by first port. */ + Ports = "Ports", + /** Sort by first volume. */ + Volumes = "Volumes" +} /** * List all containers on the target servers. * Response: [ListAllContainersResponse]. @@ -8553,6 +8569,13 @@ export interface ListAllContainers { * across queries for more pages. */ limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ContainerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List all stack services part of the target stacks. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index 21e06f1780..9fe1f537e8 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -27,14 +27,14 @@ export var FileFormat; })(FileFormat || (FileFormat = {})); export var ActionState; (function (ActionState) { - /** Unknown case */ - ActionState["Unknown"] = "Unknown"; + /** Currently running */ + ActionState["Running"] = "Running"; /** Last clone / pull successful (or never cloned) */ ActionState["Ok"] = "Ok"; /** Last clone / pull failed */ ActionState["Failed"] = "Failed"; - /** Currently running */ - ActionState["Running"] = "Running"; + /** Unknown case */ + ActionState["Unknown"] = "Unknown"; })(ActionState || (ActionState = {})); export var TagQueryBehavior; (function (TagQueryBehavior) { @@ -419,11 +419,11 @@ export var ContainerStateStatusEnum; (function (ContainerStateStatusEnum) { ContainerStateStatusEnum["Running"] = "running"; ContainerStateStatusEnum["Created"] = "created"; - ContainerStateStatusEnum["Paused"] = "paused"; ContainerStateStatusEnum["Restarting"] = "restarting"; - ContainerStateStatusEnum["Exited"] = "exited"; ContainerStateStatusEnum["Stopping"] = "stopping"; ContainerStateStatusEnum["Removing"] = "removing"; + ContainerStateStatusEnum["Paused"] = "paused"; + ContainerStateStatusEnum["Exited"] = "exited"; ContainerStateStatusEnum["Dead"] = "dead"; ContainerStateStatusEnum["Empty"] = ""; })(ContainerStateStatusEnum || (ContainerStateStatusEnum = {})); @@ -737,18 +737,18 @@ export var ProcedureState; })(ProcedureState || (ProcedureState = {})); export var RepoState; (function (RepoState) { - /** Unknown case */ - RepoState["Unknown"] = "Unknown"; - /** Last clone / pull successful (or never cloned) */ - RepoState["Ok"] = "Ok"; - /** Last clone / pull failed */ - RepoState["Failed"] = "Failed"; /** Currently cloning */ RepoState["Cloning"] = "Cloning"; /** Currently pulling */ RepoState["Pulling"] = "Pulling"; /** Currently building */ RepoState["Building"] = "Building"; + /** Last clone / pull successful (or never cloned) */ + RepoState["Ok"] = "Ok"; + /** Last clone / pull failed */ + RepoState["Failed"] = "Failed"; + /** Unknown case */ + RepoState["Unknown"] = "Unknown"; })(RepoState || (RepoState = {})); export var ResourceSyncState; (function (ResourceSyncState) { @@ -837,6 +837,23 @@ export var AlerterSortBy; /** Sort by enabled. */ AlerterSortBy["Enabled"] = "Enabled"; })(AlerterSortBy || (AlerterSortBy = {})); +export var ContainerSortBy; +(function (ContainerSortBy) { + /** Sort by container name. Default. */ + ContainerSortBy["Name"] = "Name"; + /** Sort by host Server name. */ + ContainerSortBy["Server"] = "Server"; + /** Sort by container state. */ + ContainerSortBy["State"] = "State"; + /** Sort by image. */ + ContainerSortBy["Image"] = "Image"; + /** Sort by first network. */ + ContainerSortBy["Networks"] = "Networks"; + /** Sort by first port. */ + ContainerSortBy["Ports"] = "Ports"; + /** Sort by first volume. */ + ContainerSortBy["Volumes"] = "Volumes"; +})(ContainerSortBy || (ContainerSortBy = {})); export var BuilderSortBy; (function (BuilderSortBy) { /** Sort by name. Default. */ diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 055ae3f81b..74fbf14fbd 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -2,6 +2,7 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; import { useRead, useTagsFilter } from "@/lib/hooks"; +import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; @@ -17,6 +18,8 @@ import ListPagination, { RESOURCE_PAGE_SIZE, } from "@/components/list-pagination"; +const CONTAINER_SORT_KEYS = Object.values(Types.ContainerSortBy); + export default function Containers() { const [selectedServers, setSelectedServers] = useState([]); @@ -35,11 +38,17 @@ export default function Containers() { const tags = useTagsFilter(); - // Set to page 0 whenever any filter changes, + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.ContainerSortBy; + sort_desc?: boolean; + }>({}); + + // Set to page 0 whenever any filter or the sort changes, // otherwise the query can point past the last page and come back empty. useEffect(() => { setPage(0); - }, [selectedServers, tags]); + }, [selectedServers, tags, sort.sort_by, sort.sort_desc]); const _containers = useRead( @@ -50,6 +59,8 @@ export default function Containers() { tags, page, limit: RESOURCE_PAGE_SIZE, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, }, { refetchInterval: 15_000 }, ).data ?? []; @@ -62,8 +73,23 @@ export default function Containers() { { + const sort = sorting.find((s) => + CONTAINER_SORT_KEYS.includes(s.id as Types.ContainerSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.ContainerSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} columns={[ { + id: "Name", accessorKey: "name", size: 260, header: ({ column }) => ( @@ -78,6 +104,7 @@ export default function Containers() { ), }, { + id: "Server", accessorKey: "server_id", size: 200, sortingFn: (a, b) => { @@ -100,6 +127,7 @@ export default function Containers() { ), }, { + id: "State", accessorKey: "state", size: 160, header: ({ column }) => ( @@ -116,6 +144,7 @@ export default function Containers() { }, }, { + id: "Image", accessorKey: "image", size: 300, header: ({ column }) => ( @@ -131,6 +160,7 @@ export default function Containers() { ), }, { + id: "Networks", accessorKey: "networks.0", size: 200, header: ({ column }) => ( @@ -159,6 +189,7 @@ export default function Containers() { ), }, { + id: "Ports", accessorKey: "ports.0", size: 200, sortingFn: (a, b) => { @@ -189,6 +220,7 @@ export default function Containers() { ), }, { + id: "Volumes", accessorKey: "volumes.0", size: 200, header: ({ column }) => ( From 9a8cc083154c2cf54518f80c413e69b272b93e33 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 12:51:14 -0700 Subject: [PATCH 138/203] move list stack methods --- bin/core/src/api/read/stack.rs | 214 +++++++++++++-------------- client/core/rs/src/api/read/stack.rs | 196 ++++++++++++------------ 2 files changed, 205 insertions(+), 205 deletions(-) diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 5530a020b8..16a2bf0024 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -53,6 +53,113 @@ impl Resolve for GetStack { } } +impl Resolve for ListStacks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result> { + let all_tags = if self.query.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let only_update_available = self.query.specific.update_available; + let states = self.query.specific.states.clone(); + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let sort_by: resource::ListItemSort = + match self.sort_by { + StackSortBy::Name => resource::ListItemSort::Name, + StackSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.repo.cmp(&b.info.repo) + })) + } + StackSortBy::Host => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + let host_a = if a.info.swarm_id.is_empty() { + &a.info.server_name + } else { + &a.info.swarm_name + }; + let host_b = if b.info.swarm_id.is_empty() { + &b.info.server_name + } else { + &b.info.swarm_name + }; + host_a.cmp(host_b) + })) + } + StackSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info.state.cmp(&b.info.state) + })) + } + }; + let stacks = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |stack| { + (!only_update_available + || stack + .info + .services + .iter() + .any(|service| service.update_available)) + && (states.is_empty() || states.contains(&stack.info.state)) + }, + ) + .await?; + Ok(stacks) + } +} + +impl Resolve for ListFullStacks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let all_tags = if self.query.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let states = self.query.specific.states.clone(); + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + Ok( + resource::list_full_for_user_filtered::( + self.query, + limit, + self.page, + user, + PermissionLevel::Read.into(), + &all_tags, + |stack| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_cached_stack_state(&stack.id).await) + { + Some(stack) + } else { + None + } + } + }, + ) + .await?, + ) + } +} + impl Resolve for ListStackServices { async fn resolve( self, @@ -473,113 +580,6 @@ impl Resolve for ListCommonStackBuildExtraArgs { } } -impl Resolve for ListStacks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result> { - let all_tags = if self.query.tags.is_empty() { - vec![] - } else { - get_all_tags(None).await? - }; - let only_update_available = self.query.specific.update_available; - let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - let sort_by: resource::ListItemSort = - match self.sort_by { - StackSortBy::Name => resource::ListItemSort::Name, - StackSortBy::Source => { - resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.repo.cmp(&b.info.repo) - })) - } - StackSortBy::Host => { - resource::ListItemSort::InMemory(Box::new(|a, b| { - let host_a = if a.info.swarm_id.is_empty() { - &a.info.server_name - } else { - &a.info.swarm_name - }; - let host_b = if b.info.swarm_id.is_empty() { - &b.info.server_name - } else { - &b.info.swarm_name - }; - host_a.cmp(host_b) - })) - } - StackSortBy::State => { - resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) - })) - } - }; - let stacks = resource::list_items_for_user::( - self.query, - resource::ListItemsQueryOptions { - limit, - page: self.page, - sort_desc: self.sort_desc, - sort_by, - }, - user, - PermissionLevel::Read.into(), - &all_tags, - |stack| { - (!only_update_available - || stack - .info - .services - .iter() - .any(|service| service.update_available)) - && (states.is_empty() || states.contains(&stack.info.state)) - }, - ) - .await?; - Ok(stacks) - } -} - -impl Resolve for ListFullStacks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let all_tags = if self.query.tags.is_empty() { - vec![] - } else { - get_all_tags(None).await? - }; - let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); - Ok( - resource::list_full_for_user_filtered::( - self.query, - limit, - self.page, - user, - PermissionLevel::Read.into(), - &all_tags, - |stack| { - let states = states.clone(); - async move { - if states.is_empty() - || states - .contains(&get_cached_stack_state(&stack.id).await) - { - Some(stack) - } else { - None - } - } - }, - ) - .await?, - ) - } -} - impl Resolve for GetStackActionState { async fn resolve( self, diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index a1062a48b0..00e6bfcf8c 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -48,6 +48,104 @@ pub type GetStackResponse = Stack; // +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListStacks", + description = "List stacks matching optional query.", + request_body(content = ListStacks), + responses( + (status = 200, description = "The list of stacks", body = ListStacksResponse), + ), +)] +pub fn list_stacks() {} + +/// List stacks matching optional query. Response: [ListStacksResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListStacksResponse)] +#[error(mogh_error::Error)] +pub struct ListStacks { + /// optional structured query to filter stacks. + #[serde(default)] + pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: StackSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, +} + +#[typeshare] +pub type ListStacksResponse = Vec; + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListFullStacks", + description = "List stacks matching optional query.", + request_body(content = ListFullStacks), + responses( + (status = 200, description = "The list of stacks", body = ListFullStacksResponse), + ), +)] +pub fn list_full_stacks() {} + +/// List stacks matching optional query. Response: [ListFullStacksResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListFullStacksResponse)] +#[error(mogh_error::Error)] +pub struct ListFullStacks { + /// optional structured query to filter stacks. + #[serde(default)] + pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, +} + +#[typeshare] +pub type ListFullStacksResponse = Vec; + +// + #[cfg(feature = "utoipa")] #[utoipa::path( post, @@ -392,104 +490,6 @@ pub type ListCommonStackBuildExtraArgsResponse = Vec; // -#[cfg(feature = "utoipa")] -#[utoipa::path( - post, - path = "/ListStacks", - description = "List stacks matching optional query.", - request_body(content = ListStacks), - responses( - (status = 200, description = "The list of stacks", body = ListStacksResponse), - ), -)] -pub fn list_stacks() {} - -/// List stacks matching optional query. Response: [ListStacksResponse]. -#[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[empty_traits(KomodoReadRequest)] -#[response(ListStacksResponse)] -#[error(mogh_error::Error)] -pub struct ListStacks { - /// optional structured query to filter stacks. - #[serde(default)] - pub query: StackQuery, - - /// Retrieve more results by incrementing the page. - /// `page: 0` is default. - #[serde(default)] - pub page: U64, - - /// Set the limit for number of resources per-page. - /// `limit: 100` is default. - /// - /// Passing `limit: 0` returns all results (unlimited). - /// - /// Note: the page logic relies on this being consistent - /// across queries for more pages. - pub limit: Option, - - /// Sort the results by this field. - /// Defaults to Name. Non-Name sorts are applied in memory - /// after querying all matching resources. - #[serde(default)] - pub sort_by: StackSortBy, - - /// Reverse the sort direction. - #[serde(default)] - pub sort_desc: bool, -} - -#[typeshare] -pub type ListStacksResponse = Vec; - -// - -#[cfg(feature = "utoipa")] -#[utoipa::path( - post, - path = "/ListFullStacks", - description = "List stacks matching optional query.", - request_body(content = ListFullStacks), - responses( - (status = 200, description = "The list of stacks", body = ListFullStacksResponse), - ), -)] -pub fn list_full_stacks() {} - -/// List stacks matching optional query. Response: [ListFullStacksResponse]. -#[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[empty_traits(KomodoReadRequest)] -#[response(ListFullStacksResponse)] -#[error(mogh_error::Error)] -pub struct ListFullStacks { - /// optional structured query to filter stacks. - #[serde(default)] - pub query: StackQuery, - - /// Retrieve more results by incrementing the page. - /// `page: 0` is default. - #[serde(default)] - pub page: U64, - - /// Set the limit for number of resources per-page. - /// `limit: 100` is default. - /// - /// Passing `limit: 0` returns all results (unlimited). - /// - /// Note: the page logic relies on this being consistent - /// across queries for more pages. - pub limit: Option, -} - -#[typeshare] -pub type ListFullStacksResponse = Vec; - -// - #[cfg(feature = "utoipa")] #[utoipa::path( post, From 784d9b1a6a364ce05d280e6afb62b60c01b0c262 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 12:56:39 -0700 Subject: [PATCH 139/203] deploy 2.3.0-dev-35 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- bin/cli/src/command/container.rs | 8 +++++++- bin/cli/src/command/terminal.rs | 2 ++ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19482801f7..7b31599d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index bb4f942eda..7d8ce6cc7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-34" +version = "2.3.0-dev-35" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index 36fcaf500f..a07063491b 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -14,7 +14,9 @@ use komodo_client::{ }, docker::{ self, - container::{ContainerListItem, ContainerStateStatusEnum}, + container::{ + ContainerListItem, ContainerSortBy, ContainerStateStatusEnum, + }, }, }, }; @@ -67,6 +69,8 @@ async fn list_containers( limit: Some(100), // Page is more naturally given starting as 1, 2, 3. page: if *page == 0 { 0 } else { *page - 1 }, + sort_by: ContainerSortBy::Server, + sort_desc: false, }), )?; @@ -154,6 +158,8 @@ pub async fn inspect_container( state: Default::default(), limit: Some(300), page: 0, + sort_by: ContainerSortBy::Server, + sort_desc: false, }), )?; diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index e922296541..b7c6c4e59d 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -134,6 +134,8 @@ async fn get_server( state: Default::default(), limit: Some(0), page: 0, + sort_by: Default::default(), + sort_desc: false, }) .await?; From 36a81e1dbd810faaac5d6354df451a66ac60acc2 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 13:20:23 -0700 Subject: [PATCH 140/203] stack / deployment state sorting prefers update available shown first --- bin/core/src/api/read/deployment.rs | 5 ++++- bin/core/src/api/read/stack.rs | 5 ++++- client/core/rs/src/entities/stack.rs | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 1ece7e4bdf..89597026a3 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -92,7 +92,10 @@ impl Resolve for ListDeployments { } DeploymentSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + // Use ! with update available to order 'true' first + (!a.info.update_available) + .cmp(&!b.info.update_available) + .then_with(|| a.info.state.cmp(&b.info.state)) })) } }; diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 16a2bf0024..0c9fbc9cc2 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -91,7 +91,10 @@ impl Resolve for ListStacks { } StackSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + // Use ! with update available to order 'true' first + (!a.info.update_available()) + .cmp(&!b.info.update_available()) + .then_with(|| a.info.state.cmp(&b.info.state)) })) } }; diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 6248442110..f3714f458f 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -194,6 +194,12 @@ pub struct StackListItemInfo { pub latest_hash: Option, } +impl StackListItemInfo { + pub fn update_available(&self) -> bool { + self.services.iter().any(|s| s.update_available) + } +} + #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] From 3784828eb0d3b8f378cb94166e2d567fba99ed5e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 13:30:32 -0700 Subject: [PATCH 141/203] ensure name is always fallback sort --- bin/core/src/api/read/action.rs | 10 ++++++++-- bin/core/src/api/read/build.rs | 10 ++++++++-- bin/core/src/api/read/builder.rs | 5 ++++- bin/core/src/api/read/deployment.rs | 20 ++++++++++++++------ bin/core/src/api/read/docker.rs | 21 ++++++++++----------- bin/core/src/api/read/procedure.rs | 10 ++++++++-- bin/core/src/api/read/repo.rs | 5 ++++- bin/core/src/api/read/server.rs | 10 ++++++++-- bin/core/src/api/read/stack.rs | 20 ++++++++++++++------ bin/core/src/api/read/swarm.rs | 5 ++++- bin/core/src/api/read/sync.rs | 15 ++++++++++++--- 11 files changed, 94 insertions(+), 37 deletions(-) diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index a22955fb66..d4823b554c 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -53,12 +53,18 @@ impl Resolve for ListActions { ActionSortBy::Name => resource::ListItemSort::Name, ActionSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } ActionSortBy::NextRun => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.next_scheduled_run.cmp(&b.info.next_scheduled_run) + a.info + .next_scheduled_run + .cmp(&b.info.next_scheduled_run) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 04d83eb9fd..47a42d3048 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -62,12 +62,18 @@ impl Resolve for ListBuilds { BuildSortBy::Name => resource::ListItemSort::Name, BuildSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.repo.cmp(&b.info.repo) + a.info + .repo + .cmp(&b.info.repo) + .then_with(|| a.name.cmp(&b.name)) })) } BuildSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 06d1d53e10..2b21526e86 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -54,7 +54,10 @@ impl Resolve for ListBuilders { } BuilderSortBy::InstanceType => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.instance_type.cmp(&b.info.instance_type) + a.info + .instance_type + .cmp(&b.info.instance_type) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 89597026a3..280b14ce54 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -72,7 +72,10 @@ impl Resolve for ListDeployments { DeploymentSortBy::Name => resource::ListItemSort::Name, DeploymentSortBy::Image => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.image.cmp(&b.info.image) + a.info + .image + .cmp(&b.info.image) + .then_with(|| a.name.cmp(&b.name)) })) } DeploymentSortBy::Host => { @@ -87,15 +90,20 @@ impl Resolve for ListDeployments { } else { &b.info.swarm_name }; - host_a.cmp(host_b) + host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name)) })) } DeploymentSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - // Use ! with update available to order 'true' first - (!a.info.update_available) - .cmp(&!b.info.update_available) - .then_with(|| a.info.state.cmp(&b.info.state)) + a.info + .state + .cmp(&b.info.state) + .then_with(|| { + // Use ! with update available to order 'true' first + (!a.info.update_available) + .cmp(&!b.info.update_available) + }) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 5d76a4c5f9..233410756e 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -137,10 +137,8 @@ impl Resolve for ListAllContainers { // The containers all come from the in memory status cache, // so all matching containers are collected and sorted // before applying pagination. - let compare = - |a: &ContainerListItem, b: &ContainerListItem| match self - .sort_by - { + let compare = |a: &ContainerListItem, b: &ContainerListItem| { + match self.sort_by { ContainerSortBy::Name => a.name.cmp(&b.name), ContainerSortBy::Server => a.server_name.cmp(&b.server_name), ContainerSortBy::State => a.state.cmp(&b.state), @@ -156,15 +154,16 @@ impl Resolve for ListAllContainers { ContainerSortBy::Volumes => { a.volumes.first().cmp(&b.volumes.first()) } - }; + } + // Fall back to name based sorting for equal sort keys. + // Inside `compare`, so descending sorts are fully descending, + // matching the List apis. + .then_with(|| a.name.cmp(&b.name)) + }; if self.sort_desc { - containers.sort_by(|a, b| { - compare(b, a).then_with(|| a.name.cmp(&b.name)) - }); + containers.sort_by(|a, b| compare(b, a)); } else { - containers.sort_by(|a, b| { - compare(a, b).then_with(|| a.name.cmp(&b.name)) - }); + containers.sort_by(|a, b| compare(a, b)); } let skip = limit.saturating_mul(self.page) as usize; diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 90cfee5493..145149b456 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -52,12 +52,18 @@ impl Resolve for ListProcedures { ProcedureSortBy::Name => resource::ListItemSort::Name, ProcedureSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } ProcedureSortBy::NextRun => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.next_scheduled_run.cmp(&b.info.next_scheduled_run) + a.info + .next_scheduled_run + .cmp(&b.info.next_scheduled_run) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 5ea0585c11..cf4e603692 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -58,7 +58,10 @@ impl Resolve for ListRepos { } RepoSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 651a9f2cd3..83eadb8dab 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -120,12 +120,18 @@ impl Resolve for ListServers { } ServerSortBy::Version => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.version.cmp(&b.info.version) + a.info + .version + .cmp(&b.info.version) + .then_with(|| a.name.cmp(&b.name)) })) } ServerSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 0c9fbc9cc2..0f1ae395ac 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -71,7 +71,10 @@ impl Resolve for ListStacks { StackSortBy::Name => resource::ListItemSort::Name, StackSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.repo.cmp(&b.info.repo) + a.info + .repo + .cmp(&b.info.repo) + .then_with(|| a.name.cmp(&b.name)) })) } StackSortBy::Host => { @@ -86,15 +89,20 @@ impl Resolve for ListStacks { } else { &b.info.swarm_name }; - host_a.cmp(host_b) + host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name)) })) } StackSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - // Use ! with update available to order 'true' first - (!a.info.update_available()) - .cmp(&!b.info.update_available()) - .then_with(|| a.info.state.cmp(&b.info.state)) + a.info + .state + .cmp(&b.info.state) + .then_with(|| { + // Use ! with update available to order 'true' first + (!a.info.update_available()) + .cmp(&!b.info.update_available()) + }) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index a9e44141a3..b269deb89f 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -51,7 +51,10 @@ impl Resolve for ListSwarms { SwarmSortBy::Name => resource::ListItemSort::Name, SwarmSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } }; diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 73a6fd58a2..a63ab485ae 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -50,17 +50,26 @@ impl Resolve for ListResourceSyncs { ResourceSyncSortBy::Name => resource::ListItemSort::Name, ResourceSyncSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.repo.cmp(&b.info.repo) + a.info + .repo + .cmp(&b.info.repo) + .then_with(|| a.name.cmp(&b.name)) })) } ResourceSyncSortBy::Branch => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.branch.cmp(&b.info.branch) + a.info + .branch + .cmp(&b.info.branch) + .then_with(|| a.name.cmp(&b.name)) })) } ResourceSyncSortBy::State => { resource::ListItemSort::InMemory(Box::new(|a, b| { - a.info.state.cmp(&b.info.state) + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) })) } }; From 52e8b5932c1c9a77e3c6b682dc7487b473c09903 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 13:31:02 -0700 Subject: [PATCH 142/203] deploy 2.3.0-dev-36 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b31599d33..e3e1497261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7d8ce6cc7a..ec9ce47331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-35" +version = "2.3.0-dev-36" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From f75926e8486f2852f61c987907f32ef5acb58676 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 15:23:33 -0700 Subject: [PATCH 143/203] fix Source sorting for stacks, builds, syncs --- bin/core/src/api/read/build.rs | 8 ++++++-- bin/core/src/api/read/stack.rs | 8 ++++++-- bin/core/src/api/read/sync.rs | 8 ++++++-- bin/core/src/resource/build.rs | 5 ++++- bin/core/src/resource/stack.rs | 5 ++++- bin/core/src/resource/sync.rs | 5 ++++- client/core/rs/src/entities/build.rs | 3 +++ client/core/rs/src/entities/stack.rs | 3 +++ client/core/rs/src/entities/sync.rs | 3 +++ client/core/ts/src/types.ts | 6 ++++++ ui/public/client/types.d.ts | 6 ++++++ 11 files changed, 51 insertions(+), 9 deletions(-) diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 47a42d3048..bd5a3e36d2 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -63,8 +63,12 @@ impl Resolve for ListBuilds { BuildSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { a.info - .repo - .cmp(&b.info.repo) + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) .then_with(|| a.name.cmp(&b.name)) })) } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 0f1ae395ac..f4006d6709 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -72,8 +72,12 @@ impl Resolve for ListStacks { StackSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { a.info - .repo - .cmp(&b.info.repo) + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) .then_with(|| a.name.cmp(&b.name)) })) } diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index a63ab485ae..5abf638195 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -51,8 +51,12 @@ impl Resolve for ListResourceSyncs { ResourceSyncSortBy::Source => { resource::ListItemSort::InMemory(Box::new(|a, b| { a.info - .repo - .cmp(&b.info.repo) + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) .then_with(|| a.name.cmp(&b.name)) })) } diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index d06b0b75f7..9b544ac2b5 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -74,8 +74,9 @@ impl super::KomodoResource for Build { build.config.repo, build.config.branch, build.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if build.config.linked_repo.is_empty() { default_git } else { @@ -89,6 +90,7 @@ impl super::KomodoResource for Build { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -107,6 +109,7 @@ impl super::KomodoResource for Build { files_on_host: build.config.files_on_host, dockerfile_contents: !build.config.dockerfile.is_empty(), linked_repo: build.config.linked_repo, + linked_repo_name, repo_link: repo_link( &git_provider, &repo, diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index 8668c35464..2f7208fd1a 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -145,8 +145,9 @@ impl super::KomodoResource for Stack { stack.config.repo, stack.config.branch, stack.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if stack.config.linked_repo.is_empty() { default_git } else { @@ -160,6 +161,7 @@ impl super::KomodoResource for Stack { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -223,6 +225,7 @@ impl super::KomodoResource for Stack { server_id: stack.config.server_id, server_name, linked_repo: stack.config.linked_repo, + linked_repo_name, missing_files: stack.info.missing_files, files_on_host: stack.config.files_on_host, repo_link: repo_link( diff --git a/bin/core/src/resource/sync.rs b/bin/core/src/resource/sync.rs index bd15125951..8f209e6c59 100644 --- a/bin/core/src/resource/sync.rs +++ b/bin/core/src/resource/sync.rs @@ -57,8 +57,9 @@ impl super::KomodoResource for ResourceSync { resource_sync.config.repo, resource_sync.config.branch, resource_sync.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if resource_sync.config.linked_repo.is_empty() { default_git } else { @@ -72,6 +73,7 @@ impl super::KomodoResource for ResourceSync { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -88,6 +90,7 @@ impl super::KomodoResource for ResourceSync { files_on_host: resource_sync.config.files_on_host, managed: resource_sync.config.managed, linked_repo: resource_sync.config.linked_repo, + linked_repo_name, repo_link: repo_link( &git_provider, &repo, diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index ff2e6bb2c0..11accc9d27 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -218,6 +218,9 @@ pub struct BuildListItemInfo { /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain pub git_provider: String, /// The repo used as the source of the build diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index f3714f458f..f77a07f475 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -165,6 +165,9 @@ pub struct StackListItemInfo { pub file_contents: bool, /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain pub git_provider: String, /// The configured repo diff --git a/client/core/rs/src/entities/sync.rs b/client/core/rs/src/entities/sync.rs index f6cd6ba223..fee384feac 100644 --- a/client/core/rs/src/entities/sync.rs +++ b/client/core/rs/src/entities/sync.rs @@ -36,6 +36,9 @@ pub struct ResourceSyncListItemInfo { pub resource_path: Vec, /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain. pub git_provider: String, /// The Github repo used as the source of the sync resources diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 1960a89f9c..6a2c1c03bf 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -808,6 +808,8 @@ export interface BuildListItemInfo { dockerfile_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The repo used as the source of the build */ @@ -5399,6 +5401,8 @@ export interface ResourceSyncListItemInfo { resource_path: string[]; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain. */ git_provider: string; /** The Github repo used as the source of the sync resources */ @@ -5559,6 +5563,8 @@ export interface StackListItemInfo { file_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The configured repo */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index e50629f9d0..8b4d427e5d 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -798,6 +798,8 @@ export interface BuildListItemInfo { dockerfile_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The repo used as the source of the build */ @@ -5292,6 +5294,8 @@ export interface ResourceSyncListItemInfo { resource_path: string[]; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain. */ git_provider: string; /** The Github repo used as the source of the sync resources */ @@ -5438,6 +5442,8 @@ export interface StackListItemInfo { file_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The configured repo */ From e4bad13907dac3982d167d696b8954a38fcf41d1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 15:24:04 -0700 Subject: [PATCH 144/203] deploy 2.3.0-dev-37 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3e1497261..2b493127f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index ec9ce47331..231080e04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-36" +version = "2.3.0-dev-37" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 2f34411e14357b8e048d122ada1d0fe1aea4169d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 17:17:14 -0700 Subject: [PATCH 145/203] keepPreviousData method to prevent lots of loading flashes --- ui/package.json | 4 +- ui/src/pages/alerts.tsx | 23 +++-- ui/src/pages/containers.tsx | 13 ++- ui/src/pages/dashboard/tables.tsx | 12 ++- ui/src/pages/resources.tsx | 22 ++--- ui/src/pages/updates.tsx | 21 +++-- ui/src/resources/multi-selector.tsx | 14 ++- ui/src/resources/selector.tsx | 20 ++-- ui/yarn.lock | 137 ++++++++++++++++++++++++++-- 9 files changed, 206 insertions(+), 60 deletions(-) diff --git a/ui/package.json b/ui/package.json index 586652da13..dcc0a02208 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,7 @@ "jotai-location": "^0.6.2", "lucide-react": "^1.22.0", "mogh_auth_client": "^1.7.0", - "mogh_ui": "^0.7.2", + "mogh_ui": "^0.7.3", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.5.1", "react": "^19.2.7", @@ -50,7 +50,7 @@ "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "sass-embedded": "^1.100.0", - "typescript": "^6.0.3", + "typescript": "^7.0.2", "vite": "^8.1.2" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/ui/src/pages/alerts.tsx b/ui/src/pages/alerts.tsx index 471c781d6b..74068e95f4 100644 --- a/ui/src/pages/alerts.tsx +++ b/ui/src/pages/alerts.tsx @@ -14,6 +14,7 @@ import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; import { ActionIcon, Group, Pagination, Select, Stack } from "@mantine/core"; import { Types } from "komodo_client"; +import { keepPreviousData } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; @@ -51,15 +52,21 @@ export default function Alerts() { [params], ); - const { data: alerts } = useRead("ListAlerts", { - query: { - "target.type": type, - "target.id": id, - "data.type": alertType, - resolved: !open, + const { data: alerts } = useRead( + "ListAlerts", + { + query: { + "target.type": type, + "target.id": id, + "data.type": alertType, + resolved: !open, + }, + page, }, - page, - }); + // Keep the previous rows visible while fetching after a + // query key change (page / filters) to prevent table flashing. + { placeholderData: keepPreviousData }, + ); const alertTypes: string[] = type ? (ALERT_TYPES_BY_RESOURCE[type] ?? FALLBACK_ALERT_TYPES) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 74fbf14fbd..f4edb35008 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -2,6 +2,7 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; import { useRead, useTagsFilter } from "@/lib/hooks"; +import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; @@ -50,7 +51,7 @@ export default function Containers() { setPage(0); }, [selectedServers, tags, sort.sort_by, sort.sort_desc]); - const _containers = + const containers = useRead( "ListAllContainers", { @@ -62,12 +63,14 @@ export default function Containers() { sort_by: sort.sort_by, sort_desc: sort.sort_desc, }, - { refetchInterval: 15_000 }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + placeholderData: keepPreviousData, + }, ).data ?? []; - // Prevent flashes during loading - const containers = useDebounce(_containers, 300); - const Table = useMemo( () => ( show && , diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 02345f8e29..47e362d41a 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -90,7 +90,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) { ? { update_available: filterUpdateAvailable } : undefined, }; - const _resources = + const resources = useRead( `List${type}s`, { @@ -99,20 +99,16 @@ export default function Resources({ _type }: { _type?: UsableResource }) { sort_by: sort.sort_by as any, sort_desc: sort.sort_desc, }, - { refetchInterval: 15_000 }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + // Must NOT keep them across a change of resource type. + placeholderData: (prev, prevQuery) => + prevQuery?.queryKey[0] === `List${type}s` ? prev : undefined, + }, ).data ?? []; - // Debounce: prevents flashing when typing / fetching. - // Must also set immediately upon change of resource type - const [resources, setResources] = useState(_resources); - useEffect(() => setResources(_resources), [type]); - useEffect(() => { - const handler = setTimeout(() => setResources(_resources), 300); - return () => { - clearTimeout(handler); - }; - }, [_resources]); - const RC = ResourceComponents[type]; const Table = useMemo( diff --git a/ui/src/pages/updates.tsx b/ui/src/pages/updates.tsx index 779488715b..603b34665f 100644 --- a/ui/src/pages/updates.tsx +++ b/ui/src/pages/updates.tsx @@ -22,6 +22,7 @@ import { Text, } from "@mantine/core"; import { Types } from "komodo_client"; +import { keepPreviousData } from "@tanstack/react-query"; import { useState } from "react"; import { useSearchParams } from "react-router-dom"; @@ -37,14 +38,20 @@ export default function Updates() { operation: params.get("operation") as Types.Operation | null, }; - const { data: updates } = useRead("ListUpdates", { - query: { - "target.type": type ?? undefined, - "target.id": id ?? undefined, - operation: operation ?? undefined, + const { data: updates } = useRead( + "ListUpdates", + { + query: { + "target.type": type ?? undefined, + "target.id": id ?? undefined, + operation: operation ?? undefined, + }, + page, }, - page, - }); + // Keep the previous rows visible while fetching after a + // query key change (page / filters) to prevent table flashing. + { placeholderData: keepPreviousData }, + ); return ( (__resources?.filter( (r) => @@ -77,9 +84,6 @@ export default function ResourceSelector({ [__resources], ); - // Prevent flashing when typing / fetching - const resources = useDebounce(_resources, 300); - const name = selectedResource?.name; if ( diff --git a/ui/yarn.lock b/ui/yarn.lock index 496921ea59..541da21507 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -459,6 +459,106 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== +"@typescript/typescript-aix-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6" + integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ== + +"@typescript/typescript-darwin-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235" + integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA== + +"@typescript/typescript-darwin-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4" + integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA== + +"@typescript/typescript-freebsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa" + integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ== + +"@typescript/typescript-freebsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd" + integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw== + +"@typescript/typescript-linux-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f" + integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ== + +"@typescript/typescript-linux-arm@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e" + integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ== + +"@typescript/typescript-linux-loong64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad" + integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ== + +"@typescript/typescript-linux-mips64el@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0" + integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA== + +"@typescript/typescript-linux-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d" + integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA== + +"@typescript/typescript-linux-riscv64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1" + integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ== + +"@typescript/typescript-linux-s390x@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e" + integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw== + +"@typescript/typescript-linux-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2" + integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A== + +"@typescript/typescript-netbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901" + integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA== + +"@typescript/typescript-netbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab" + integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA== + +"@typescript/typescript-openbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf" + integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ== + +"@typescript/typescript-openbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f" + integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg== + +"@typescript/typescript-sunos-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88" + integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g== + +"@typescript/typescript-win32-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7" + integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ== + +"@typescript/typescript-win32-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c" + integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g== + "@vitejs/plugin-react@^6.0.3": version "6.0.3" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz#55f1d7f558534d10aef03c007dc208b7c3771ce4" @@ -913,10 +1013,10 @@ mogh_auth_client@^1.7.0: dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.2.tgz#d6d7e42aaa2256679277994bf519eef79c0eec11" - integrity sha512-kRQ/9g2E+gSZzxOc1wwHwiKpldf+mtaAq1tEMAQSF2tTNI59OIZIuJSZHvbcJPQ0IfB7e7meW8Be+3aEmnvHyQ== +mogh_ui@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.3.tgz#7aea4de43d2dc91c93fab09bd2a179f7ef434ac7" + integrity sha512-wVwkatCKmX/WB3lpowEPOy4q3o9o3hvG4rhiqdXyp/zyC7B14Bi5yT2BFiEcbzZ9jGx2/fC4c5SBCQYGGFyr0A== monaco-editor@^0.55.1: version "0.55.1" @@ -1512,10 +1612,31 @@ type-fest@^5.7.0: dependencies: tagged-tag "^1.0.0" -typescript@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" - integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== +typescript@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582" + integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA== + optionalDependencies: + "@typescript/typescript-aix-ppc64" "7.0.2" + "@typescript/typescript-darwin-arm64" "7.0.2" + "@typescript/typescript-darwin-x64" "7.0.2" + "@typescript/typescript-freebsd-arm64" "7.0.2" + "@typescript/typescript-freebsd-x64" "7.0.2" + "@typescript/typescript-linux-arm" "7.0.2" + "@typescript/typescript-linux-arm64" "7.0.2" + "@typescript/typescript-linux-loong64" "7.0.2" + "@typescript/typescript-linux-mips64el" "7.0.2" + "@typescript/typescript-linux-ppc64" "7.0.2" + "@typescript/typescript-linux-riscv64" "7.0.2" + "@typescript/typescript-linux-s390x" "7.0.2" + "@typescript/typescript-linux-x64" "7.0.2" + "@typescript/typescript-netbsd-arm64" "7.0.2" + "@typescript/typescript-netbsd-x64" "7.0.2" + "@typescript/typescript-openbsd-arm64" "7.0.2" + "@typescript/typescript-openbsd-x64" "7.0.2" + "@typescript/typescript-sunos-x64" "7.0.2" + "@typescript/typescript-win32-arm64" "7.0.2" + "@typescript/typescript-win32-x64" "7.0.2" undici-types@~8.3.0: version "8.3.0" From da49b12938e36b225a47ef20705cc8ee6f1c8629 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 17:41:20 -0700 Subject: [PATCH 146/203] omni search tweaks / fixes --- ui/src/app/topbar/omni-search/hooks.tsx | 44 ++++++++++------ ui/src/app/topbar/omni-search/index.tsx | 3 +- ui/src/lib/hooks.ts | 67 +++++++++++++++++++++---- ui/src/lib/utils.ts | 10 ++++ 4 files changed, 97 insertions(+), 27 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index a15150f53e..865dd61987 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -6,7 +6,11 @@ import { useUser, } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; -import { terminalLink, usableResourcePath } from "@/lib/utils"; +import { + terminalLink, + termMatchesTypeKeyword, + usableResourcePath, +} from "@/lib/utils"; import { RESOURCE_TARGETS, ResourceComponents } from "@/resources"; import { spotlight, @@ -22,7 +26,6 @@ import { hexColorByIntention, useDebounce } from "mogh_ui"; import { containerStateIntention, swarmStateIntention } from "@/lib/color"; const ITEM_LIMIT = 7; -let count = 0; export function useOmniSearch(opened: boolean): { search: string; @@ -55,7 +58,7 @@ export function useOmniSearch(opened: boolean): { () => ({ terms: debouncedTerms // Allows search like 'cont my name' to return containers matching 'my name' - .filter((term) => !"container".includes(term)), + .filter((term) => !termMatchesTypeKeyword("containers", term)), limit: 10, page: 0, }), @@ -72,7 +75,7 @@ export function useOmniSearch(opened: boolean): { () => ({ terms: debouncedTerms // Allows search like 'serv my name' to return services matching 'my name' - .filter((term) => !"service".includes(term)), + .filter((term) => !termMatchesTypeKeyword("services", term)), limit: 10, page: 0, }), @@ -95,7 +98,8 @@ export function useOmniSearch(opened: boolean): { if (searchTerms.length === 0) return true; const lower = c.name.toLowerCase(); return searchTerms.every( - (term) => lower.includes(term) || "terminals".includes(term), + (term) => + lower.includes(term) || termMatchesTypeKeyword("terminals", term), ); }); }, [_terminals, searchTerms]); @@ -182,7 +186,8 @@ export function useOmniSearch(opened: boolean): { ...RESOURCE_TARGETS.map((_type) => { const type = _type === "ResourceSync" ? "Sync" : _type; - const lowerType = type.toLowerCase(); + // Matches the server side term stripping in useAllResources + const typeKeyword = type.toLowerCase() + "s"; const Components = ResourceComponents[_type]; return { group: type + "s", @@ -194,7 +199,8 @@ export function useOmniSearch(opened: boolean): { searchTerms.length === 0 || searchTerms.every( (term) => - lowerName.includes(term) || lowerType.includes(term), + lowerName.includes(term) || + termMatchesTypeKeyword(typeKeyword, term), ) ); }) @@ -249,7 +255,7 @@ export function useOmniSearch(opened: boolean): { group: "Containers", actions: containers?.map((container) => ({ - id: container.server_id ?? "" + " " + container.name, + id: (container.server_id ?? "") + " " + container.name, label: container.name, description: "Server: " + container.server_name, onClick: () => @@ -281,25 +287,33 @@ export function useOmniSearch(opened: boolean): { })) ?? [], }, ]; - }, [resources, containers, services, terminals, searchTerms]); + }, [ + resources, + containers, + services, + terminals, + searchTerms, + user, + nav, + setSettingsView, + ]); // LIMIT the action count for performance. - // Reset count on render before creating actual actions. - count = 0; + let count = 0; const actions: SpotlightActionGroupData[] = []; for (const group of _actions) { const groupActions = []; for (const action of group.actions) { - groupActions.push(action); - count += 1; - if (count > ITEM_LIMIT) { + if (count >= ITEM_LIMIT) { break; } + groupActions.push(action); + count += 1; } if (groupActions.length) { actions.push({ group: group.group, actions: groupActions }); } - if (count > ITEM_LIMIT) { + if (count >= ITEM_LIMIT) { break; } } diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index 1c7c98ced8..7d82789359 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -67,7 +67,8 @@ export default function OmniSearch({}: {}) { Clear on close setClearOnClose((v) => !v)} + onClick={(e) => e.stopPropagation()} + onChange={() => setClearOnClose((v) => !v)} /> diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 5983c7e217..703cdf7914 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -21,6 +21,7 @@ import { UsableResource, RESOURCE_TARGETS } from "@/resources"; import { hasMinimumPermissions, resourceTargetFromTerminalTarget, + termMatchesTypeKeyword, } from "@/lib/utils"; import { notifications } from "@mantine/notifications"; @@ -303,7 +304,11 @@ export function useAllResources( Swarm: useRead( "ListSwarms", { - query: { terms: terms?.filter((term) => !"swarm".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("swarms", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -311,7 +316,11 @@ export function useAllResources( Server: useRead( "ListServers", { - query: { terms: terms?.filter((term) => !"server".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("servers", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -319,7 +328,11 @@ export function useAllResources( Stack: useRead( "ListStacks", { - query: { terms: terms?.filter((term) => !"stack".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("stacks", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -327,7 +340,11 @@ export function useAllResources( Deployment: useRead( "ListDeployments", { - query: { terms: terms?.filter((term) => !"deployment".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("deployments", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -335,7 +352,11 @@ export function useAllResources( Build: useRead( "ListBuilds", { - query: { terms: terms?.filter((term) => !"build".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("builds", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -343,7 +364,11 @@ export function useAllResources( Repo: useRead( "ListRepos", { - query: { terms: terms?.filter((term) => !"repo".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("repos", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -351,7 +376,11 @@ export function useAllResources( Procedure: useRead( "ListProcedures", { - query: { terms: terms?.filter((term) => !"procedure".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("procedures", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -359,7 +388,11 @@ export function useAllResources( Action: useRead( "ListActions", { - query: { terms: terms?.filter((term) => !"action".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("actions", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -367,7 +400,11 @@ export function useAllResources( Builder: useRead( "ListBuilders", { - query: { terms: terms?.filter((term) => !"builder".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("builders", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -375,7 +412,11 @@ export function useAllResources( Alerter: useRead( "ListAlerters", { - query: { terms: terms?.filter((term) => !"alerter".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("alerters", term), + ), + }, limit, }, { refetchInterval, enabled }, @@ -383,7 +424,11 @@ export function useAllResources( ResourceSync: useRead( "ListResourceSyncs", { - query: { terms: terms?.filter((term) => !"sync".includes(term)) }, + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("syncs", term), + ), + }, limit, }, { refetchInterval, enabled }, diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index f7b612c23f..4bb5fb2759 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -122,6 +122,16 @@ export function hasMinimumPermissions( return true; } +/** + * Returns true if the search term is shorthand for the given resource type keyword, + * eg. term 'cont' matches keyword 'containers'. This lets searches like 'cont my-name' + * scope to containers matching 'my-name'. Requires at least 3 characters so short + * terms like 'on' are still used to search by name. + */ +export function termMatchesTypeKeyword(keyword: string, term: string) { + return term.length >= 3 && keyword.startsWith(term); +} + export function usableResourcePath(resource: UsableResource) { if (resource === "ResourceSync") return "resource-syncs"; return `${resource.toLowerCase()}s`; From 338cc69ce91426107793a1cc20c27513658b066e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 11 Jul 2026 17:48:53 -0700 Subject: [PATCH 147/203] fix clear on close --- ui/src/app/topbar/omni-search/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index 7d82789359..04a05d987d 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -67,7 +67,6 @@ export default function OmniSearch({}: {}) { Clear on close e.stopPropagation()} onChange={() => setClearOnClose((v) => !v)} /> From 9f548bceee939e8172379be1376bb8496aee3e4d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 13:50:06 -0700 Subject: [PATCH 148/203] paginated schedules --- bin/core/src/api/read/schedule.rs | 62 +++++++++++++--- client/core/rs/src/api/read/schedule.rs | 34 ++++++++- client/core/rs/src/entities/action.rs | 20 +++++- client/core/rs/src/entities/procedure.rs | 20 +++++- client/core/rs/src/entities/schedule.rs | 17 +++++ client/core/ts/src/types.ts | 48 +++++++++++++ ui/public/client/types.d.ts | 37 ++++++++++ ui/public/client/types.js | 11 +++ ui/src/pages/schedules.tsx | 90 +++++++++++++++++++----- 9 files changed, 308 insertions(+), 31 deletions(-) diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index 7df93ae75f..e2b6f253a3 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -3,11 +3,11 @@ use komodo_client::{ api::read::*, entities::{ ResourceTarget, - action::Action, + action::{Action, ActionQuerySpecifics}, permission::PermissionLevel, - procedure::Procedure, + procedure::{Procedure, ProcedureQuerySpecifics}, resource::{ResourceQuery, TemplatesQueryBehavior}, - schedule::Schedule, + schedule::{Schedule, ScheduleSortBy}, }, }; use mogh_resolver::Resolve; @@ -32,6 +32,11 @@ impl Resolve for ListSchedules { templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), + terms: self.terms.clone(), + specific: ActionQuerySpecifics { + scheduled: Some(true), + ..Default::default() + }, ..Default::default() }, None, @@ -45,6 +50,11 @@ impl Resolve for ListSchedules { templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), + terms: self.terms, + specific: ProcedureQuerySpecifics { + scheduled: Some(true), + ..Default::default() + }, ..Default::default() }, None, @@ -98,12 +108,44 @@ impl Resolve for ListSchedules { let (actions, procedures) = tokio::join!(join_all(actions), join_all(procedures)); - Ok( - actions - .into_iter() - .chain(procedures) - .filter(|s| !s.schedule.is_empty()) - .collect(), - ) + // The terms / scheduled filters are already applied + // at the db level by the queries above. + let mut schedules = + actions.into_iter().chain(procedures).collect::>(); + + // The schedules are composed in memory across resource types, + // so all matching schedules are collected and sorted + // before applying pagination. + let compare = |a: &Schedule, b: &Schedule| { + match self.sort_by { + ScheduleSortBy::Name => a.name.cmp(&b.name), + ScheduleSortBy::Schedule => a.schedule.cmp(&b.schedule), + // Order unscheduled (None) last, matching the UI. + ScheduleSortBy::NextRun => { + (a.next_scheduled_run.is_none(), a.next_scheduled_run).cmp( + &(b.next_scheduled_run.is_none(), b.next_scheduled_run), + ) + } + ScheduleSortBy::Enabled => a.enabled.cmp(&b.enabled), + } + // Fall back to name based sorting for equal sort keys. + // Inside `compare`, so descending sorts are fully descending, + // matching the List apis. + .then_with(|| a.name.cmp(&b.name)) + }; + if self.sort_desc { + schedules.sort_by(|a, b| compare(b, a)); + } else { + schedules.sort_by(|a, b| compare(a, b)); + } + + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(schedules.into_iter().skip(skip).take(take).collect()) } } diff --git a/client/core/rs/src/api/read/schedule.rs b/client/core/rs/src/api/read/schedule.rs index 5f1c2bb21d..63a5039313 100644 --- a/client/core/rs/src/api/read/schedule.rs +++ b/client/core/rs/src/api/read/schedule.rs @@ -4,7 +4,11 @@ use typeshare::typeshare; use crate::{ deserializers::string_list_deserializer, - entities::{resource::TagQueryBehavior, schedule::Schedule}, + entities::{ + U64, + resource::TagQueryBehavior, + schedule::{Schedule, ScheduleSortBy}, + }, }; use super::KomodoReadRequest; @@ -38,6 +42,34 @@ pub struct ListSchedules { /// 'All' or 'Any' #[serde(default)] pub tag_behavior: TagQueryBehavior, + + /// Filter by target name. + /// Returned schedules have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of schedules per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: ScheduleSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/entities/action.rs b/client/core/rs/src/entities/action.rs index 51eac1e0c7..b24e823908 100644 --- a/client/core/rs/src/entities/action.rs +++ b/client/core/rs/src/entities/action.rs @@ -281,8 +281,26 @@ pub struct ActionQuerySpecifics { /// If empty, does not filter by state. #[serde(default)] pub states: Vec, + /// Query only for Actions with (or without) + /// a schedule configured. + #[serde(default)] + pub scheduled: Option, } impl super::resource::AddFilters for ActionQuerySpecifics { - fn add_filters(&self, _filters: &mut Document) {} + fn add_filters(&self, filters: &mut Document) { + if let Some(scheduled) = self.scheduled { + if scheduled { + filters.insert( + "config.schedule", + bson::doc! { "$nin": ["", null] }, + ); + } else { + filters.insert( + "config.schedule", + bson::doc! { "$in": ["", null] }, + ); + } + } + } } diff --git a/client/core/rs/src/entities/procedure.rs b/client/core/rs/src/entities/procedure.rs index ce85bdbf06..80e96a0afc 100644 --- a/client/core/rs/src/entities/procedure.rs +++ b/client/core/rs/src/entities/procedure.rs @@ -281,8 +281,26 @@ pub struct ProcedureQuerySpecifics { /// If empty, does not filter by state. #[serde(default)] pub states: Vec, + /// Query only for Procedures with (or without) + /// a schedule configured. + #[serde(default)] + pub scheduled: Option, } impl super::resource::AddFilters for ProcedureQuerySpecifics { - fn add_filters(&self, _: &mut Document) {} + fn add_filters(&self, filters: &mut Document) { + if let Some(scheduled) = self.scheduled { + if scheduled { + filters.insert( + "config.schedule", + bson::doc! { "$nin": ["", null] }, + ); + } else { + filters.insert( + "config.schedule", + bson::doc! { "$in": ["", null] }, + ); + } + } + } } diff --git a/client/core/rs/src/entities/schedule.rs b/client/core/rs/src/entities/schedule.rs index 86a20185c0..93f2d7b56d 100644 --- a/client/core/rs/src/entities/schedule.rs +++ b/client/core/rs/src/entities/schedule.rs @@ -30,3 +30,20 @@ pub struct Schedule { /// Resource tags. pub tags: Vec, } + +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ScheduleSortBy { + /// Sort by target name. Default. + #[default] + Name, + /// Sort by the schedule expression. + Schedule, + /// Sort by next scheduled run. + NextRun, + /// Sort by enabled. + Enabled, +} diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 6a2c1c03bf..429f162ee0 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -224,6 +224,11 @@ export interface ActionQuerySpecifics { * If empty, does not filter by state. */ states?: ActionState[]; + /** + * Query only for Actions with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ActionQuery = ResourceQuery; @@ -5823,6 +5828,11 @@ export interface ProcedureQuerySpecifics { * If empty, does not filter by state. */ states?: ProcedureState[]; + /** + * Query only for Procedures with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ProcedureQuery = ResourceQuery; @@ -9699,6 +9709,17 @@ export interface ListResourceSyncs { sort_desc?: boolean; } +export enum ScheduleSortBy { + /** Sort by target name. Default. */ + Name = "Name", + /** Sort by the schedule expression. */ + Schedule = "Schedule", + /** Sort by next scheduled run. */ + NextRun = "NextRun", + /** Sort by enabled. */ + Enabled = "Enabled", +} + /** * List configured schedules. * Response: [ListSchedulesResponse]. @@ -9708,6 +9729,33 @@ export interface ListSchedules { tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + /** + * Filter by target name. + * Returned schedules have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of schedules per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ScheduleSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 8b4d427e5d..28bf2ad168 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -9243,6 +9243,16 @@ export interface ListResourceSyncs { /** Reverse the sort direction. */ sort_desc?: boolean; } +export declare enum ScheduleSortBy { + /** Sort by target name. Default. */ + Name = "Name", + /** Sort by the schedule expression. */ + Schedule = "Schedule", + /** Sort by next scheduled run. */ + NextRun = "NextRun", + /** Sort by enabled. */ + Enabled = "Enabled" +} /** * List configured schedules. * Response: [ListSchedulesResponse]. @@ -9252,6 +9262,33 @@ export interface ListSchedules { tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + /** + * Filter by target name. + * Returned schedules have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of schedules per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ScheduleSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List the secret keys (not values) in the core configuration file. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index 9fe1f537e8..925db8d3b1 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -914,6 +914,17 @@ export var ResourceSyncSortBy; /** Sort by state. */ ResourceSyncSortBy["State"] = "State"; })(ResourceSyncSortBy || (ResourceSyncSortBy = {})); +export var ScheduleSortBy; +(function (ScheduleSortBy) { + /** Sort by target name. Default. */ + ScheduleSortBy["Name"] = "Name"; + /** Sort by the schedule expression. */ + ScheduleSortBy["Schedule"] = "Schedule"; + /** Sort by next scheduled run. */ + ScheduleSortBy["NextRun"] = "NextRun"; + /** Sort by enabled. */ + ScheduleSortBy["Enabled"] = "Enabled"; +})(ScheduleSortBy || (ScheduleSortBy = {})); export var ServerSortBy; (function (ServerSortBy) { /** Sort by name. Default. */ diff --git a/ui/src/pages/schedules.tsx b/ui/src/pages/schedules.tsx index 63572b872f..4ded5e1c38 100644 --- a/ui/src/pages/schedules.tsx +++ b/ui/src/pages/schedules.tsx @@ -1,5 +1,6 @@ import TagsFilter from "@/components/tags/filter"; import TableTags from "@/components/tags/table"; +import ListPagination from "@/components/list-pagination"; import { usePermissions, useRead, @@ -7,23 +8,65 @@ import { useTags, useWrite, } from "@/lib/hooks"; -import { filterBySplit } from "mogh_ui"; import { UsableResource } from "@/resources"; import ResourceLink from "@/resources/link"; import { ICONS } from "@/lib/icons"; -import { DataTable, SortableHeader } from "mogh_ui"; +import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; import { Page } from "mogh_ui"; import { SearchInput } from "mogh_ui"; import { Group, Stack, Switch } from "@mantine/core"; import { notifications } from "@mantine/notifications"; -import { useState } from "react"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { useEffect, useMemo, useState } from "react"; + +const SCHEDULE_SORT_KEYS = Object.values(Types.ScheduleSortBy); export default function Schedules() { useSetTitle("Schedules"); const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo( + () => + debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term), + [debouncedSearch], + ); const { tags } = useTags(); - const schedules = useRead("ListSchedules", { tags }).data; - const filtered = filterBySplit(schedules ?? [], search, (item) => item.name); + + const [page, setPage] = useState(0); + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.ScheduleSortBy; + sort_desc?: boolean; + }>({}); + + // Set to page 0 whenever any filter or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, tags, sort.sort_by, sort.sort_desc]); + + const schedules = + useRead( + "ListSchedules", + { + tags, + terms, + page, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ).data ?? []; return ( + { + const sort = sorting.find((s) => + SCHEDULE_SORT_KEYS.includes(s.id as Types.ScheduleSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.ScheduleSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} columns={[ { size: 200, + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -56,6 +119,7 @@ export default function Schedules() { }, { size: 200, + id: "Schedule", accessorKey: "schedule", header: ({ column }) => ( @@ -63,22 +127,11 @@ export default function Schedules() { }, { size: 200, + id: "NextRun", accessorKey: "next_scheduled_run", header: ({ column }) => ( ), - sortingFn: (a, b) => { - const sa = a.original.next_scheduled_run; - const sb = b.original.next_scheduled_run; - - if (!sa && !sb) return 0; - if (!sa) return 1; - if (!sb) return -1; - - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, cell: ({ row }) => row.original.next_scheduled_run ? new Date(row.original.next_scheduled_run).toLocaleString() @@ -86,6 +139,7 @@ export default function Schedules() { }, { size: 100, + id: "Enabled", accessorKey: "enabled", header: ({ column }) => ( From 4c282916632e83a183106d9540c3d6aa509c8211 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 13:55:24 -0700 Subject: [PATCH 149/203] for efficient schedule sort --- bin/core/src/api/read/schedule.rs | 39 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index e2b6f253a3..f821edb4f7 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use futures_util::future::join_all; use komodo_client::{ api::read::*, @@ -116,23 +118,30 @@ impl Resolve for ListSchedules { // The schedules are composed in memory across resource types, // so all matching schedules are collected and sorted // before applying pagination. - let compare = |a: &Schedule, b: &Schedule| { + // All comparators fall back to name based sorting for equal + // sort keys, inside `compare`, so descending sorts are fully + // descending, matching the List apis. + let compare: fn(&Schedule, &Schedule) -> Ordering = match self.sort_by { - ScheduleSortBy::Name => a.name.cmp(&b.name), - ScheduleSortBy::Schedule => a.schedule.cmp(&b.schedule), + ScheduleSortBy::Name => |a, b| a.name.cmp(&b.name), + ScheduleSortBy::Schedule => |a, b| { + a.schedule + .cmp(&b.schedule) + .then_with(|| a.name.cmp(&b.name)) + }, // Order unscheduled (None) last, matching the UI. - ScheduleSortBy::NextRun => { - (a.next_scheduled_run.is_none(), a.next_scheduled_run).cmp( - &(b.next_scheduled_run.is_none(), b.next_scheduled_run), - ) - } - ScheduleSortBy::Enabled => a.enabled.cmp(&b.enabled), - } - // Fall back to name based sorting for equal sort keys. - // Inside `compare`, so descending sorts are fully descending, - // matching the List apis. - .then_with(|| a.name.cmp(&b.name)) - }; + ScheduleSortBy::NextRun => |a, b| { + (a.next_scheduled_run.is_none(), a.next_scheduled_run) + .cmp(&( + b.next_scheduled_run.is_none(), + b.next_scheduled_run, + )) + .then_with(|| a.name.cmp(&b.name)) + }, + ScheduleSortBy::Enabled => |a, b| { + a.enabled.cmp(&b.enabled).then_with(|| a.name.cmp(&b.name)) + }, + }; if self.sort_desc { schedules.sort_by(|a, b| compare(b, a)); } else { From 51cceb48e62500f95d4577e039380a7338fff95f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 14:33:25 -0700 Subject: [PATCH 150/203] paginated terminals --- bin/core/src/api/read/terminal.rs | 166 ++++++++++++++++-------- client/core/rs/src/api/read/terminal.rs | 33 ++++- client/core/rs/src/entities/terminal.rs | 19 +++ client/core/ts/src/types.ts | 40 ++++++ ui/public/client/types.d.ts | 49 +++++++ ui/public/client/types.js | 13 ++ ui/src/app/topbar/omni-search/hooks.tsx | 30 ++--- ui/src/components/terminal/section.tsx | 2 + ui/src/pages/terminals/index.tsx | 87 ++++++++++++- 9 files changed, 360 insertions(+), 79 deletions(-) diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index a9a6ea0063..e9b2152821 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -1,15 +1,19 @@ +use std::cmp::Ordering; + use anyhow::Context as _; use futures_util::{ FutureExt, StreamExt as _, stream::FuturesUnordered, }; use komodo_client::{ - api::read::{ListTerminals, ListTerminalsResponse}, + api::read::{ + DEFAULT_LIST_LIMIT, ListTerminals, ListTerminalsResponse, + }, entities::{ deployment::Deployment, permission::PermissionLevel, server::Server, stack::Stack, - terminal::{Terminal, TerminalTarget}, + terminal::{Terminal, TerminalSortBy, TerminalTarget}, user::User, }, }; @@ -31,57 +35,111 @@ impl Resolve for ListTerminals { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { - let Some(target) = self.target else { - return list_all_terminals_for_user(user, self.use_names).await; - }; - match &target { - TerminalTarget::Server { server } => { - let server = server - .as_ref() - .context("Must provide 'target.params.server'") - .status_code(StatusCode::BAD_REQUEST)?; - let server = get_check_permissions::( - server, - user, - PermissionLevel::Read.terminal(), - ) - .await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Container { server, .. } => { - let server = get_check_permissions::( - server, - user, - PermissionLevel::Read.terminal(), - ) - .await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Stack { stack, .. } => { - let server = get_check_permissions::( - stack, - user, - PermissionLevel::Read.terminal(), - ) - .await? - .config - .server_id; - let server = resource::get::(&server).await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Deployment { deployment } => { - let server = get_check_permissions::( - deployment, - user, - PermissionLevel::Read.terminal(), - ) - .await? - .config - .server_id; - let server = resource::get::(&server).await?; - list_terminals_on_server(&server, Some(target)).await + let mut terminals = match self.target { + None => { + list_all_terminals_for_user(user, self.use_names).await? } + Some(target) => match &target { + TerminalTarget::Server { server } => { + let server = server + .as_ref() + .context("Must provide 'target.params.server'") + .status_code(StatusCode::BAD_REQUEST)?; + let server = get_check_permissions::( + server, + user, + PermissionLevel::Read.terminal(), + ) + .await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Container { server, .. } => { + let server = get_check_permissions::( + server, + user, + PermissionLevel::Read.terminal(), + ) + .await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Stack { stack, .. } => { + let server = get_check_permissions::( + stack, + user, + PermissionLevel::Read.terminal(), + ) + .await? + .config + .server_id; + let server = resource::get::(&server).await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Deployment { deployment } => { + let server = get_check_permissions::( + deployment, + user, + PermissionLevel::Read.terminal(), + ) + .await? + .config + .server_id; + let server = resource::get::(&server).await?; + list_terminals_on_server(&server, Some(target)).await? + } + }, + }; + + // The terminals come from Periphery agents rather than the db, + // so the terms filter / sort / pagination are applied in memory. + if !self.terms.is_empty() { + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); + terminals.retain(|terminal| { + let name = terminal.name.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }); + } + + // All comparators fall back to name based sorting for equal + // sort keys, inside `compare`, so descending sorts are fully + // descending, matching the List apis. + let compare: fn(&Terminal, &Terminal) -> Ordering = + match self.sort_by { + TerminalSortBy::Name => |a, b| a.name.cmp(&b.name), + TerminalSortBy::Target => |a, b| { + a.target.cmp(&b.target).then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Command => |a, b| { + a.command.cmp(&b.command).then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Size => |a, b| { + a.stored_size_kb + .total_cmp(&b.stored_size_kb) + .then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Created => |a, b| { + a.created_at + .cmp(&b.created_at) + .then_with(|| a.name.cmp(&b.name)) + }, + }; + if self.sort_desc { + terminals.sort_by(|a, b| compare(b, a)); + } else { + terminals.sort_by(|a, b| compare(a, b)); } + + let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(terminals.into_iter().skip(skip).take(take).collect()) } } @@ -145,7 +203,7 @@ async fn list_all_terminals_for_user( } } - let mut terminals = servers + let terminals = servers .into_iter() .map(|(server, server_permission)| async move { ( @@ -230,10 +288,6 @@ async fn list_all_terminals_for_user( .flatten() .collect::>(); - terminals.sort_by(|a, b| { - a.target.cmp(&b.target).then(a.name.cmp(&b.name)) - }); - Ok(terminals) } diff --git a/client/core/rs/src/api/read/terminal.rs b/client/core/rs/src/api/read/terminal.rs index c2d4f5fd87..ba3ae82853 100644 --- a/client/core/rs/src/api/read/terminal.rs +++ b/client/core/rs/src/api/read/terminal.rs @@ -2,7 +2,10 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::terminal::{Terminal, TerminalTarget}; +use crate::entities::{ + U64, + terminal::{Terminal, TerminalSortBy, TerminalTarget}, +}; use super::KomodoReadRequest; @@ -34,6 +37,34 @@ pub struct ListTerminals { /// Return results with resource names instead of ids. #[serde(default)] pub use_names: bool, + + /// Filter by terminal name. + /// Returned terminals have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of terminals per-page. + /// `limit: 100` is default. + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: TerminalSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/entities/terminal.rs b/client/core/rs/src/entities/terminal.rs index 19def3958e..3f789bbdcf 100644 --- a/client/core/rs/src/entities/terminal.rs +++ b/client/core/rs/src/entities/terminal.rs @@ -29,6 +29,25 @@ pub struct Terminal { pub created_at: I64, } +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum TerminalSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by target. + Target, + /// Sort by init command. + Command, + /// Sort by stored size. + Size, + /// Sort by created timestamp. + Created, +} + #[typeshare] #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 429f162ee0..05032edfce 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -9978,6 +9978,19 @@ export interface ListTags { query?: MongoDocument; } +export enum TerminalSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by target. */ + Target = "Target", + /** Sort by init command. */ + Command = "Command", + /** Sort by stored size. */ + Size = "Size", + /** Sort by created timestamp. */ + Created = "Created", +} + /** * List Terminals. * Response: [ListTerminalsResponse]. @@ -9987,6 +10000,33 @@ export interface ListTerminals { target?: TerminalTarget; /** Return results with resource names instead of ids. */ use_names?: boolean; + /** + * Filter by terminal name. + * Returned terminals have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of terminals per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: TerminalSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 28bf2ad168..bb800fa3dd 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -202,6 +202,11 @@ export interface ActionQuerySpecifics { * If empty, does not filter by state. */ states?: ActionState[]; + /** + * Query only for Actions with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ActionQuery = ResourceQuery; export type AlerterEndpoint = @@ -5680,6 +5685,11 @@ export interface ProcedureQuerySpecifics { * If empty, does not filter by state. */ states?: ProcedureState[]; + /** + * Query only for Procedures with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ProcedureQuery = ResourceQuery; export type PushRecentlyViewedResponse = NoData; @@ -9493,6 +9503,18 @@ export interface ListSystemProcesses { export interface ListTags { query?: MongoDocument; } +export declare enum TerminalSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by target. */ + Target = "Target", + /** Sort by init command. */ + Command = "Command", + /** Sort by stored size. */ + Size = "Size", + /** Sort by created timestamp. */ + Created = "Created" +} /** * List Terminals. * Response: [ListTerminalsResponse]. @@ -9502,6 +9524,33 @@ export interface ListTerminals { target?: TerminalTarget; /** Return results with resource names instead of ids. */ use_names?: boolean; + /** + * Filter by terminal name. + * Returned terminals have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of terminals per-page. + * `limit: 100` is default. + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: TerminalSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Paginated endpoint for updates matching optional query. diff --git a/ui/public/client/types.js b/ui/public/client/types.js index 925db8d3b1..6335906504 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -954,6 +954,19 @@ export var SwarmSortBy; /** Sort by state. */ SwarmSortBy["State"] = "State"; })(SwarmSortBy || (SwarmSortBy = {})); +export var TerminalSortBy; +(function (TerminalSortBy) { + /** Sort by name. Default. */ + TerminalSortBy["Name"] = "Name"; + /** Sort by target. */ + TerminalSortBy["Target"] = "Target"; + /** Sort by init command. */ + TerminalSortBy["Command"] = "Command"; + /** Sort by stored size. */ + TerminalSortBy["Size"] = "Size"; + /** Sort by created timestamp. */ + TerminalSortBy["Created"] = "Created"; +})(TerminalSortBy || (TerminalSortBy = {})); export var ServiceUserQueryBehavior; (function (ServiceUserQueryBehavior) { /** Include service users in results. Default. */ diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 865dd61987..114b941302 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -88,21 +88,21 @@ export function useOmniSearch(opened: boolean): { enabled: opened && !!debouncedTerms.length, }).data; - const _terminals = useRead( - "ListTerminals", - {}, - { refetchInterval: 15_000, enabled: opened }, - ).data; - const terminals = useMemo(() => { - return _terminals?.filter((c) => { - if (searchTerms.length === 0) return true; - const lower = c.name.toLowerCase(); - return searchTerms.every( - (term) => - lower.includes(term) || termMatchesTypeKeyword("terminals", term), - ); - }); - }, [_terminals, searchTerms]); + const terminalsQuery: Types.ListTerminals = useMemo( + () => ({ + terms: debouncedTerms + // Allows search like 'term my name' to return terminals matching 'my name' + .filter((term) => !termMatchesTypeKeyword("terminals", term)), + limit: 10, + page: 0, + }), + [debouncedTerms], + ); + + const terminals = useRead("ListTerminals", terminalsQuery, { + refetchInterval: 15_000, + enabled: opened, + }).data; const user = useUser().data; const resources = useAllResources(debouncedTerms, 10, 15_000, opened); diff --git a/ui/src/components/terminal/section.tsx b/ui/src/components/terminal/section.tsx index d662171f1a..c3d6c755c8 100644 --- a/ui/src/components/terminal/section.tsx +++ b/ui/src/components/terminal/section.tsx @@ -24,6 +24,8 @@ export default function TerminalSection({ "ListTerminals", { target, + // Always show all terminals on the resource. + limit: 0, }, { refetchInterval: 5_000, diff --git a/ui/src/pages/terminals/index.tsx b/ui/src/pages/terminals/index.tsx index fb7a0c5195..487cf5e8c8 100644 --- a/ui/src/pages/terminals/index.tsx +++ b/ui/src/pages/terminals/index.tsx @@ -1,4 +1,5 @@ import TerminalTargetLink from "@/pages/terminals/target-link"; +import ListPagination from "@/components/list-pagination"; import { useRead, useSetTitle } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { terminalLink } from "@/lib/utils"; @@ -6,22 +7,68 @@ import { DataTable, fmtDateWithMinutes, SortableHeader, - filterBySplit, + useDebounce, } from "mogh_ui"; import { Page } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; -import { useState } from "react"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import DeleteTerminal from "./delete"; import BatchDeleteAllTerminals from "./batch-delete"; import NewTerminal from "./new"; import { SearchInput } from "mogh_ui"; +const TERMINAL_SORT_KEYS = Object.values(Types.TerminalSortBy); + export default function Terminals() { useSetTitle("Terminals"); const [search, setSearch] = useState(""); - const { data: terminals, refetch, isPending } = useRead("ListTerminals", {}); - const filtered = filterBySplit(terminals ?? [], search, (item) => item.name); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo( + () => + debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term), + [debouncedSearch], + ); + + const [page, setPage] = useState(0); + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.TerminalSortBy; + sort_desc?: boolean; + }>({}); + + // Set to page 0 whenever the search or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, sort.sort_by, sort.sort_desc]); + + const { + data: terminals, + refetch, + isPending, + } = useRead( + "ListTerminals", + { + terms, + page, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ); + return ( - + + + + { + const sort = sorting.find((s) => + TERMINAL_SORT_KEYS.includes(s.id as Types.TerminalSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.TerminalSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -66,6 +135,7 @@ export default function Terminals() { }, { size: 200, + id: "Target", accessorKey: "target", header: ({ column }) => ( @@ -75,6 +145,7 @@ export default function Terminals() { ), }, { + id: "Command", accessorKey: "command", header: ({ column }) => ( @@ -87,7 +158,8 @@ export default function Terminals() { }, { size: 100, - accessorKey: "size", + id: "Size", + accessorKey: "stored_size_kb", header: ({ column }) => ( ), @@ -102,6 +174,7 @@ export default function Terminals() { ), }, { + id: "Created", accessorKey: "created_at", header: ({ column }) => ( From d3f5e80804fa050ce4ead85e77dc7306e5962532 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 14:38:37 -0700 Subject: [PATCH 151/203] fix terminals cli --- bin/cli/src/command/list.rs | 4 ++++ client/core/rs/src/api/read/schedule.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index 0444527055..c44df71d5e 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -228,6 +228,8 @@ async fn list_terminals( .read(ListTerminals { target: None, use_names: true, + limit: Some(0), + ..Default::default() }) .await?; if !terminals.is_empty() { @@ -245,6 +247,8 @@ async fn list_schedules( .read(ListSchedules { tags: filters.tags.clone(), tag_behavior: Default::default(), + limit: Some(0), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() diff --git a/client/core/rs/src/api/read/schedule.rs b/client/core/rs/src/api/read/schedule.rs index 63a5039313..bf87026a1e 100644 --- a/client/core/rs/src/api/read/schedule.rs +++ b/client/core/rs/src/api/read/schedule.rs @@ -30,7 +30,7 @@ pub fn list_schedules() {} /// List configured schedules. /// Response: [ListSchedulesResponse]. #[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] #[response(ListSchedulesResponse)] From 4abbaa51bc2092626fbde93b81192f41c61ee15a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 14:41:27 -0700 Subject: [PATCH 152/203] fix clippy lint --- bin/core/src/api/read/schedule.rs | 2 +- bin/core/src/api/read/terminal.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index f821edb4f7..ec74a7bbf0 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -145,7 +145,7 @@ impl Resolve for ListSchedules { if self.sort_desc { schedules.sort_by(|a, b| compare(b, a)); } else { - schedules.sort_by(|a, b| compare(a, b)); + schedules.sort_by(compare); } let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index e9b2152821..5a8a75a4cf 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -129,7 +129,7 @@ impl Resolve for ListTerminals { if self.sort_desc { terminals.sort_by(|a, b| compare(b, a)); } else { - terminals.sort_by(|a, b| compare(a, b)); + terminals.sort_by(compare); } let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); From 7a59de36f67a54670201d2925a14178df2e7615b Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 14:41:53 -0700 Subject: [PATCH 153/203] deploy 2.3.0-dev-38 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b493127f7..77fbdb0f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 231080e04c..605fcb4f8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-37" +version = "2.3.0-dev-38" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From a3920c6801a5b53b8b60a493ec3bc3bd4db4d354 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 15:42:33 -0700 Subject: [PATCH 154/203] configurable default pagination, including 0 (no pagination) --- bin/cli/src/command/list.rs | 22 +++--- bin/core/src/api/read/action.rs | 6 +- bin/core/src/api/read/alerter.rs | 6 +- bin/core/src/api/read/build.rs | 6 +- bin/core/src/api/read/builder.rs | 6 +- bin/core/src/api/read/deployment.rs | 6 +- bin/core/src/api/read/docker.rs | 4 +- bin/core/src/api/read/mod.rs | 7 ++ bin/core/src/api/read/procedure.rs | 6 +- bin/core/src/api/read/repo.rs | 6 +- bin/core/src/api/read/schedule.rs | 4 +- bin/core/src/api/read/server.rs | 6 +- bin/core/src/api/read/stack.rs | 8 +-- bin/core/src/api/read/swarm.rs | 6 +- bin/core/src/api/read/sync.rs | 6 +- bin/core/src/api/read/terminal.rs | 8 +-- bin/core/src/config.rs | 3 + client/core/rs/src/api/read/action.rs | 6 +- client/core/rs/src/api/read/alerter.rs | 6 +- client/core/rs/src/api/read/build.rs | 6 +- client/core/rs/src/api/read/builder.rs | 6 +- client/core/rs/src/api/read/deployment.rs | 6 +- client/core/rs/src/api/read/docker.rs | 3 +- client/core/rs/src/api/read/mod.rs | 8 +-- client/core/rs/src/api/read/procedure.rs | 6 +- client/core/rs/src/api/read/repo.rs | 6 +- client/core/rs/src/api/read/schedule.rs | 3 +- client/core/rs/src/api/read/server.rs | 6 +- client/core/rs/src/api/read/stack.rs | 9 ++- client/core/rs/src/api/read/swarm.rs | 6 +- client/core/rs/src/api/read/sync.rs | 6 +- client/core/rs/src/api/read/terminal.rs | 3 +- client/core/rs/src/entities/config/core.rs | 16 ++++- client/core/ts/src/types.ts | 80 +++++++++++++++------- compose/compose.env | 3 + config/core.config.toml | 5 ++ ui/public/client/types.d.ts | 80 +++++++++++++++------- ui/src/components/list-pagination.tsx | 19 +++-- ui/src/pages/containers.tsx | 5 +- 39 files changed, 264 insertions(+), 146 deletions(-) diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index c44df71d5e..ddb2fb4a0c 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -329,7 +329,7 @@ impl ListResources for ServerListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -377,7 +377,7 @@ impl ListResources for SwarmListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -435,7 +435,7 @@ impl ListResources for StackListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false @@ -514,7 +514,7 @@ impl ListResources for DeploymentListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false @@ -594,7 +594,7 @@ impl ListResources for BuildListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false @@ -657,7 +657,7 @@ impl ListResources for RepoListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -710,7 +710,7 @@ impl ListResources for ProcedureListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -763,7 +763,7 @@ impl ListResources for ActionListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -816,7 +816,7 @@ impl ListResources for ResourceSyncListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -866,7 +866,7 @@ impl ListResources for BuilderListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, @@ -903,7 +903,7 @@ impl ListResources for AlerterListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), - limit: Some(100), + limit: None, page: page.saturating_sub(1), sort_by: Default::default(), sort_desc: false, diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index d4823b554c..7206f3d882 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -18,7 +18,7 @@ use crate::{ state::{action_state_cache, action_states}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetAction { async fn resolve( @@ -47,7 +47,7 @@ impl Resolve for ListActions { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { ActionSortBy::Name => resource::ListItemSort::Name, @@ -99,7 +99,7 @@ impl Resolve for ListFullActions { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 25800c0636..97c08d5190 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -17,7 +17,7 @@ use crate::{ state::db_client, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetAlerter { async fn resolve( @@ -45,7 +45,7 @@ impl Resolve for ListAlerters { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { AlerterSortBy::Name => resource::ListItemSort::Name, @@ -85,7 +85,7 @@ impl Resolve for ListFullAlerters { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index bd5a3e36d2..dac73142b3 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -27,7 +27,7 @@ use crate::{ state::{action_states, build_state_cache, db_client}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetBuild { async fn resolve( @@ -56,7 +56,7 @@ impl Resolve for ListBuilds { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { BuildSortBy::Name => resource::ListItemSort::Name, @@ -110,7 +110,7 @@ impl Resolve for ListFullBuilds { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 2b21526e86..b6826c9471 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -17,7 +17,7 @@ use crate::{ state::db_client, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetBuilder { async fn resolve( @@ -45,7 +45,7 @@ impl Resolve for ListBuilders { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { BuilderSortBy::Name => resource::ListItemSort::Name, @@ -90,7 +90,7 @@ impl Resolve for ListFullBuilders { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 280b14ce54..208e918cfb 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -36,7 +36,7 @@ use crate::{ }, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetDeployment { async fn resolve( @@ -66,7 +66,7 @@ impl Resolve for ListDeployments { }; let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { DeploymentSortBy::Name => resource::ListItemSort::Name, @@ -140,7 +140,7 @@ impl Resolve for ListFullDeployments { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs index 233410756e..9a50cd5d6e 100644 --- a/bin/core/src/api/read/docker.rs +++ b/bin/core/src/api/read/docker.rs @@ -26,7 +26,7 @@ use mogh_resolver::Resolve; use periphery_client::api as periphery; use crate::{ - api::read::ReadArgs, + api::read::{ReadArgs, list_limit}, helpers::{periphery_client, query::get_all_tags}, permission::{get_check_permissions, list_resources_for_user}, resource, @@ -100,7 +100,7 @@ impl Resolve for ListAllContainers { .await?; let mut containers = Vec::::new(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); // Match terms case insensitively. let terms = self .terms diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 6c323e1c8c..1f0e1745ef 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -67,6 +67,12 @@ pub struct ReadArgs { pub user: User, } +/// Resolve the page limit for List apis, falling back to the +/// configured `default_pagination_limit` when not provided. +fn list_limit(limit: Option) -> u64 { + limit.unwrap_or(core_config().default_pagination_limit) +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants, @@ -370,6 +376,7 @@ impl Resolve for GetCoreInfo { disable_websocket_reconnect: config.disable_websocket_reconnect, enable_fancy_toml: config.enable_fancy_toml, timezone: config.timezone.clone(), + default_pagination_limit: config.default_pagination_limit, public_key: core_keys().load().public.to_string(), }; Ok(info) diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 145149b456..3c72404a79 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -17,7 +17,7 @@ use crate::{ state::{action_states, procedure_state_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetProcedure { async fn resolve( @@ -46,7 +46,7 @@ impl Resolve for ListProcedures { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { ProcedureSortBy::Name => resource::ListItemSort::Name, @@ -98,7 +98,7 @@ impl Resolve for ListFullProcedures { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index cf4e603692..a99a47f672 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -17,7 +17,7 @@ use crate::{ state::{action_states, repo_state_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetRepo { async fn resolve( @@ -46,7 +46,7 @@ impl Resolve for ListRepos { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { RepoSortBy::Name => resource::ListItemSort::Name, @@ -94,7 +94,7 @@ impl Resolve for ListFullRepos { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index ec74a7bbf0..d4dde51045 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -20,7 +20,7 @@ use crate::{ schedule::get_schedule_item_info, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for ListSchedules { async fn resolve( @@ -148,7 +148,7 @@ impl Resolve for ListSchedules { schedules.sort_by(compare); } - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let skip = limit.saturating_mul(self.page) as usize; let take = if limit == 0 { usize::MAX diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 83eadb8dab..36fe71bd60 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -38,7 +38,7 @@ use crate::{ state::{action_states, db_client, server_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetServersSummary { async fn resolve( @@ -111,7 +111,7 @@ impl Resolve for ListServers { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { ServerSortBy::Name => resource::ListItemSort::Name, @@ -166,7 +166,7 @@ impl Resolve for ListFullServers { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index f4006d6709..4d66d04d75 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -35,7 +35,7 @@ use crate::{ state::{action_states, stack_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetStack { async fn resolve( @@ -65,7 +65,7 @@ impl Resolve for ListStacks { }; let only_update_available = self.query.specific.update_available; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { StackSortBy::Name => resource::ListItemSort::Name, @@ -147,7 +147,7 @@ impl Resolve for ListFullStacks { get_all_tags(None).await? }; let states = self.query.specific.states.clone(); - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user_filtered::( self.query, @@ -224,7 +224,7 @@ impl Resolve for ListAllStackServices { let mut services = Vec::::new(); let mut skipped = 0; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let limit_usize = limit as usize; // Eg. page 1 skips until after 100 services, page 2 after 200. let skip = limit.saturating_mul(self.page); diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index b269deb89f..05a574a5ac 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -17,7 +17,7 @@ use crate::{ state::{action_states, server_status_cache, swarm_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetSwarm { async fn resolve( @@ -45,7 +45,7 @@ impl Resolve for ListSwarms { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { SwarmSortBy::Name => resource::ListItemSort::Name, @@ -87,7 +87,7 @@ impl Resolve for ListFullSwarms { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 5abf638195..57335d63c0 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -16,7 +16,7 @@ use crate::{ resource, state::action_states, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetResourceSync { async fn resolve( @@ -44,7 +44,7 @@ impl Resolve for ListResourceSyncs { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let sort_by: resource::ListItemSort = match self.sort_by { ResourceSyncSortBy::Name => resource::ListItemSort::Name, @@ -106,7 +106,7 @@ impl Resolve for ListFullResourceSyncs { } else { get_all_tags(None).await? }; - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index 5a8a75a4cf..f7ebe89983 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -5,9 +5,7 @@ use futures_util::{ FutureExt, StreamExt as _, stream::FuturesUnordered, }; use komodo_client::{ - api::read::{ - DEFAULT_LIST_LIMIT, ListTerminals, ListTerminalsResponse, - }, + api::read::{ListTerminals, ListTerminalsResponse}, entities::{ deployment::Deployment, permission::PermissionLevel, @@ -26,7 +24,7 @@ use crate::{ resource, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; // @@ -132,7 +130,7 @@ impl Resolve for ListTerminals { terminals.sort_by(compare); } - let limit = self.limit.unwrap_or(DEFAULT_LIST_LIMIT); + let limit = list_limit(self.limit); let skip = limit.saturating_mul(self.page) as usize; let take = if limit == 0 { usize::MAX diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index c08d79894d..31cc5e363d 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -348,6 +348,9 @@ pub fn core_config() -> &'static CoreConfig { transparent_mode: env .komodo_transparent_mode .unwrap_or(config.transparent_mode), + default_pagination_limit: env + .komodo_default_pagination_limit + .unwrap_or(config.default_pagination_limit), ui_write_disabled: env .komodo_ui_write_disabled .unwrap_or(config.ui_write_disabled), diff --git a/client/core/rs/src/api/read/action.rs b/client/core/rs/src/api/read/action.rs index 8d59db871f..e5bd65e6ef 100644 --- a/client/core/rs/src/api/read/action.rs +++ b/client/core/rs/src/api/read/action.rs @@ -74,7 +74,8 @@ pub struct ListActions { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -128,7 +129,8 @@ pub struct ListFullActions { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs index 4b27583f2e..5905372f60 100644 --- a/client/core/rs/src/api/read/alerter.rs +++ b/client/core/rs/src/api/read/alerter.rs @@ -71,7 +71,8 @@ pub struct ListAlerters { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -125,7 +126,8 @@ pub struct ListFullAlerters { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs index 2a2992420b..6447a9d3a9 100644 --- a/client/core/rs/src/api/read/build.rs +++ b/client/core/rs/src/api/read/build.rs @@ -75,7 +75,8 @@ pub struct ListBuilds { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -129,7 +130,8 @@ pub struct ListFullBuilds { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs index db98323fb9..7c18618a13 100644 --- a/client/core/rs/src/api/read/builder.rs +++ b/client/core/rs/src/api/read/builder.rs @@ -70,7 +70,8 @@ pub struct ListBuilders { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -123,7 +124,8 @@ pub struct ListFullBuilders { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index fe3e773ca4..7f252f2331 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -80,7 +80,8 @@ pub struct ListDeployments { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -135,7 +136,8 @@ pub struct ListFullDeployments { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index f70148e625..6d27798b5a 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -110,7 +110,8 @@ pub struct ListAllContainers { pub page: U64, /// Set the limit for number of containers per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 4be53fdf97..a3b3e335d9 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -53,7 +53,7 @@ pub use user_group::*; pub use variable::*; use crate::entities::{ - ResourceTarget, Timelength, + ResourceTarget, Timelength, U64, config::{GitProvider, ImageRegistry}, }; @@ -64,10 +64,6 @@ pub trait KomodoReadRequest: HasResponse {} // -pub const DEFAULT_LIST_LIMIT: u64 = 100; - -// - #[cfg(feature = "utoipa")] #[utoipa::path( post, @@ -150,6 +146,8 @@ pub struct GetCoreInfoResponse { pub timezone: String, /// Public key for Core / Periphery authentication. pub public_key: String, + /// Default pagination limit for the UI to use. + pub default_pagination_limit: U64, } // diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs index ab6272cb1b..2e031950cd 100644 --- a/client/core/rs/src/api/read/procedure.rs +++ b/client/core/rs/src/api/read/procedure.rs @@ -74,7 +74,8 @@ pub struct ListProcedures { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -128,7 +129,8 @@ pub struct ListFullProcedures { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs index 8ffe9c6a52..3b9db0290a 100644 --- a/client/core/rs/src/api/read/repo.rs +++ b/client/core/rs/src/api/read/repo.rs @@ -73,7 +73,8 @@ pub struct ListRepos { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -127,7 +128,8 @@ pub struct ListFullRepos { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/schedule.rs b/client/core/rs/src/api/read/schedule.rs index bf87026a1e..fdefe22ae2 100644 --- a/client/core/rs/src/api/read/schedule.rs +++ b/client/core/rs/src/api/read/schedule.rs @@ -54,7 +54,8 @@ pub struct ListSchedules { pub page: U64, /// Set the limit for number of schedules per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index c9ca29b9c1..d302240f8a 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -77,7 +77,8 @@ pub struct ListServers { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -131,7 +132,8 @@ pub struct ListFullServers { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 00e6bfcf8c..83d72fd457 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -78,7 +78,8 @@ pub struct ListStacks { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -132,7 +133,8 @@ pub struct ListFullStacks { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -220,7 +222,8 @@ pub struct ListAllStackServices { pub page: U64, /// Set the limit for number of services per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index cdba961a91..eb71c764ef 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -84,7 +84,8 @@ pub struct ListSwarms { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -138,7 +139,8 @@ pub struct ListFullSwarms { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/sync.rs b/client/core/rs/src/api/read/sync.rs index b8521c6a3f..ebd60848a6 100644 --- a/client/core/rs/src/api/read/sync.rs +++ b/client/core/rs/src/api/read/sync.rs @@ -74,7 +74,8 @@ pub struct ListResourceSyncs { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// @@ -128,7 +129,8 @@ pub struct ListFullResourceSyncs { pub page: U64, /// Set the limit for number of resources per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/api/read/terminal.rs b/client/core/rs/src/api/read/terminal.rs index ba3ae82853..8bde511a1e 100644 --- a/client/core/rs/src/api/read/terminal.rs +++ b/client/core/rs/src/api/read/terminal.rs @@ -49,7 +49,8 @@ pub struct ListTerminals { pub page: U64, /// Set the limit for number of terminals per-page. - /// `limit: 100` is default. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). /// /// Passing `limit: 0` returns all results (unlimited). /// diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 8a9678514a..3f5bef257a 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -118,6 +118,8 @@ pub struct Env { /// Override `transparent_mode` pub komodo_transparent_mode: Option, + /// Override `default_pagination_limit` + pub komodo_default_pagination_limit: Option, /// Override `ui_write_disabled` pub komodo_ui_write_disabled: Option, /// Override `enable_new_users` @@ -397,6 +399,12 @@ pub struct CoreConfig { #[serde(default)] pub timezone: String, + /// Set the default pagination limit for the API and UI to use. + /// Default: 30 + /// Recommended: 100 or less + #[serde(default = "default_default_pagination_limit")] + pub default_pagination_limit: u64, + /// Disable user ability to use the UI to update resource configuration. #[serde(default)] pub ui_write_disabled: bool, @@ -814,6 +822,10 @@ fn default_private_key() -> String { String::from("file:/config/keys/core.key") } +fn default_default_pagination_limit() -> u64 { + 30 +} + fn default_ui_path() -> String { "/app/ui".to_string() } @@ -894,10 +906,11 @@ impl Default for CoreConfig { port: default_core_port(), bind_ip: default_core_bind_ip(), internet_interface: Default::default(), - private_key: Default::default(), + private_key: default_private_key(), periphery_public_keys: Default::default(), passkey: Default::default(), timezone: Default::default(), + default_pagination_limit: default_default_pagination_limit(), ui_write_disabled: Default::default(), disable_confirm_dialog: Default::default(), disable_websocket_reconnect: Default::default(), @@ -983,6 +996,7 @@ impl CoreConfig { periphery_public_keys: config.periphery_public_keys, passkey: config.passkey.as_deref().map(empty_or_redacted), timezone: config.timezone, + default_pagination_limit: config.default_pagination_limit, first_server_address: config.first_server_address, first_server_name: config.first_server_name, jwt_secret: empty_or_redacted(&config.jwt_secret), diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 05032edfce..e300625cc4 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -8063,6 +8063,8 @@ export interface GetCoreInfoResponse { timezone: string; /** Public key for Core / Periphery authentication. */ public_key: string; + /** Default pagination limit for the UI to use. */ + default_pagination_limit: U64; } /** Get a specific deployment by name or id. Response: [Deployment]. */ @@ -8843,7 +8845,8 @@ export interface ListActions { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8881,7 +8884,8 @@ export interface ListAlerters { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8990,7 +8994,8 @@ export interface ListAllContainers { page?: U64; /** * Set the limit for number of containers per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9030,7 +9035,8 @@ export interface ListAllStackServices { page?: U64; /** * Set the limit for number of services per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9095,7 +9101,8 @@ export interface ListBuilders { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9133,7 +9140,8 @@ export interface ListBuilds { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9232,7 +9240,8 @@ export interface ListDeployments { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9261,7 +9270,8 @@ export interface ListFullActions { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9282,7 +9292,8 @@ export interface ListFullAlerters { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9302,7 +9313,8 @@ export interface ListFullBuilders { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9323,7 +9335,8 @@ export interface ListFullBuilds { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9347,7 +9360,8 @@ export interface ListFullDeployments { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9368,7 +9382,8 @@ export interface ListFullProcedures { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9389,7 +9404,8 @@ export interface ListFullRepos { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9410,7 +9426,8 @@ export interface ListFullResourceSyncs { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9431,7 +9448,8 @@ export interface ListFullServers { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9452,7 +9470,8 @@ export interface ListFullStacks { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9473,7 +9492,8 @@ export interface ListFullSwarms { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9611,7 +9631,8 @@ export interface ListProcedures { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9651,7 +9672,8 @@ export interface ListRepos { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9691,7 +9713,8 @@ export interface ListResourceSyncs { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9741,7 +9764,8 @@ export interface ListSchedules { page?: U64; /** * Set the limit for number of schedules per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9792,7 +9816,8 @@ export interface ListServers { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9838,7 +9863,8 @@ export interface ListStacks { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9939,7 +9965,8 @@ export interface ListSwarms { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -10012,7 +10039,8 @@ export interface ListTerminals { page?: U64; /** * Set the limit for number of terminals per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * diff --git a/compose/compose.env b/compose/compose.env index a3fe6553f8..69f41e72f5 100644 --- a/compose/compose.env +++ b/compose/compose.env @@ -57,6 +57,9 @@ KOMODO_DISABLE_CONFIRM_DIALOG=false ## Disable creating the default Procedures on first startup. KOMODO_DISABLE_INIT_RESOURCES=false +## Change the default pagination limit for the API and UI to use. +KOMODO_DEFAULT_PAGINATION_LIMIT=30 + ## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE KOMODO_WEBHOOK_SECRET=a_random_secret ## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE diff --git a/config/core.config.toml b/config/core.config.toml index 26dadfe06e..cdea94a1c5 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -79,6 +79,11 @@ private_key = "file:/config/keys/core.key" ## Optional, no default. # first_server_address = "" +## Set the default pagination limit for the API and UI to use. +## Env: KOMODO_DEFAULT_PAGINATION_LIMIT +## Default: 30 +default_pagination_limit = 30 + ## Disables write support on resources in the UI. ## This protects users that that would normally have write priviledges during their UI usage, ## when they intend to fully rely on ResourceSyncs to manage config. diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index bb800fa3dd..9e872bb518 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -7731,6 +7731,8 @@ export interface GetCoreInfoResponse { timezone: string; /** Public key for Core / Periphery authentication. */ public_key: string; + /** Default pagination limit for the UI to use. */ + default_pagination_limit: U64; } /** Get a specific deployment by name or id. Response: [Deployment]. */ export interface GetDeployment { @@ -8436,7 +8438,8 @@ export interface ListActions { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8472,7 +8475,8 @@ export interface ListAlerters { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8577,7 +8581,8 @@ export interface ListAllContainers { page?: U64; /** * Set the limit for number of containers per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8616,7 +8621,8 @@ export interface ListAllStackServices { page?: U64; /** * Set the limit for number of services per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8676,7 +8682,8 @@ export interface ListBuilders { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8712,7 +8719,8 @@ export interface ListBuilds { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8803,7 +8811,8 @@ export interface ListDeployments { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8831,7 +8840,8 @@ export interface ListFullActions { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8851,7 +8861,8 @@ export interface ListFullAlerters { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8870,7 +8881,8 @@ export interface ListFullBuilders { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8890,7 +8902,8 @@ export interface ListFullBuilds { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8913,7 +8926,8 @@ export interface ListFullDeployments { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8933,7 +8947,8 @@ export interface ListFullProcedures { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8953,7 +8968,8 @@ export interface ListFullRepos { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8973,7 +8989,8 @@ export interface ListFullResourceSyncs { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -8993,7 +9010,8 @@ export interface ListFullServers { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9013,7 +9031,8 @@ export interface ListFullStacks { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9033,7 +9052,8 @@ export interface ListFullSwarms { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9160,7 +9180,8 @@ export interface ListProcedures { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9198,7 +9219,8 @@ export interface ListRepos { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9236,7 +9258,8 @@ export interface ListResourceSyncs { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9284,7 +9307,8 @@ export interface ListSchedules { page?: U64; /** * Set the limit for number of schedules per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9332,7 +9356,8 @@ export interface ListServers { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9375,7 +9400,8 @@ export interface ListStacks { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9467,7 +9493,8 @@ export interface ListSwarms { page?: U64; /** * Set the limit for number of resources per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * @@ -9536,7 +9563,8 @@ export interface ListTerminals { page?: U64; /** * Set the limit for number of terminals per-page. - * `limit: 100` is default. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). * * Passing `limit: 0` returns all results (unlimited). * diff --git a/ui/src/components/list-pagination.tsx b/ui/src/components/list-pagination.tsx index 88178c34ea..3fb815fc06 100644 --- a/ui/src/components/list-pagination.tsx +++ b/ui/src/components/list-pagination.tsx @@ -1,7 +1,17 @@ +import { useRead } from "@/lib/hooks"; import { Group, Pagination } from "@mantine/core"; -/** Matches the server side default page size on `List` calls. */ -export const RESOURCE_PAGE_SIZE = 100; +/** + * The server side default page size on `List` calls, + * from the Core config `default_pagination_limit`. + */ +export function usePageSize() { + const limit = useRead("GetCoreInfo", {}).data?.default_pagination_limit; + // Fall back to the Core config default while loading. + if (limit === undefined) return 30; + // `limit: 0` disables pagination, so the page is never full. + return limit === 0 ? Infinity : limit; +} /** * Pagination controls for the paginated `List` calls. @@ -17,10 +27,11 @@ export default function ListPagination({ setPage: (page: number) => void; count: number; }) { - if (count < RESOURCE_PAGE_SIZE && page === 0) return null; + const pageSize = usePageSize(); + if (count < pageSize && page === 0) return null; return ( = RESOURCE_PAGE_SIZE ? page + 2 : page + 1} + total={count >= pageSize ? page + 2 : page + 1} value={page + 1} onChange={(page) => setPage(page - 1)} > diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index f4edb35008..13482a13dc 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -15,9 +15,7 @@ import ResourceLink from "@/resources/link"; import { SearchInput } from "mogh_ui"; import TagsFilter from "@/components/tags/filter"; import ResourceMultiSelector from "@/resources/multi-selector"; -import ListPagination, { - RESOURCE_PAGE_SIZE, -} from "@/components/list-pagination"; +import ListPagination from "@/components/list-pagination"; const CONTAINER_SORT_KEYS = Object.values(Types.ContainerSortBy); @@ -59,7 +57,6 @@ export default function Containers() { servers: selectedServers, tags, page, - limit: RESOURCE_PAGE_SIZE, sort_by: sort.sort_by, sort_desc: sort.sort_desc, }, From e3a4b55ea763065d41358dd130c761a67b552b4c Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 15:43:22 -0700 Subject: [PATCH 155/203] deploy 2.3.0-dev-39 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77fbdb0f49..6abb42a218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 605fcb4f8d..aadbf9af08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-38" +version = "2.3.0-dev-39" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 5a53735c402f72e74ccae012aab70bfb132ecf2d Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 16:08:08 -0700 Subject: [PATCH 156/203] omnisearch use keepPreviousData --- ui/src/app/topbar/omni-search/hooks.tsx | 7 +++++- ui/src/lib/hooks.ts | 29 +++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 114b941302..abe3b8719e 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -24,6 +24,7 @@ import { DOCKER_LINK_ICONS } from "@/components/docker/link"; import { Types } from "komodo_client"; import { hexColorByIntention, useDebounce } from "mogh_ui"; import { containerStateIntention, swarmStateIntention } from "@/lib/color"; +import { keepPreviousData } from "@tanstack/react-query"; const ITEM_LIMIT = 7; @@ -69,6 +70,9 @@ export function useOmniSearch(opened: boolean): { refetchInterval: 15_000, // Only fetch when open and there is query typed enabled: opened && !!debouncedTerms.length, + // Keep the previous results visible while fetching after a search + // change to prevent resource flashing. + placeholderData: keepPreviousData, }).data; const servicesQuery: Types.ListAllStackServices = useMemo( @@ -84,8 +88,8 @@ export function useOmniSearch(opened: boolean): { const services = useRead("ListAllStackServices", servicesQuery, { refetchInterval: 15_000, - // Only fetch when open and there is query typed enabled: opened && !!debouncedTerms.length, + placeholderData: keepPreviousData, }).data; const terminalsQuery: Types.ListTerminals = useMemo( @@ -102,6 +106,7 @@ export function useOmniSearch(opened: boolean): { const terminals = useRead("ListTerminals", terminalsQuery, { refetchInterval: 15_000, enabled: opened, + placeholderData: keepPreviousData, }).data; const user = useUser().data; diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 703cdf7914..ba44aea81c 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -8,6 +8,7 @@ import { import { UseMutationOptions, UseQueryOptions, + keepPreviousData, useMutation, useQuery, useQueryClient, @@ -311,7 +312,13 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { + refetchInterval, + enabled, + // Keep the previous results visible while fetching after a search + // change to prevent resource flashing. + placeholderData: keepPreviousData, + }, ).data, Server: useRead( "ListServers", @@ -323,7 +330,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Stack: useRead( "ListStacks", @@ -335,7 +342,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Deployment: useRead( "ListDeployments", @@ -347,7 +354,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Build: useRead( "ListBuilds", @@ -359,7 +366,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Repo: useRead( "ListRepos", @@ -371,7 +378,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Procedure: useRead( "ListProcedures", @@ -383,7 +390,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Action: useRead( "ListActions", @@ -395,7 +402,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Builder: useRead( "ListBuilders", @@ -407,7 +414,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, Alerter: useRead( "ListAlerters", @@ -419,7 +426,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, ResourceSync: useRead( "ListResourceSyncs", @@ -431,7 +438,7 @@ export function useAllResources( }, limit, }, - { refetchInterval, enabled }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, ).data, }; } From d2ddf352564cb42f133a2ccd3680b0368f8ca911 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 16:30:29 -0700 Subject: [PATCH 157/203] Fix omni search auto highlight first item --- ui/src/app/topbar/omni-search/hooks.tsx | 35 ++++++++++++++++++++----- ui/src/app/topbar/omni-search/index.tsx | 8 +++--- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index abe3b8719e..2f5ce5dc5e 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -13,11 +13,11 @@ import { } from "@/lib/utils"; import { RESOURCE_TARGETS, ResourceComponents } from "@/resources"; import { - spotlight, + createSpotlight, SpotlightActionData, SpotlightActionGroupData, } from "@mantine/spotlight"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { TemplateMarker } from "@/components/template-marker"; import { DOCKER_LINK_ICONS } from "@/components/docker/link"; @@ -28,11 +28,10 @@ import { keepPreviousData } from "@tanstack/react-query"; const ITEM_LIMIT = 7; -export function useOmniSearch(opened: boolean): { - search: string; - setSearch: (value: string) => void; - actions: SpotlightActionGroupData[]; -} { +// Use a dedicated store to allow selection control below. +const [spotlightStore, spotlight] = createSpotlight(); + +export function useOmniSearch(opened: boolean) { const navigate = useNavigate(); const nav = useCallback( (to: string) => { @@ -303,6 +302,26 @@ export function useOmniSearch(opened: boolean): { setSettingsView, ]); + useEffect(() => { + // Mantine only auto selects the first action when the query changes, + // but the omni search actions settle later (debounce + fetch), + // which can leave nothing selected. Select the first action + // whenever newly settled actions render. + const { listId } = spotlightStore.getState(); + const list = listId ? document.getElementById(listId) : null; + if (!list) return; + list.querySelector("[data-selected]")?.removeAttribute("data-selected"); + const first = list.querySelector("[data-action]"); + if (first) { + first.setAttribute("data-selected", "true"); + first.scrollIntoView({ block: "nearest" }); + } + spotlightStore.updateState((state) => ({ + ...state, + selected: first ? 0 : -1, + })); + }, [_actions]); + // LIMIT the action count for performance. let count = 0; const actions: SpotlightActionGroupData[] = []; @@ -327,6 +346,8 @@ export function useOmniSearch(opened: boolean): { search, setSearch, actions, + spotlight, + spotlightStore, }; } diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index 04a05d987d..45a0908299 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -1,5 +1,5 @@ import { ActionIcon, Badge, Button, Group, Switch, Text } from "@mantine/core"; -import { Spotlight, spotlight } from "@mantine/spotlight"; +import { Spotlight } from "@mantine/spotlight"; import { useOmniSearch } from "./hooks"; import { ICONS } from "@/lib/icons"; import { useShiftKeyListener } from "mogh_ui"; @@ -8,10 +8,9 @@ import { useLocalStorage } from "@mantine/hooks"; import { useState } from "react"; export default function OmniSearch({}: {}) { - // Gates all omni search queries, so nothing is fetched / polled - // until the spotlight is actually opened. const [opened, setOpened] = useState(false); - const { search, setSearch, actions } = useOmniSearch(opened); + const { search, setSearch, actions, spotlight, spotlightStore } = + useOmniSearch(opened); useShiftKeyListener("S", () => spotlight.open()); const [clearOnClose, setClearOnClose] = useLocalStorage({ key: "omnisearch.clearOnClose", @@ -46,6 +45,7 @@ export default function OmniSearch({}: {}) { Date: Sun, 12 Jul 2026 17:49:24 -0700 Subject: [PATCH 158/203] drop resource action guard / reset action state before pushing updates to clients for requery --- bin/core/src/api/execute/action.rs | 6 +- bin/core/src/api/execute/build.rs | 6 +- bin/core/src/api/execute/deployment.rs | 48 +++++++++-- bin/core/src/api/execute/procedure.rs | 6 +- bin/core/src/api/execute/repo.rs | 24 +++++- bin/core/src/api/execute/server.rs | 108 ++++++++++++++++++++----- bin/core/src/api/execute/stack.rs | 24 +++++- bin/core/src/api/execute/sync.rs | 10 ++- bin/core/src/stack/execute.rs | 6 +- ui/src/lib/socket.tsx | 2 + ui/src/pages/dashboard/active.tsx | 12 +-- 11 files changed, 208 insertions(+), 44 deletions(-) diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index b3195a0638..8d602ef326 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -125,7 +125,7 @@ impl Resolve for RunAction { // This will set action state back to default when dropped. // Will also check to ensure action not already busy before updating. - let _action_guard = action_state.update_custom( + let action_guard = action_state.update_custom( |state| state.running += 1, |state| state.running -= 1, false, @@ -248,6 +248,10 @@ impl Resolve for RunAction { action_cancel_cache().remove(&update.id).await; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + res?; // Need to manually update the update before cache refresh, diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index 8ecaf99a5c..9b5efc059e 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -153,7 +153,7 @@ impl Resolve for RunBuild { // This will set action state back to default when dropped. // Will also check to ensure build not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.building = true)?; if build.config.auto_increment_version { @@ -376,6 +376,10 @@ impl Resolve for RunBuild { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index 9e50cac9b7..95e72f6b3b 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -115,7 +115,7 @@ impl Resolve for Deploy { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -286,6 +286,11 @@ impl Resolve for Deploy { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + update_update(update.clone()).await?; Ok(update) @@ -466,7 +471,7 @@ impl Resolve for PullDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -477,6 +482,10 @@ impl Resolve for PullDeployment { update.logs.push(log); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -524,7 +533,7 @@ impl Resolve for StartDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.starting = true)?; let mut update = update.clone(); @@ -549,6 +558,10 @@ impl Resolve for StartDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -596,7 +609,7 @@ impl Resolve for RestartDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.restarting = true)?; let mut update = update.clone(); @@ -623,6 +636,10 @@ impl Resolve for RestartDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -670,7 +687,7 @@ impl Resolve for PauseDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pausing = true)?; let mut update = update.clone(); @@ -695,6 +712,10 @@ impl Resolve for PauseDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -742,7 +763,7 @@ impl Resolve for UnpauseDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.unpausing = true)?; let mut update = update.clone(); @@ -769,6 +790,10 @@ impl Resolve for UnpauseDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -818,7 +843,7 @@ impl Resolve for StopDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.stopping = true)?; let mut update = update.clone(); @@ -851,6 +876,10 @@ impl Resolve for StopDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -932,7 +961,7 @@ impl Resolve for DestroyDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.destroying = true)?; let mut update = update.clone(); @@ -1017,6 +1046,9 @@ impl Resolve for DestroyDeployment { update.logs.push(log); update.finalize(); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index 4b73de96fc..b0f22cc86d 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -124,7 +124,7 @@ fn resolve_inner( // This will set action state back to default when dropped. // Will also check to ensure procedure not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.running = true)?; update_update(update.clone()).await?; @@ -160,6 +160,10 @@ fn resolve_inner( update.finalize(); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs index 214e5d5f53..3eeb0be30f 100644 --- a/bin/core/src/api/execute/repo.rs +++ b/bin/core/src/api/execute/repo.rs @@ -108,7 +108,7 @@ impl Resolve for CloneRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.cloning = true)?; let mut update = update.clone(); @@ -179,6 +179,10 @@ impl Resolve for CloneRepo { ); }; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + handle_repo_update_return(update).await } } @@ -248,7 +252,7 @@ impl Resolve for PullRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -323,6 +327,10 @@ impl Resolve for PullRepo { ); }; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + handle_repo_update_return(update).await } } @@ -438,7 +446,7 @@ impl Resolve for BuildRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.building = true)?; let mut update = update.clone(); @@ -516,6 +524,9 @@ impl Resolve for BuildRepo { "get builder", format_serror(&e.context("failed to get builder").into()), )); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); return handle_builder_early_return( update, repo.id, repo.name, false, ) @@ -548,6 +559,9 @@ impl Resolve for BuildRepo { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; info!("builder cleaned up"); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); return handle_builder_early_return(update, repo.id, repo.name, true).await }, }; @@ -595,6 +609,10 @@ impl Resolve for BuildRepo { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, diff --git a/bin/core/src/api/execute/server.rs b/bin/core/src/api/execute/server.rs index 3efdb30561..c019375a12 100644 --- a/bin/core/src/api/execute/server.rs +++ b/bin/core/src/api/execute/server.rs @@ -56,7 +56,7 @@ impl Resolve for StartContainer { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.starting_containers = true)?; let mut update = update.clone(); @@ -83,6 +83,10 @@ impl Resolve for StartContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -124,7 +128,7 @@ impl Resolve for RestartContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.restarting_containers = true)?; let mut update = update.clone(); @@ -153,6 +157,10 @@ impl Resolve for RestartContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -194,7 +202,7 @@ impl Resolve for PauseContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pausing_containers = true)?; let mut update = update.clone(); @@ -221,6 +229,10 @@ impl Resolve for PauseContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -262,7 +274,7 @@ impl Resolve for UnpauseContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.unpausing_containers = true)?; let mut update = update.clone(); @@ -291,6 +303,10 @@ impl Resolve for UnpauseContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -334,7 +350,7 @@ impl Resolve for StopContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.stopping_containers = true)?; let mut update = update.clone(); @@ -363,6 +379,10 @@ impl Resolve for StopContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -412,7 +432,7 @@ impl Resolve for DestroyContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_containers = true)?; let mut update = update.clone(); @@ -443,6 +463,10 @@ impl Resolve for DestroyContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -483,7 +507,7 @@ impl Resolve for StartAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.starting_containers = true)?; let mut update = update.clone(); @@ -507,6 +531,10 @@ impl Resolve for StartAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -547,7 +575,7 @@ impl Resolve for RestartAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.restarting_containers = true)?; let mut update = update.clone(); @@ -573,6 +601,10 @@ impl Resolve for RestartAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -613,7 +645,7 @@ impl Resolve for PauseAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pausing_containers = true)?; let mut update = update.clone(); @@ -637,6 +669,10 @@ impl Resolve for PauseAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -677,7 +713,7 @@ impl Resolve for UnpauseAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.unpausing_containers = true)?; let mut update = update.clone(); @@ -703,6 +739,10 @@ impl Resolve for UnpauseAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -743,7 +783,7 @@ impl Resolve for StopAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state + let action_guard = action_state .update(|state| state.stopping_containers = true)?; let mut update = update.clone(); @@ -767,6 +807,10 @@ impl Resolve for StopAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -807,7 +851,7 @@ impl Resolve for PruneContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_containers = true)?; let mut update = update.clone(); @@ -836,6 +880,10 @@ impl Resolve for PruneContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -941,7 +989,7 @@ impl Resolve for PruneNetworks { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_networks = true)?; let mut update = update.clone(); @@ -968,6 +1016,10 @@ impl Resolve for PruneNetworks { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1070,7 +1122,7 @@ impl Resolve for PruneImages { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_images = true)?; let mut update = update.clone(); @@ -1095,6 +1147,10 @@ impl Resolve for PruneImages { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1200,7 +1256,7 @@ impl Resolve for PruneVolumes { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_volumes = true)?; let mut update = update.clone(); @@ -1225,6 +1281,10 @@ impl Resolve for PruneVolumes { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1265,7 +1325,7 @@ impl Resolve for PruneDockerBuilders { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_builders = true)?; let mut update = update.clone(); @@ -1290,6 +1350,10 @@ impl Resolve for PruneDockerBuilders { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1330,7 +1394,7 @@ impl Resolve for PruneBuildx { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_buildx = true)?; let mut update = update.clone(); @@ -1355,6 +1419,10 @@ impl Resolve for PruneBuildx { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1395,7 +1463,7 @@ impl Resolve for PruneSystem { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_system = true)?; let mut update = update.clone(); @@ -1419,6 +1487,10 @@ impl Resolve for PruneSystem { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index 27442ef5e4..ec710f6a0d 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -136,7 +136,7 @@ impl Resolve for DeployStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -330,6 +330,10 @@ impl Resolve for DeployStack { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -955,7 +959,7 @@ impl Resolve for PullStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -972,6 +976,10 @@ impl Resolve for PullStack { update.logs.extend(res.logs); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1231,7 +1239,7 @@ impl Resolve for DestroyStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.destroying = true)?; let mut update = update.clone(); @@ -1261,6 +1269,10 @@ impl Resolve for DestroyStack { refresh_swarm_cache(&swarm, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1331,7 +1343,7 @@ impl Resolve for RunStackService { let action_state = action_states().stack.get_or_insert_default(&stack.id).await; - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -1391,6 +1403,10 @@ impl Resolve for RunStackService { update.logs.push(log); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/sync.rs b/bin/core/src/api/execute/sync.rs index 7273b1aeae..5e0c709938 100644 --- a/bin/core/src/api/execute/sync.rs +++ b/bin/core/src/api/execute/sync.rs @@ -98,7 +98,7 @@ impl Resolve for RunSync { // This will set action state back to default when dropped. // Will also check to ensure sync not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.syncing = true)?; let mut update = update.clone(); @@ -311,6 +311,10 @@ impl Resolve for RunSync { ), ); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; return Ok(update); } @@ -442,6 +446,10 @@ impl Resolve for RunSync { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/stack/execute.rs b/bin/core/src/stack/execute.rs index 265687aefe..2f48cd665b 100644 --- a/bin/core/src/stack/execute.rs +++ b/bin/core/src/stack/execute.rs @@ -80,7 +80,7 @@ pub async fn execute_compose_with_stack_and_server< // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state.update(set_in_progress)?; + let action_guard = action_state.update(set_in_progress)?; // Send update here for UI to recheck action state update_update(update.clone()).await?; @@ -105,6 +105,10 @@ pub async fn execute_compose_with_stack_and_server< refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/ui/src/lib/socket.tsx b/ui/src/lib/socket.tsx index 2d4ba1bb56..b021ad773b 100644 --- a/ui/src/lib/socket.tsx +++ b/ui/src/lib/socket.tsx @@ -201,6 +201,7 @@ function onUpdate( if (["Server", "Deployment", "Stack"].includes(update.target.type)) { invalidate( ["ListContainers"], + ["ListAllContainers"], ["InspectContainer"], ["ListNetworks"], ["InspectNetwork"], @@ -265,6 +266,7 @@ function onUpdate( if (update.target.type === "Deployment") { invalidate( ["ListDeployments"], + ["ListFullDeployments"], ["GetDeploymentsSummary"], ["ListContainers"], ["ListNetworks"], diff --git a/ui/src/pages/dashboard/active.tsx b/ui/src/pages/dashboard/active.tsx index bb5dfcde87..cbd90c229a 100644 --- a/ui/src/pages/dashboard/active.tsx +++ b/ui/src/pages/dashboard/active.tsx @@ -33,7 +33,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const deployments = useRead( @@ -50,7 +50,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const builds = useRead( @@ -61,7 +61,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const repos = useRead( @@ -78,7 +78,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const procedures = useRead( @@ -89,7 +89,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const actions = useRead( @@ -100,7 +100,7 @@ export default function DashboardActiveResources() { }, limit: 0, }, - { refetchInterval: 15_000 }, + { refetchInterval: 10_000 }, ).data ?? []; const globalAutoUpdates = useRead("ListUpdates", { From 9e05db4edf630d40e073ea053645cd0c69ce6744 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 12 Jul 2026 17:50:02 -0700 Subject: [PATCH 159/203] deploy 2.3.0-dev-40 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6abb42a218..a3282f368e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "async_timing_util", @@ -3453,7 +3453,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "arc-swap", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "arc-swap", @@ -4642,7 +4642,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "encoding", @@ -7001,7 +7001,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "axum", @@ -8017,7 +8017,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index aadbf9af08..bb6797b05c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-39" +version = "2.3.0-dev-40" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 0d2318a40a1b11f16236fed3ad6bccdc3c7993ac Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 13 Jul 2026 18:15:02 -0700 Subject: [PATCH 160/203] builder instance type use divided children --- ui/src/resources/builder/instance-type.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/src/resources/builder/instance-type.tsx b/ui/src/resources/builder/instance-type.tsx index f3e77211b1..a1dac466a5 100644 --- a/ui/src/resources/builder/instance-type.tsx +++ b/ui/src/resources/builder/instance-type.tsx @@ -2,13 +2,18 @@ import { Bot } from "lucide-react"; import { useBuilder } from "."; import ResourceLink from "../link"; import { Group } from "@mantine/core"; +import { DividedChildren } from "mogh_ui"; export default function BuilderInstanceType({ id }: { id: string }) { let info = useBuilder(id)?.info; if (info?.builder_type === "Server") { - return info.instance_type - ?.split(",") - .map((id) => ); + return ( + + {info.instance_type?.split(",").map((id) => ( + + ))} + + ); } else { return ( From 687440782d69331ed6b075a85261bb892a67ebed Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 13 Jul 2026 18:32:19 -0700 Subject: [PATCH 161/203] default pagination limit 50 --- bin/cli/src/command/container.rs | 2 +- client/core/rs/src/entities/config/core.rs | 5 ++--- compose/compose.env | 2 +- config/core.config.toml | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index a07063491b..5f037b1b82 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -156,7 +156,7 @@ pub async fn inspect_container( tags: Default::default(), terms: vec![inspect.container.clone()], state: Default::default(), - limit: Some(300), + limit: Some(0), page: 0, sort_by: ContainerSortBy::Server, sort_desc: false, diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 3f5bef257a..f776a2e28e 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -400,8 +400,7 @@ pub struct CoreConfig { pub timezone: String, /// Set the default pagination limit for the API and UI to use. - /// Default: 30 - /// Recommended: 100 or less + /// Default: 50 #[serde(default = "default_default_pagination_limit")] pub default_pagination_limit: u64, @@ -823,7 +822,7 @@ fn default_private_key() -> String { } fn default_default_pagination_limit() -> u64 { - 30 + 50 } fn default_ui_path() -> String { diff --git a/compose/compose.env b/compose/compose.env index 69f41e72f5..428556d232 100644 --- a/compose/compose.env +++ b/compose/compose.env @@ -58,7 +58,7 @@ KOMODO_DISABLE_CONFIRM_DIALOG=false KOMODO_DISABLE_INIT_RESOURCES=false ## Change the default pagination limit for the API and UI to use. -KOMODO_DEFAULT_PAGINATION_LIMIT=30 +KOMODO_DEFAULT_PAGINATION_LIMIT=50 ## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE KOMODO_WEBHOOK_SECRET=a_random_secret diff --git a/config/core.config.toml b/config/core.config.toml index cdea94a1c5..f8c949efa3 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -81,8 +81,8 @@ private_key = "file:/config/keys/core.key" ## Set the default pagination limit for the API and UI to use. ## Env: KOMODO_DEFAULT_PAGINATION_LIMIT -## Default: 30 -default_pagination_limit = 30 +## Default: 50 +default_pagination_limit = 50 ## Disables write support on resources in the UI. ## This protects users that that would normally have write priviledges during their UI usage, From 0593cc659c44063d70fc2e26ca3028c184d262ca Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 14 Jul 2026 00:14:59 -0700 Subject: [PATCH 162/203] UI: useDebouncedTermSearch hook --- ui/src/lib/hooks.ts | 25 ++++++++++++++++++++++++- ui/src/pages/containers.tsx | 21 +++++++-------------- ui/src/pages/dashboard/tables.tsx | 19 ++++++++----------- ui/src/pages/resources.tsx | 18 ++++++------------ ui/src/pages/schedules.tsx | 26 +++++++++++--------------- ui/src/pages/terminals/index.tsx | 27 ++++++++------------------- ui/src/resources/multi-selector.tsx | 17 +++-------------- 7 files changed, 67 insertions(+), 86 deletions(-) diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index ba44aea81c..68ef41eccf 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -13,7 +13,7 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { atom, useAtom } from "jotai"; import { atomFamily } from "jotai-family"; import { atomWithHash } from "jotai-location"; @@ -25,6 +25,7 @@ import { termMatchesTypeKeyword, } from "@/lib/utils"; import { notifications } from "@mantine/notifications"; +import { useDebounce } from "mogh_ui"; export function komodo_client() { return KomodoClient(KOMODO_BASE_URL, { @@ -182,6 +183,28 @@ export function atomWithStorage(key: string, init: T) { ); } +export function useDebouncedTermSearch(props?: { + debounce?: number; + onUpdate?: () => void; +}) { + const { debounce = 200, onUpdate } = props ?? {}; + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, debounce); + const terms = useMemo(() => { + onUpdate?.(); + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); + return { + search, + setSearch, + terms, + }; +} + export function useResourceParamType() { const type = useParams().type; if (!type) return undefined; diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 13482a13dc..34a4928d8e 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -1,11 +1,11 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; -import { useRead, useTagsFilter } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead, useTagsFilter } from "@/lib/hooks"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; -import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; import { Group, Stack } from "@mantine/core"; @@ -23,17 +23,10 @@ export default function Containers() { const [selectedServers, setSelectedServers] = useState([]); const [page, setPage] = useState(0); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const searchTerms = useMemo(() => { - // back to first page if search changes - setPage(0); - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); const tags = useTagsFilter(); @@ -53,7 +46,7 @@ export default function Containers() { useRead( "ListAllContainers", { - terms: searchTerms, + terms, servers: selectedServers, tags, page, diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 9db5335ac1..0bb5ae3787 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -1,6 +1,11 @@ import TagsFilter from "@/components/tags/filter"; import { keepPreviousData } from "@tanstack/react-query"; -import { useRead, useTagsFilter, useTemplatesQueryBehavior } from "@/lib/hooks"; +import { + useDebouncedTermSearch, + useRead, + useTagsFilter, + useTemplatesQueryBehavior, +} from "@/lib/hooks"; import { usableResourcePath } from "@/lib/utils"; import { RequiredResourceComponents, @@ -8,7 +13,7 @@ import { UsableResource, } from "@/resources"; import { ICONS } from "@/lib/icons"; -import { Section, useDebounce } from "mogh_ui"; +import { Section } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; import { useEffect, useMemo, useState } from "react"; import ListPagination from "@/components/list-pagination"; @@ -18,15 +23,7 @@ import { ShowHideButton } from "mogh_ui"; import { SearchInput } from "mogh_ui"; export default function DashboardTables() { - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo(() => { - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + const { search, setSearch, terms } = useDebouncedTermSearch(); const Tables = useMemo( () => diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 47e362d41a..45e51678d9 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { + useDebouncedTermSearch, useFilterByUpdateAvailable, useRead, useResourceParamType, @@ -10,7 +11,7 @@ import { } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; -import { Page, useDebounce } from "mogh_ui"; +import { Page } from "mogh_ui"; import { Group, Stack } from "@mantine/core"; import ListPagination from "@/components/list-pagination"; import { TableSkeleton } from "mogh_ui"; @@ -34,17 +35,10 @@ export default function Resources({ _type }: { _type?: UsableResource }) { useSetTitle(name + "s"); const [page, setPage] = useState(0); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo(() => { - // Set to page 0 whenever search changes - setPage(0); - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); const [filterUpdateAvailable, toggleFilterUpdateAvailable] = useFilterByUpdateAvailable(); diff --git a/ui/src/pages/schedules.tsx b/ui/src/pages/schedules.tsx index 4ded5e1c38..abc05fe87f 100644 --- a/ui/src/pages/schedules.tsx +++ b/ui/src/pages/schedules.tsx @@ -2,42 +2,38 @@ import TagsFilter from "@/components/tags/filter"; import TableTags from "@/components/tags/table"; import ListPagination from "@/components/list-pagination"; import { + useDebouncedTermSearch, usePermissions, useRead, useSetTitle, - useTags, + useTagsFilter, useWrite, } from "@/lib/hooks"; import { UsableResource } from "@/resources"; import ResourceLink from "@/resources/link"; import { ICONS } from "@/lib/icons"; -import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { SearchInput } from "mogh_ui"; import { Group, Stack, Switch } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; const SCHEDULE_SORT_KEYS = Object.values(Types.ScheduleSortBy); export default function Schedules() { useSetTitle("Schedules"); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); - const { tags } = useTags(); const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + + const tags = useTagsFilter(); + // Server side sort, passed up from the table. const [sort, setSort] = useState<{ sort_by?: Types.ScheduleSortBy; diff --git a/ui/src/pages/terminals/index.tsx b/ui/src/pages/terminals/index.tsx index 487cf5e8c8..fab32f949b 100644 --- a/ui/src/pages/terminals/index.tsx +++ b/ui/src/pages/terminals/index.tsx @@ -1,19 +1,14 @@ import TerminalTargetLink from "@/pages/terminals/target-link"; import ListPagination from "@/components/list-pagination"; -import { useRead, useSetTitle } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead, useSetTitle } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { terminalLink } from "@/lib/utils"; -import { - DataTable, - fmtDateWithMinutes, - SortableHeader, - useDebounce, -} from "mogh_ui"; +import { DataTable, fmtDateWithMinutes, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import DeleteTerminal from "./delete"; import BatchDeleteAllTerminals from "./batch-delete"; @@ -24,19 +19,13 @@ const TERMINAL_SORT_KEYS = Object.values(Types.TerminalSortBy); export default function Terminals() { useSetTitle("Terminals"); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + // Server side sort, passed up from the table. const [sort, setSort] = useState<{ sort_by?: Types.TerminalSortBy; diff --git a/ui/src/resources/multi-selector.tsx b/ui/src/resources/multi-selector.tsx index bad14a4ae7..7e815be63b 100644 --- a/ui/src/resources/multi-selector.tsx +++ b/ui/src/resources/multi-selector.tsx @@ -1,9 +1,8 @@ -import { useRead } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead } from "@/lib/hooks"; import { keepPreviousData } from "@tanstack/react-query"; import { MultiSelect } from "@mantine/core"; import { Types } from "komodo_client"; -import { useDebounce } from "mogh_ui"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { UsableResource } from "."; import { fmtResourceType } from "@/lib/formatting"; @@ -25,17 +24,7 @@ export default function ResourceMultiSelector({ templates?: Types.TemplatesQueryBehavior; placeholder?: string; }) { - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); + const { search, setSearch, terms } = useDebouncedTermSearch(); const resources = useRead( From 2b96558d07b721330cacc84bc27a6efb2652c320 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 14 Jul 2026 00:35:39 -0700 Subject: [PATCH 163/203] stats page --- ui/src/app/sidebar.tsx | 6 + ui/src/app/topbar/omni-search/hooks.tsx | 6 + ui/src/lib/hooks.ts | 8 +- ui/src/pages/stats.tsx | 532 ++++++++++++++++++++++++ ui/src/router.tsx | 2 + 5 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 ui/src/pages/stats.tsx diff --git a/ui/src/app/sidebar.tsx b/ui/src/app/sidebar.tsx index bfff9c1f67..998bcb0092 100644 --- a/ui/src/app/sidebar.tsx +++ b/ui/src/app/sidebar.tsx @@ -31,6 +31,12 @@ const Sidebar = ({ close }: { close: () => void }) => { to="/terminals" {...linkProps} /> + } + to="/stats" + {...linkProps} + /> , onClick: () => nav("/terminals"), }, + { + id: "Stats", + label: "Stats", + leftSection: , + onClick: () => nav("/stats"), + }, { id: "Schedules", label: "Schedules", diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 68ef41eccf..0562f11600 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -26,6 +26,7 @@ import { } from "@/lib/utils"; import { notifications } from "@mantine/notifications"; import { useDebounce } from "mogh_ui"; +import { useLocalStorage } from "@mantine/hooks"; export function komodo_client() { return KomodoClient(KOMODO_BASE_URL, { @@ -186,9 +187,12 @@ export function atomWithStorage(key: string, init: T) { export function useDebouncedTermSearch(props?: { debounce?: number; onUpdate?: () => void; + localStorageKey?: string; }) { - const { debounce = 200, onUpdate } = props ?? {}; - const [search, setSearch] = useState(""); + const { debounce = 200, onUpdate, localStorageKey } = props ?? {}; + const [search, setSearch] = localStorageKey + ? useLocalStorage({ key: localStorageKey, defaultValue: "" }) + : useState(""); const debouncedSearch = useDebounce(search, debounce); const terms = useMemo(() => { onUpdate?.(); diff --git a/ui/src/pages/stats.tsx b/ui/src/pages/stats.tsx new file mode 100644 index 0000000000..8a17c68f67 --- /dev/null +++ b/ui/src/pages/stats.tsx @@ -0,0 +1,532 @@ +import TagsFilter from "@/components/tags/filter"; +import { + komodo_client, + useDebouncedTermSearch, + useRead, + useSetTitle, + useTagsFilter, +} from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import ResourceMultiSelector from "@/resources/multi-selector"; +import { useStatsGranularity } from "@/resources/server/hooks"; +import StatsServerTable from "@/resources/server/table/stats"; +import { + Center, + Group, + Loader, + Select, + SimpleGrid, + Stack, + Text, +} from "@mantine/core"; +import { useLocalStorage } from "@mantine/hooks"; +import { keepPreviousData, useQueries } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { ChartLine, Download, Upload } from "lucide-react"; +import { + fmtSizeBytes, + InfoCard, + Page, + SearchInput, + Section, + StatBar, +} from "mogh_ui"; +import { ReactNode, useMemo } from "react"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +/** + * Fixed categorical palette for per-server chart series (Mantine hues, + * CVD-separation and contrast validated on light and dark surfaces). + * Colors are assigned by position in the full server list, so filtering + * never repaints the remaining servers. + */ +const SERVER_COLORS = [ + "#1c7ed6", // blue.7 + "#e8590c", // orange.8 + "#0ca678", // teal.7 + "#be4bdb", // grape.6 + "#f03e3e", // red.7 + "#1098ad", // cyan.7 + "#7950f2", // violet.6 + "#d6336c", // pink.7 +]; + +/** Beyond this many series, lines become unreadable and colors would repeat. */ +const MAX_CHART_SERVERS = SERVER_COLORS.length; + +type StatType = + | "Cpu" + | "Memory" + | "Disk" + | "Network Ingress" + | "Network Egress" + | "Load Average"; + +const STAT_TYPES: [StatType, ReactNode][] = [ + ["Cpu", ], + ["Memory", ], + ["Disk", ], + ["Load Average", ], + ["Network Ingress", ], + ["Network Egress", ], +]; + +export default function Stats() { + useSetTitle("Stats"); + + // Server ids + const [selectedServers, setSelectedServers] = useLocalStorage({ + key: "stats.selected-servers.v1", + defaultValue: [], + }); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + localStorageKey: "stats.search.v1", + }); + const tags = useTagsFilter(); + + const servers = + useRead( + "ListServers", + { query: { terms, tags }, limit: 0 }, + { refetchInterval: 30_000, placeholderData: keepPreviousData }, + ).data ?? []; + + // Stable color per server, keyed on the full (unfiltered) list + // so the assignment survives filtering. + const serverColors = useMemo(() => { + const colors = new Map(); + servers.forEach((server, i) => + colors.set(server.id, SERVER_COLORS[i % SERVER_COLORS.length]), + ); + return colors; + }, [servers]); + + const filteredServers = useMemo( + () => + selectedServers.length + ? servers.filter((server) => selectedServers.includes(server.name)) + : servers, + [servers, selectedServers], + ); + + const Content = useMemo( + () => + servers.length === 0 ? ( + No Servers found. + ) : ( + <> + + +
}> + +
+ + ), + [servers, filteredServers], + ); + + return ( + + + + + + + + + + + + {Content} + + + ); +} + +function CurrentStatsSummary({ servers }: { servers: Types.ServerListItem[] }) { + const statsQueries = useQueries({ + queries: servers.map((server) => ({ + // Matches the useRead("GetSystemStats", ...) query key, + // so the cache is shared with the server table cells below. + queryKey: ["GetSystemStats", { server: server.id }], + queryFn: () => + komodo_client().read("GetSystemStats", { server: server.id }), + enabled: server.info.state === Types.ServerState.Ok, + refetchInterval: 10_000, + })), + }); + + const okCount = servers.filter( + (server) => server.info.state === Types.ServerState.Ok, + ).length; + const disabledCount = servers.filter( + (server) => server.info.state === Types.ServerState.Disabled, + ).length; + const unreachableCount = servers.length - okCount - disabledCount; + + const stats = statsQueries.flatMap((query) => query.data ?? []); + const cpu = + stats.length > 0 + ? stats.reduce((acc, s) => acc + s.cpu_perc, 0) / stats.length + : 0; + const memUsed = stats.reduce((acc, s) => acc + s.mem_used_gb, 0); + const memTotal = stats.reduce((acc, s) => acc + s.mem_total_gb, 0); + const diskUsed = stats.reduce( + (acc, s) => acc + s.disks.reduce((acc, disk) => acc + disk.used_gb, 0), + 0, + ); + const diskTotal = stats.reduce( + (acc, s) => acc + s.disks.reduce((acc, disk) => acc + disk.total_gb, 0), + 0, + ); + + return ( +
+ + } + w={{ base: "100%", lg: 300 }} + gap="0.2rem" + justify="space-between" + > + + {okCount} of {servers.length} reachable + + + {unreachableCount > 0 && ( + + {unreachableCount} unreachable + + )} + {disabledCount > 0 && ( + + {disabledCount} disabled + + )} + + + } + description={ + <> + Average across {stats.length} reachable server + {stats.length === 1 ? "" : "s"} + + } + percentage={cpu} + warning={75} + critical={90} + /> + } + description={ + memTotal > 0 && ( + <> + {memUsed.toFixed(1)} GB of{" "} + {memTotal.toFixed(1)} GB in use + + ) + } + percentage={memTotal > 0 ? (memUsed / memTotal) * 100 : 0} + warning={75} + critical={90} + /> + } + description={ + diskTotal > 0 && ( + <> + {diskUsed.toFixed(1)} GB of{" "} + {diskTotal.toFixed(1)} GB in use + + ) + } + percentage={diskTotal > 0 ? (diskUsed / diskTotal) * 100 : 0} + warning={75} + critical={90} + /> + +
+ ); +} + +function HistoricalStats({ + servers, + serverColors, +}: { + servers: Types.ServerListItem[]; + serverColors: Map; +}) { + const [granularity, setGranularity] = useStatsGranularity(); + + const chartServers = servers + .filter((server) => server.info.state === Types.ServerState.Ok) + .slice(0, MAX_CHART_SERVERS); + + const historicalQueries = useQueries({ + queries: chartServers.map((server) => ({ + queryKey: [ + "GetHistoricalServerStats", + { server: server.id, granularity }, + ], + queryFn: () => + komodo_client().read("GetHistoricalServerStats", { + server: server.id, + granularity, + }), + refetchInterval: + granularity === Types.Timelength.FiveSeconds + ? 5_000 + : granularity === Types.Timelength.FifteenSeconds + ? 10_000 + : 15_000, + })), + }); + + const series = chartServers.map((server, i) => ({ + name: server.name, + color: serverColors.get(server.id) ?? SERVER_COLORS[0], + stats: historicalQueries[i].data?.stats ?? [], + })); + const isPending = historicalQueries.some((query) => query.isPending); + + return ( +
} + titleRight={ + + setSortBy(s ?? Types.ServerSortBy.LoadAverage)} + data={[ + Types.ServerSortBy.LoadAverage, + Types.ServerSortBy.Cpu, + Types.ServerSortBy.Memory, + Types.ServerSortBy.Disk, + Types.ServerSortBy.Network, + ].map((s) => ({ value: s, label: fmtUpperCamelcase(s) }))} + allowDeselect={false} + leftSection={ + + Sort By: + + } + leftSectionWidth={64} + leftSectionPointerEvents="none" + /> + @@ -165,18 +198,6 @@ export default function Stats() { } function CurrentStatsSummary({ servers }: { servers: Types.ServerListItem[] }) { - const statsQueries = useQueries({ - queries: servers.map((server) => ({ - // Matches the useRead("GetSystemStats", ...) query key, - // so the cache is shared with the server table cells below. - queryKey: ["GetSystemStats", { server: server.id }], - queryFn: () => - komodo_client().read("GetSystemStats", { server: server.id }), - enabled: server.info.state === Types.ServerState.Ok, - refetchInterval: 10_000, - })), - }); - const okCount = servers.filter( (server) => server.info.state === Types.ServerState.Ok, ).length; @@ -185,21 +206,17 @@ function CurrentStatsSummary({ servers }: { servers: Types.ServerListItem[] }) { ).length; const unreachableCount = servers.length - okCount - disabledCount; - const stats = statsQueries.flatMap((query) => query.data ?? []); + // The list items already carry the latest stats, + // no per server stats queries needed. + const stats = servers.flatMap((server) => server.info.stats ?? []); const cpu = stats.length > 0 ? stats.reduce((acc, s) => acc + s.cpu_perc, 0) / stats.length : 0; const memUsed = stats.reduce((acc, s) => acc + s.mem_used_gb, 0); const memTotal = stats.reduce((acc, s) => acc + s.mem_total_gb, 0); - const diskUsed = stats.reduce( - (acc, s) => acc + s.disks.reduce((acc, disk) => acc + disk.used_gb, 0), - 0, - ); - const diskTotal = stats.reduce( - (acc, s) => acc + s.disks.reduce((acc, disk) => acc + disk.total_gb, 0), - 0, - ); + const diskUsed = stats.reduce((acc, s) => acc + s.disk_used_gb, 0); + const diskTotal = stats.reduce((acc, s) => acc + s.disk_total_gb, 0); return (
diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 79968dcf3e..6cad56232d 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -325,7 +325,7 @@ export const ServerComponents: RequiredResourceComponents< if (dontShow) { return null; } - const pending = isPending || starting; + const pending = isPending || !!starting; return ( server && ( void; } & BoxProps) { const { preferences } = useDashboardPreferences(); if (preferences.showServerStats) { - return ; + return ( + + ); } else { - return ; + return ( + + ); } } diff --git a/ui/src/resources/server/table/standard.tsx b/ui/src/resources/server/table/standard.tsx index cce71770c0..ec04c451d4 100644 --- a/ui/src/resources/server/table/standard.tsx +++ b/ui/src/resources/server/table/standard.tsx @@ -17,10 +17,7 @@ export default function StandardServerTable({ resources: Types.ServerListItem[]; /** When provided, sorting is handled server side, * and sort updates are passed to this callback. */ - onServerSort?: (sort: { - sort_by?: string; - sort_desc?: boolean; - }) => void; + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; } & BoxProps) { const selectionState = useResourceSelectionState("Server"); const deployments = useRead("ListDeployments", { limit: 0 }).data; @@ -45,9 +42,7 @@ export default function StandardServerTable({ onServerSort && ((sorting) => { const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); - onServerSort( - sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, - ); + onServerSort(sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}); }) } tableKey="standard-server-table" diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 77707e4eb6..4511b9cd74 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -8,16 +8,40 @@ import { StatCell } from "mogh_ui"; import ServerVersion from "@/resources/server/version"; import ServerDiskUsage from "../diskUsage"; +const SORT_KEYS = ["Name", "Cpu", "Memory", "Disk", "LoadAverage", "Network"]; + export default function StatsServerTable({ resources, + onServerSort, + disableSorting, ...boxProps }: { resources: Types.ServerListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; + disableSorting?: boolean; } & BoxProps) { const selectionState = useResourceSelectionState("Server"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort + ? { + sort_by: sort.id, + // Descending first + sort_desc: !sort.desc, + } + : {}, + ); + }) + } tableKey="monitoring-server-table" data={resources} selectOptions={{ @@ -26,43 +50,88 @@ export default function StatsServerTable({ }} columns={[ { - size: 250, + id: "Name", accessorKey: "name", header: ({ column }) => ( - + ), cell: ({ row }) => ( ), }, { - header: "CPU", - size: 200, - cell: ({ row }) => , + id: "Cpu", + accessorKey: "id", + // The stats are fetched per row on the client, + // sorting is only available server side. + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Memory", - size: 200, - cell: ({ row }) => , + id: "Memory", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Disk", - size: 200, - cell: ({ row }) => , + id: "Disk", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Load Avg", - size: 160, - cell: ({ row }) => , + id: "LoadAverage", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Net", - size: 100, - cell: ({ row }) => , + id: "Network", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { header: "Version", - size: 160, cell: ({ row }) => , }, ]} @@ -70,36 +139,38 @@ export default function StatsServerTable({ ); } -function CpuCell({ id }: { id: string }) { - const stats = useServerStats(id); +function CpuCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const cpu = stats?.cpu_perc ?? 0; - const { cpuWarning: warning, cpuCritical: critical } = - useServerThresholds(id); + const { cpuWarning: warning, cpuCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical"; return ; } -function MemCell({ id }: { id: string }) { - const stats = useServerStats(id); +function MemCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; 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 { memWarning: warning, memCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; return ; } -function DiskCell({ id }: { id: string }) { - const stats = useServerStats(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; +function DiskCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; + const used = stats?.disk_used_gb ?? 0; + const total = stats?.disk_total_gb ?? 0; const perc = total > 0 ? (used / total) * 100 : 0; - const { diskWarning: warning, diskCritical: critical } = - useServerThresholds(id); + const { diskWarning: warning, diskCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; return ( @@ -107,13 +178,23 @@ function DiskCell({ id }: { id: string }) { value={stats ? perc : undefined} intent={intent} infoDisabled={!stats} - info={} + info={} /> ); } -function LoadAvgCell({ id }: { id: string }) { +/** + * The individual disk list is not included in the list item stats. + * Only mounted when the disk hover card opens, so the full + * GetSystemStats query is only made on demand. + */ +function DiskUsageInfo({ id }: { id: string }) { const stats = useServerStats(id); + return ; +} + +function LoadAvgCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const one = stats?.load_average?.one; const five = stats?.load_average?.five; const fifteen = stats?.load_average?.fifteen; @@ -147,8 +228,8 @@ function LoadAvgCell({ id }: { id: string }) { ); } -function NetCell({ id }: { id: string }) { - const stats = useServerStats(id); +function NetCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const ingress = stats?.network_ingress_bytes ?? 0; const egress = stats?.network_egress_bytes ?? 0; if (!stats) { diff --git a/ui/src/resources/table.tsx b/ui/src/resources/table.tsx index 92ef8e746f..5bc4f8b717 100644 --- a/ui/src/resources/table.tsx +++ b/ui/src/resources/table.tsx @@ -144,8 +144,6 @@ export default function ResourceTable({ return ; } - console.log(newProps) - return ( diff --git a/ui/yarn.lock b/ui/yarn.lock index 541da21507..efbefb7a73 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -750,10 +750,10 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.7.tgz#721d63913db5111dd6dfda8d3a748cfd7982d44a" - integrity sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw== +dompurify@3.4.8: + version "3.4.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.8.tgz#6c54f8c207160e7f83fcb7f4fd05a82ac36b1cdc" + integrity sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -1013,17 +1013,17 @@ mogh_auth_client@^1.7.0: dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.3.tgz#7aea4de43d2dc91c93fab09bd2a179f7ef434ac7" - integrity sha512-wVwkatCKmX/WB3lpowEPOy4q3o9o3hvG4rhiqdXyp/zyC7B14Bi5yT2BFiEcbzZ9jGx2/fC4c5SBCQYGGFyr0A== +mogh_ui@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.5.tgz#93efc0a9902332de76e6b820f23107c6c7a447d2" + integrity sha512-5Qszs5AlgYojPmQhYlSk4inRnuDwX1qUsERF7di5WkrfHwyEdf7FE7/DxQ2g8BJAP13SjibQ73mHSNilb1jRzA== -monaco-editor@^0.55.1: - version "0.55.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.55.1.tgz#e74c6fe5a6bf985b817d2de3eb88d56afc494a1b" - integrity sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A== +monaco-editor@^0.56.0: + version "0.56.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.56.0.tgz#e85ac00b7ab7092ca1d077894504919af5073cf0" + integrity sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg== dependencies: - dompurify "3.2.7" + dompurify "3.4.8" marked "14.0.0" monaco-languageserver-types@^0.4.0: From 6f6e20c9ef4dd8e2594fecf00fd11109af0fc93b Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 21 Jul 2026 15:16:53 -0700 Subject: [PATCH 187/203] fix clippy lint --- bin/core/src/monitor/record.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/core/src/monitor/record.rs b/bin/core/src/monitor/record.rs index 066782d602..559ee734c8 100644 --- a/bin/core/src/monitor/record.rs +++ b/bin/core/src/monitor/record.rs @@ -20,7 +20,7 @@ pub async fn record_server_stats(ts: i64) { ts, sid: status.id.clone(), cpu_perc: stats.cpu_perc, - load_average: stats.load_average.clone(), + load_average: stats.load_average, mem_total_gb: stats.mem_total_gb, mem_used_gb: stats.mem_used_gb, mem_buff_cache_gb: stats.mem_buff_cache_gb, From ac3d6729612c4a441345bfdb1b6ea51e7a045a09 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 21 Jul 2026 15:17:23 -0700 Subject: [PATCH 188/203] deploy 2.3.0-dev-45 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59de2a5111..821971e9f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "komodo_client", "libc", @@ -1706,7 +1706,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "async-compression", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "bytes", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "formatting", @@ -2190,7 +2190,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "mogh_error", ] @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "command", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "komodo_client", @@ -3382,7 +3382,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "bcrypt", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "async_timing_util", @@ -3454,7 +3454,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "arc-swap", @@ -3527,7 +3527,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "arc-swap", @@ -4644,7 +4644,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "encoding", @@ -7003,7 +7003,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "axum", @@ -8019,7 +8019,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 598cbbfafa..71a7899a3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-44" +version = "2.3.0-dev-45" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 88c833e2325d0fbf3870b1406e290c83d3c0aa76 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 21 Jul 2026 15:52:15 -0700 Subject: [PATCH 189/203] fix ui build after monaco bump --- ui/package.json | 2 +- ui/vite.config.ts | 15 ++++++++++++--- ui/yarn.lock | 8 ++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ui/package.json b/ui/package.json index a7ea3cac2a..2588f2d35b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,7 @@ "jotai-location": "^0.6.2", "lucide-react": "^1.22.0", "mogh_auth_client": "^1.7.0", - "mogh_ui": "^0.7.5", + "mogh_ui": "^0.7.6", "monaco-editor": "^0.56.0", "monaco-yaml": "^5.5.1", "react": "^19.2.7", diff --git a/ui/vite.config.ts b/ui/vite.config.ts index ab640ae617..58e1051abf 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -12,9 +12,17 @@ export default defineConfig({ allowedHosts: process.env.ALLOWED_HOSTS?.split(","), }, resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, + alias: [ + { find: "@", replacement: path.resolve(__dirname, "./src") }, + // monaco-editor >= 0.53 has an exports map ("./*.js": "./esm/vs/*.js"), + // so legacy deep imports like "monaco-editor/esm/vs/..." no longer + // resolve. monaco-worker-manager (used by monaco-yaml's worker) still + // imports the legacy path — rewrite it to the exports-map form. + { + find: /^monaco-editor\/esm\/vs\/(.*)$/, + replacement: "monaco-editor/$1", + }, + ], dedupe: [ "@mantine/core", "@mantine/form", @@ -32,6 +40,7 @@ export default defineConfig({ "react-router-dom", ], }, + optimizeDeps: { exclude: ["mogh_ui"] }, css: { preprocessorOptions: { scss: { diff --git a/ui/yarn.lock b/ui/yarn.lock index efbefb7a73..a441a54699 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1013,10 +1013,10 @@ mogh_auth_client@^1.7.0: dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.5.tgz#93efc0a9902332de76e6b820f23107c6c7a447d2" - integrity sha512-5Qszs5AlgYojPmQhYlSk4inRnuDwX1qUsERF7di5WkrfHwyEdf7FE7/DxQ2g8BJAP13SjibQ73mHSNilb1jRzA== +mogh_ui@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.6.tgz#9c9ab00f6d15a8fc6b31b3176a794eb3ef806bf2" + integrity sha512-THEdqJ/3y6RnHn3eWqxbKj0STx6aUAI4dsWekJFTrGIbE/rEBvWl449eo0v+ZpH2dgo9gIlkwDXBYx8xyBuzLA== monaco-editor@^0.56.0: version "0.56.0" From 7133b56161ebb8f88aab06c239dceb735dd5088a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 21 Jul 2026 16:02:47 -0700 Subject: [PATCH 190/203] load average chart first in stats --- ui/src/pages/stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/stats.tsx b/ui/src/pages/stats.tsx index bba27d0631..7a8b23a19a 100644 --- a/ui/src/pages/stats.tsx +++ b/ui/src/pages/stats.tsx @@ -73,10 +73,10 @@ type StatType = | "Network Egress"; const STAT_TYPES: [StatType, ReactNode][] = [ + ["Load Average", ], ["Cpu", ], ["Memory", ], ["Disk", ], - ["Load Average", ], ["Network Ingress", ], ["Network Egress", ], ]; From 89222ded8f50a86920cf5a4a84a9103635743876 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Wed, 22 Jul 2026 15:55:18 -0700 Subject: [PATCH 191/203] Fix deployment build version selector --- ui/src/lib/utils.ts | 13 ++++++++++ ui/src/resources/deployment/config/image.tsx | 26 +++++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 167c34a970..e5229120ed 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -305,3 +305,16 @@ export function setSelectedStateHandler( break; } } + +export function parseVersion(version: string): Types.Version { + const [major, minor, patch] = version + // In case of 'v2.0.0' fmt + .replaceAll("v", "") + .split(".") + .map(Number); + return { + major, + minor, + patch, + }; +} diff --git a/ui/src/resources/deployment/config/image.tsx b/ui/src/resources/deployment/config/image.tsx index 643a1df8f4..29ff62f6bc 100644 --- a/ui/src/resources/deployment/config/image.tsx +++ b/ui/src/resources/deployment/config/image.tsx @@ -14,6 +14,7 @@ import { import { Types } from "komodo_client"; import { ChevronsUpDown } from "lucide-react"; import { fmtDate, useSearchCombobox } from "mogh_ui"; +import { parseVersion } from "@/lib/utils"; export interface DeploymentImageConfigProps { image: Types.DeploymentImage | undefined; @@ -64,7 +65,8 @@ export default function DeploymentImageConfig({ + onSelect={(version) => { + console.log(version); setUpdate({ image: { ...image, @@ -73,8 +75,8 @@ export default function DeploymentImageConfig({ version, }, }, - }) - } + }); + }} disabled={disabled} /> @@ -134,7 +136,7 @@ function BuildVersionSelector({ }) { const versions = useRead( "ListBuildVersions", - { build: buildId! }, + { build: buildId!, limit: 50 }, { enabled: !!buildId }, ).data; @@ -148,7 +150,12 @@ function BuildVersionSelector({ { + onOptionSubmit={(value) => { + if (value === "Latest") { + onSelect({ major: 0, minor: 0, patch: 0 }); + } else { + onSelect(parseVersion(value)); + } combobox.closeDropdown(); }} width={250} @@ -177,16 +184,11 @@ function BuildVersionSelector({ }} /> - onSelect({ major: 0, minor: 0, patch: 0 })} - > - Latest - + Latest {filtered.map((v) => { const version = fmtVersion(v.version); return ( - + {version} {fmtDate(new Date(v.ts))} From 5c47f0659f99178524c425d8790cf47ff49bd857 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 23 Jul 2026 13:52:31 -0700 Subject: [PATCH 192/203] remove build version console log --- ui/src/resources/deployment/config/image.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/resources/deployment/config/image.tsx b/ui/src/resources/deployment/config/image.tsx index 29ff62f6bc..6f2a2f5db6 100644 --- a/ui/src/resources/deployment/config/image.tsx +++ b/ui/src/resources/deployment/config/image.tsx @@ -66,7 +66,6 @@ export default function DeploymentImageConfig({ buildId={image.params.build_id} selected={image.params.version} onSelect={(version) => { - console.log(version); setUpdate({ image: { ...image, From 09fce74da08e7b3948f99930e8d6430a12bee556 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 23 Jul 2026 15:34:06 -0700 Subject: [PATCH 193/203] provide logical core count, update load to reflect --- Cargo.lock | 360 ++++++++++-------- Cargo.toml | 42 +- bin/core/src/resource/server.rs | 13 +- bin/periphery/src/stats/mod.rs | 2 + client/core/rs/src/entities/server.rs | 4 + client/core/rs/src/entities/stats.rs | 6 + client/core/ts/src/types.ts | 13 +- ui/public/client/types.d.ts | 13 +- .../server/stats/current/load-average.tsx | 9 +- ui/src/resources/server/stats/system-info.tsx | 10 +- ui/src/resources/server/table/stats.tsx | 7 + 11 files changed, 293 insertions(+), 186 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 821971e9f8..ae00582974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -281,9 +281,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.18" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" +checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -347,9 +347,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.5" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" +checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.235.0" +version = "1.241.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeada8269fdf43855385c77d1b620512e28e6efad13d5429f58d66daca8c7842" +checksum = "36d274d82dfee2c5f8c43c023a32605521ff86a0342ed2279c6c02ed5401cefc" dependencies = [ "arc-swap", "aws-credential-types", @@ -386,6 +386,7 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -398,9 +399,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.102.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c82b3ac19f1431854f7ace3a7531674633e286bfdde21976893bfee36fd493b" +checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22" dependencies = [ "arc-swap", "aws-credential-types", @@ -411,6 +412,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -423,9 +425,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.104.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321000d2b4c5519ee573f73167f612efd7329322d9b26969ad1979f0427f1913" +checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188" dependencies = [ "arc-swap", "aws-credential-types", @@ -436,6 +438,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -448,9 +451,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.107.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0d328ba962af23ecfa3c9f23b98d3d35e325fa218d7f13d17a6bf522f8a560" +checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" dependencies = [ "arc-swap", "aws-credential-types", @@ -462,6 +465,7 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -474,9 +478,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.5" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -496,9 +500,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" dependencies = [ "futures-util", "pin-project-lite", @@ -507,9 +511,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.6" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -528,9 +532,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.12" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -547,7 +551,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -558,9 +562,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.7" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -569,28 +573,31 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", + "aws-smithy-xml", "urlencoding", ] [[package]] name = "aws-smithy-runtime" -version = "1.11.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -614,9 +621,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" +checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -632,9 +639,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" dependencies = [ "proc-macro2", "quote", @@ -643,9 +650,9 @@ dependencies = [ [[package]] name = "aws-smithy-schema" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -654,9 +661,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" dependencies = [ "base64-simd", "bytes", @@ -680,18 +687,21 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.16" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -732,7 +742,7 @@ dependencies = [ "sha1 0.10.6", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -806,7 +816,7 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "pin-project-lite", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -962,7 +972,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_urlencoded", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tower-service", @@ -1076,9 +1086,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -1240,9 +1250,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -1250,9 +1260,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -1262,14 +1272,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -2234,9 +2244,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -2244,9 +2254,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" @@ -2261,15 +2271,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -2278,15 +2288,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-timer" @@ -2296,9 +2306,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -2667,8 +2677,8 @@ dependencies = [ "idna", "ipnet", "jni", - "rand 0.10.1", - "thiserror 2.0.18", + "rand 0.10.2", + "thiserror 2.0.19", "tinyvec", "tokio", "tracing", @@ -2687,9 +2697,9 @@ dependencies = [ "jni", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "url", @@ -2712,11 +2722,11 @@ dependencies = [ "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "smallvec", "system-configuration", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", ] @@ -2915,7 +2925,7 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -3303,7 +3313,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -3400,7 +3410,7 @@ dependencies = [ "mogh_logger", "mogh_pki", "mogh_secret_file", - "rustls 0.23.41", + "rustls 0.23.42", "serde", "serde_json", "serde_qs", @@ -3433,7 +3443,7 @@ dependencies = [ "mogh_server", "mongo_indexed", "partial_derive2", - "rand 0.10.1", + "rand 0.10.2", "regex", "reqwest 0.13.4", "schemars 1.2.1", @@ -3442,9 +3452,9 @@ dependencies = [ "serde_qs", "strum 0.28.0", "svi", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.30.0", "tokio-util", "tracing", "typeshare", @@ -3501,7 +3511,7 @@ dependencies = [ "periphery_client", "regex", "reqwest 0.13.4", - "rustls 0.23.41", + "rustls 0.23.42", "serde", "serde_json", "serde_qs", @@ -3559,7 +3569,7 @@ dependencies = [ "mogh_server", "periphery_client", "portable-pty", - "rustls 0.23.41", + "rustls 0.23.42", "serde", "serde_json", "serde_yaml_ng", @@ -3594,9 +3604,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -3795,7 +3805,7 @@ dependencies = [ "mogh_resolver", "mogh_validations", "openidconnect", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "serde", "serde_json", @@ -3833,7 +3843,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml", "wildcard", ] @@ -3941,7 +3951,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713d8731c78a3b867d712e394cef4be52b278e45aeca4b80ceb41b08d1c32334" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", ] @@ -3999,7 +4009,7 @@ dependencies = [ "mongo_indexed_derive", "mongodb", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4058,7 +4068,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "rustc_version_runtime", - "rustls 0.23.41", + "rustls 0.23.42", "serde", "serde_bytes", "serde_with", @@ -4068,7 +4078,7 @@ dependencies = [ "stringprep", "strsim", "take_mut", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls 0.26.4", "tokio-util", @@ -4414,7 +4424,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -4444,7 +4454,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest 0.12.28", - "thiserror 2.0.18", + "thiserror 2.0.19", "tonic", "tracing", ] @@ -4480,7 +4490,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.4", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", ] @@ -4946,9 +4956,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.41", + "rustls 0.23.42", "socket2 0.6.3", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -4967,10 +4977,10 @@ dependencies = [ "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -5040,9 +5050,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.2", @@ -5144,9 +5154,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -5156,9 +5166,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -5217,7 +5227,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "serde", @@ -5258,7 +5268,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -5531,9 +5541,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -5578,7 +5588,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.41", + "rustls 0.23.42", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", @@ -5761,9 +5771,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5801,22 +5811,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -5832,9 +5842,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.14.0", "itoa", @@ -6127,7 +6137,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -6366,7 +6376,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91bb902c47385644774ba89f7e7982d3a32cf1da4e8870bf633cdece450eced1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -6391,6 +6401,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6413,9 +6434,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.5" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", @@ -6476,11 +6497,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -6496,13 +6517,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -6581,9 +6602,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6623,15 +6644,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.41", + "rustls 0.23.42", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -6647,34 +6668,47 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", - "rustls 0.23.41", + "tokio", + "tungstenite 0.29.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tungstenite", + "tungstenite 0.30.0", ] [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -6724,14 +6758,14 @@ dependencies = [ "ordered_hash_map", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" @@ -6896,7 +6930,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -7015,13 +7049,13 @@ dependencies = [ "mogh_pki", "periphery_client", "pin-project-lite", - "rand 0.10.1", - "rustls 0.23.41", + "rand 0.10.2", + "rustls 0.23.42", "serde", "sha1 0.11.0", "sha2 0.11.0", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.30.0", "tokio-util", "tracing", "url", @@ -7046,10 +7080,26 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.41", - "rustls-pki-types", "sha1 0.10.6", - "thiserror 2.0.18", + "thiserror 2.0.19", +] + +[[package]] +name = "tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.10.2", + "rustls 0.23.42", + "rustls-pki-types", + "sha1 0.11.0", + "thiserror 2.0.19", ] [[package]] @@ -7257,13 +7307,13 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.2", "js-sys", - "rand 0.10.1", + "rand 0.10.2", "serde_core", "wasm-bindgen", ] @@ -7556,7 +7606,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b0540e91e49de3817c314da0dd3bc518093ceacc6ea5327cb0e1eb073e5189" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 71a7899a3d..a279c9234e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,15 +52,15 @@ svi = "1.2.0" # ASYNC reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "form", "query", "rustls"] } -tokio = { version = "1.52.3", features = ["full"] } -tokio-util = { version = "0.7.18", features = ["io", "codec"] } -tokio-stream = { version = "0.1.18", features = ["sync"] } +tokio = { version = "1.53.1", features = ["full"] } +tokio-util = { version = "0.7.19", features = ["io", "codec"] } +tokio-stream = { version = "0.1.19", features = ["sync"] } pin-project-lite = "0.2.17" -futures-util = "0.3.32" +futures-util = "0.3.33" arc-swap = "1.9.2" # SERVER -tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-native-roots"] } axum = { version = "0.8.9", features = ["ws", "json", "macros"] } axum-extra = { version = "0.12.6", features = ["typed-header"] } @@ -71,40 +71,40 @@ utoipa = "5.5.0" # SER/DE ipnetwork = { version = "0.21.1", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] } -serde = { version = "1.0.228", features = ["derive"] } +serde = { version = "1.0.229", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] } bson = { version = "2.15.0" } # must keep in sync with mongodb version -toml = "1.1.2" +toml = "1.1.3" serde_yaml_ng = "0.10.0" -serde_json = "1.0.150" +serde_json = "1.0.151" serde_qs = "1.1.2" url = "2.5.8" # ERROR -anyhow = "1.0.103" -thiserror = "2.0.18" +anyhow = "1.0.104" +thiserror = "2.0.19" # SYSTEM -libc = "0.2.186" +libc = "0.2.189" # LOGGING tracing = "0.1.44" # CONFIG -clap = { version = "4.6.1", features = ["derive"] } +clap = { version = "4.6.4", features = ["derive"] } dotenvy = "0.15.7" envy = "0.4.2" # CRYPTO / AUTH -uuid = { version = "1.23.4", features = ["v4", "fast-rng", "serde"] } -rustls = { version = "0.23.41", features = ["aws-lc-rs"] } +uuid = { version = "1.24.0", features = ["v4", "fast-rng", "serde"] } +rustls = { version = "0.23.42", features = ["aws-lc-rs"] } data-encoding = "2.11.0" urlencoding = "2.1.3" bcrypt = "0.19.2" hmac = "0.13.0" sha1 = "0.11.0" sha2 = "0.11.0" -rand = "0.10.1" +rand = "0.10.2" hex = "0.4.3" # SYSTEM @@ -113,13 +113,13 @@ portable-pty = "0.9.0" shell-escape = "0.1.5" crossterm = "0.29.0" bollard = "0.21.0" -sysinfo = "0.39.5" +sysinfo = "0.39.6" shlex = "2.0.1" # CLOUD -aws-config = "1.8.18" -aws-sdk-ec2 = "1.235.0" -aws-credential-types = "1.2.14" +aws-config = "1.10.0" +aws-sdk-ec2 = "1.241.0" +aws-credential-types = "1.3.0" ## CRON english-to-cron = "0.1.7" @@ -135,8 +135,8 @@ comfy-table = "7.2.2" typeshare = "1.0.5" wildcard = "0.3.0" colored = "3.1.1" -bytes = "1.12.0" -regex = "1.12.4" +bytes = "1.12.1" +regex = "1.13.1" [profile.release] strip = "debuginfo" \ No newline at end of file diff --git a/bin/core/src/resource/server.rs b/bin/core/src/resource/server.rs index a7d8968aa5..1613a53a94 100644 --- a/bin/core/src/resource/server.rs +++ b/bin/core/src/resource/server.rs @@ -68,9 +68,15 @@ impl super::KomodoResource for Server { server: Resource, ) -> Self::ListItem { let status = server_status_cache().get(&server.id).await; - let (periphery_info, system_stats) = status + let (periphery_info, system_info, system_stats) = status .as_ref() - .map(|s| (s.periphery_info.as_ref(), s.system_stats.as_ref())) + .map(|s| { + ( + s.periphery_info.as_ref(), + s.system_info.as_ref(), + s.system_stats.as_ref(), + ) + }) .unwrap_or_default(); let ( version, @@ -98,6 +104,9 @@ impl super::KomodoResource for Server { state: status.as_ref().map(|s| s.state).unwrap_or_default(), err: status.as_ref().and_then(|s| s.err.clone()), stats: system_stats.map(Into::into), + core_count: system_info.and_then(|i| i.core_count), + logical_core_count: system_info + .and_then(|i| i.logical_core_count), region: server.config.region, address: optional_string(server.config.address), external_address: optional_string( diff --git a/bin/periphery/src/stats/mod.rs b/bin/periphery/src/stats/mod.rs index 88a62dfa0f..eb70dafb00 100644 --- a/bin/periphery/src/stats/mod.rs +++ b/bin/periphery/src/stats/mod.rs @@ -205,11 +205,13 @@ fn get_system_information( kernel: System::kernel_version(), host_name: System::host_name(), core_count: System::physical_core_count().map(|c| c as u32), + logical_core_count: Some(sys.cpus().len() as u32), cpu_brand: sys .cpus() .iter() .next() .map(|cpu| cpu.brand().to_string()) .unwrap_or_default(), + cpu_arch: System::cpu_arch(), } } diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index b8a31d70f5..c32907c48d 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -44,6 +44,10 @@ pub struct ServerListItemInfo { pub err: Option<_Serror>, /// System stats, if available pub stats: Option, + /// The server's number of physical cores. + pub core_count: Option, + /// The server's number of logical cores. + pub logical_core_count: Option, /// Region of the server. pub region: String, /// Address of the server, or null if empty. diff --git a/client/core/rs/src/entities/stats.rs b/client/core/rs/src/entities/stats.rs index b21ad3315a..51292dfc7d 100644 --- a/client/core/rs/src/entities/stats.rs +++ b/client/core/rs/src/entities/stats.rs @@ -18,10 +18,16 @@ pub struct SystemInformation { pub kernel: Option, /// Physical core count pub core_count: Option, + /// Logical core count. + pub logical_core_count: Option, /// System hostname based off DNS pub host_name: Option, /// The CPU's brand + #[serde(default)] pub cpu_brand: String, + /// CPU architecture (eg. x86_64, aarch64, arm64) + #[serde(default)] + pub cpu_arch: String, } /// System stats stored on the database. diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 0c83827002..a3ccdb6229 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -2858,10 +2858,17 @@ export interface SystemInformation { kernel?: string; /** Physical core count */ core_count?: number; + /** + * Logical core count. If available, + * used to interpret system load accurately. + */ + logical_core_count?: number; /** System hostname based off DNS */ host_name?: string; /** The CPU's brand */ - cpu_brand: string; + cpu_brand?: string; + /** CPU architecture (eg. x86_64, aarch64, arm64) */ + cpu_arch?: string; } export type GetSystemInformationResponse = SystemInformation; @@ -5534,6 +5541,10 @@ export interface ServerListItemInfo { err?: _Serror; /** System stats, if available */ stats?: MinimalSystemStats; + /** The server's number of physical cores. */ + core_count?: number; + /** The server's number of logical cores. */ + logical_core_count?: number; /** Region of the server. */ region: string; /** Address of the server, or null if empty. */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 362bee269a..b5abe89bab 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -2992,10 +2992,17 @@ export interface SystemInformation { kernel?: string; /** Physical core count */ core_count?: number; + /** + * Logical core count. If available, + * used to interpret system load accurately. + */ + logical_core_count?: number; /** System hostname based off DNS */ host_name?: string; /** The CPU's brand */ - cpu_brand: string; + cpu_brand?: string; + /** CPU architecture (eg. x86_64, aarch64, arm64) */ + cpu_arch?: string; } export type GetSystemInformationResponse = SystemInformation; export interface SystemLoadAverage { @@ -5417,6 +5424,10 @@ export interface ServerListItemInfo { err?: _Serror; /** System stats, if available */ stats?: MinimalSystemStats; + /** The server's number of physical cores. */ + core_count?: number; + /** The server's number of logical cores. */ + logical_core_count?: number; /** Region of the server. */ region: string; /** Address of the server, or null if empty. */ diff --git a/ui/src/resources/server/stats/current/load-average.tsx b/ui/src/resources/server/stats/current/load-average.tsx index d1b50f1d20..a4e947b884 100644 --- a/ui/src/resources/server/stats/current/load-average.tsx +++ b/ui/src/resources/server/stats/current/load-average.tsx @@ -15,11 +15,12 @@ export function ServerLoadAverage({ if (!stats?.load_average) return null; const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {}; const isServerAvailable = useIsServerAvailable(id); - const cores = useRead( + const sysInfo = useRead( "GetSystemInformation", { server: id }, { enabled: isServerAvailable }, - ).data?.core_count; + ).data; + const cores = sysInfo?.logical_core_count || sysInfo?.core_count; const pct = (load: number) => cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined; @@ -37,13 +38,13 @@ export function ServerLoadAverage({ {/* CURRENT LOAD */} - + {one.toFixed(2)} {cores && cores > 0 - ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores` + ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} Cores` : "N/A"} diff --git a/ui/src/resources/server/stats/system-info.tsx b/ui/src/resources/server/stats/system-info.tsx index 81b3189088..9344914550 100644 --- a/ui/src/resources/server/stats/system-info.tsx +++ b/ui/src/resources/server/stats/system-info.tsx @@ -54,10 +54,16 @@ export default function ServerSystemInfo({ header: "CPU", accessorKey: "cpu_brand", }, + { + header: "Arch", + accessorKey: "cpu_arch", + }, { header: "Core Count", - accessorFn: ({ core_count }) => - `${core_count} Core${(core_count || 0) > 1 ? "s" : ""}`, + accessorFn: ({ core_count, logical_core_count }) => + logical_core_count + ? `${logical_core_count} Logical / ${core_count} Physical Core${(core_count || 0) > 1 ? "s" : ""}` + : `${core_count} Physical Core${(core_count || 0) > 1 ? "s" : ""}`, }, { header: "Total Memory", diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 4511b9cd74..961fe65ead 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -198,6 +198,8 @@ function LoadAvgCell({ server }: { server: Types.ServerListItem }) { const one = stats?.load_average?.one; const five = stats?.load_average?.five; const fifteen = stats?.load_average?.fifteen; + const logicalCores = server.info.logical_core_count; + const physicalCores = server.info.core_count; return ( @@ -224,6 +226,11 @@ function LoadAvgCell({ server }: { server: Types.ServerListItem }) { {fifteen !== undefined ? fifteen.toFixed(2) : "N/A"} + {logicalCores && ( + + {logicalCores} / {physicalCores} Core{logicalCores === 1 ? "" : "s"} + + )} ); } From 0581dfa22f11796fdc90bc3910f652999174233f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 23 Jul 2026 15:34:37 -0700 Subject: [PATCH 194/203] deploy 2.3.0-dev-46 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae00582974..4727d07baf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "komodo_client", "libc", @@ -1716,7 +1716,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "async-compression", @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "bytes", @@ -2048,7 +2048,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "formatting", @@ -2200,7 +2200,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "mogh_error", ] @@ -2450,7 +2450,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "command", @@ -3224,7 +3224,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "komodo_client", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "bcrypt", @@ -3422,7 +3422,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "async_timing_util", @@ -3464,7 +3464,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "arc-swap", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "arc-swap", @@ -4654,7 +4654,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "encoding", @@ -7037,7 +7037,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "axum", @@ -8069,7 +8069,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index a279c9234e..f72a084ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-45" +version = "2.3.0-dev-46" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 146efd98f151cdc562cd5de27459af7b8b026372 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 24 Jul 2026 17:52:11 -0700 Subject: [PATCH 195/203] add basic reporting functionality --- bin/core/src/config.rs | 6 + bin/core/src/helpers/prune.rs | 2 +- bin/core/src/main.rs | 2 + bin/core/src/reporting.rs | 144 +++++++++++++++++++++ client/core/rs/src/entities/config/core.rs | 43 +++++- client/core/rs/src/entities/core_report.rs | 19 +++ client/core/rs/src/entities/mod.rs | 2 + 7 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 bin/core/src/reporting.rs create mode 100644 client/core/rs/src/entities/core_report.rs diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 31cc5e363d..21e919dd54 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -433,6 +433,12 @@ pub fn core_config() -> &'static CoreConfig { ssl_cert_file: env .komodo_ssl_cert_file .unwrap_or(config.ssl_cert_file), + reporting_enabled: env + .komodo_reporting_enabled + .unwrap_or(config.reporting_enabled), + reporting_private_key: env + .komodo_reporting_private_key + .unwrap_or(config.reporting_private_key), ui_path: env.komodo_ui_path.unwrap_or(config.ui_path), ui_index_force_no_cache: env .komodo_ui_index_force_no_cache diff --git a/bin/core/src/helpers/prune.rs b/bin/core/src/helpers/prune.rs index f5d7963058..888762d339 100644 --- a/bin/core/src/helpers/prune.rs +++ b/bin/core/src/helpers/prune.rs @@ -55,7 +55,7 @@ async fn prune_images() -> anyhow::Result<()> { while let Some((res, server)) = futures.next().await { if let Err(e) = res { warn!( - "failed to prune images on Server {} ({}) | {e:#}", + "Failed to prune images on Server {} ({}) | {e:#}", server.name, server.id ) } diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index 3949d75c76..e96e277501 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -19,6 +19,7 @@ mod monitor; mod network; mod periphery; mod permission; +mod reporting; mod resource; mod schedule; mod stack; @@ -71,6 +72,7 @@ async fn app() -> anyhow::Result<()> { resource::spawn_action_state_refresh_loop(); schedule::spawn_schedule_executor(); helpers::prune::spawn_prune_loop(); + reporting::spawn_reporting_loop(); } .instrument(startup_span) .await; diff --git a/bin/core/src/reporting.rs b/bin/core/src/reporting.rs new file mode 100644 index 0000000000..352a4ecd0c --- /dev/null +++ b/bin/core/src/reporting.rs @@ -0,0 +1,144 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context; +use async_timing_util::{Timelength, wait_until_timelength}; +use komodo_client::entities::{ + ResourceTargetVariant, core_report::CoreReport, +}; +use mogh_pki::{ + PkiKind, RotatableKeyPair, one_way::OneWayNoiseHandshake, +}; + +use crate::{config::core_config, state::db_client}; + +pub fn spawn_reporting_loop() { + let config = core_config(); + + if !config.reporting_enabled { + return; + } + + let keys = match RotatableKeyPair::from_private_key_spec( + PkiKind::OneWay, + &config.reporting_private_key, + ) { + Ok(keys) => keys, + Err(e) => { + warn!( + "Failed to initialize reporting key pair. Reporting is disabled. {e:#}" + ); + return; + } + }; + + let client = reqwest::Client::default(); + + tokio::spawn(async move { + loop { + wait_until_timelength(Timelength::OneDay, 1100).await; + if let Err(e) = report(&client, &keys).await { + warn!("Reporting failed. {e:#}"); + } + } + }); +} + +async fn report( + client: &reqwest::Client, + keys: &RotatableKeyPair, +) -> anyhow::Result<()> { + let db = db_client(); + + let endpoint_public_key = client + .get("https://mogh.tech/report/public_key") + .send() + .await + .context("Failed to query for reporting endpoint public key for signature.")? + .error_for_status() + .context("Failed response for reporting endpoint public key for signature.")? + .text() + .await + .context("Failed to get reporting endpoint public key for signature.")?; + + let users = db + .users + .count_documents(Default::default()) + .await + .context("Failed to query database for users")?; + let servers = db + .servers + .count_documents(Default::default()) + .await + .context("Failed to query database for servers")?; + let swarms = db + .swarms + .count_documents(Default::default()) + .await + .context("Failed to query database for swarms")?; + let stacks = db + .stacks + .count_documents(Default::default()) + .await + .context("Failed to query database for stacks")?; + let deployments = db + .deployments + .count_documents(Default::default()) + .await + .context("Failed to query database for deployments")?; + let builds = db + .builds + .count_documents(Default::default()) + .await + .context("Failed to query database for builds")?; + + let count = [ + (ResourceTargetVariant::Server, servers), + (ResourceTargetVariant::Swarm, swarms), + (ResourceTargetVariant::Stack, stacks), + (ResourceTargetVariant::Deployment, deployments), + (ResourceTargetVariant::Build, builds), + ] + .into_iter() + .collect(); + + let keys = keys.load(); + + let report = CoreReport { + report_public_key: keys.public().to_string(), + users, + count, + }; + + let serialized = serde_json::to_string(&report) + .context("Failed to serialize report JSON")?; + + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64; + + let prologue = + format!("post|/report/komodo|{serialized}|{}", timestamp); + + let mut handshake = OneWayNoiseHandshake::new_initiator( + keys.private().as_bytes(), + endpoint_public_key.as_bytes(), + prologue.as_bytes(), + )?; + + let signature = handshake + .generate_signature() + .context("Failed to generate report signature")?; + + client + .post("https://mogh.tech/report/komodo") + .header("x-api-signature", signature) + .header("x-api-timestamp", timestamp) + .header("content-type", "application/json") + .body(serialized) + .send() + .await + .context("Failed to post report.")? + .error_for_status() + .context("Failed response for report post.")?; + + Ok(()) +} diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index f776a2e28e..e8589b59c6 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -296,6 +296,11 @@ pub struct Env { /// Override `ssl_cert_file` pub komodo_ssl_cert_file: Option, + /// Override `reporting_enabled` + pub komodo_reporting_enabled: Option, + /// Override `reporting_private_key` + pub komodo_reporting_private_key: Option, + /// Override `ui_path` pub komodo_ui_path: Option, /// Override `ui_index_force_no_cache` @@ -771,6 +776,28 @@ pub struct CoreConfig { #[serde(default = "default_ssl_cert_file")] pub ssl_cert_file: String, + // ============= + // = Reporting = + // ============= + /// If passed, enables semi-anonymous reporting to https://mogh.tech/komodo/report. + /// Reports are only identified by the specific + /// private key ([CoreConfig::reporting_private_key]) used + /// to sign the report. + #[serde(default)] + pub reporting_enabled: bool, + + /// Private key to sign the core reports. + /// + /// Supports openssl generated pem file, `openssl genpkey -algorithm X25519 -out private.key`. + /// To load from file, use `private_key = "file:/path/to/private.key"`. + /// + /// If a file is specified and does not exist, will try to generate one at the path + /// and use it going forward. + /// + /// Default: file:/config/keys/core-reporting.key + #[serde(default = "default_reporting_private_key")] + pub reporting_private_key: String, + // ========= // = Other = // ========= @@ -821,6 +848,10 @@ fn default_private_key() -> String { String::from("file:/config/keys/core.key") } +fn default_reporting_private_key() -> String { + String::from("file:/config/keys/core-reporting.key") +} + fn default_default_pagination_limit() -> u64 { 50 } @@ -970,6 +1001,8 @@ impl Default for CoreConfig { ssl_enabled: Default::default(), ssl_key_file: default_ssl_key_file(), ssl_cert_file: default_ssl_cert_file(), + reporting_enabled: Default::default(), + reporting_private_key: default_reporting_private_key(), ui_path: default_ui_path(), ui_index_force_no_cache: Default::default(), sync_directory: default_sync_directory(), @@ -1106,10 +1139,18 @@ impl CoreConfig { provider }) .collect(), - ssl_enabled: config.ssl_enabled, ssl_key_file: config.ssl_key_file, ssl_cert_file: config.ssl_cert_file, + reporting_enabled: config.reporting_enabled, + reporting_private_key: if self + .reporting_private_key + .starts_with("file:") + { + self.reporting_private_key.clone() + } else { + empty_or_redacted(&self.reporting_private_key) + }, ui_path: config.ui_path, ui_index_force_no_cache: config.ui_index_force_no_cache, repo_directory: config.repo_directory, diff --git a/client/core/rs/src/entities/core_report.rs b/client/core/rs/src/entities/core_report.rs new file mode 100644 index 0000000000..b3db82fde7 --- /dev/null +++ b/client/core/rs/src/entities/core_report.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::entities::ResourceTargetVariant; + +/// Semi-anonymous core data reporting. +/// Reports only include the reporting-specific core public key +/// (not the keys used for server connection). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CoreReport { + /// Reporting specific public key. + /// Must match public key obtained through request signature. + pub report_public_key: String, + /// The total number of users + pub users: u64, + /// Resource counts by type + pub count: HashMap, +} diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 17472230cf..012ece8000 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -35,6 +35,8 @@ pub mod build; pub mod builder; /// [core config][config::core] and [periphery config][config::periphery] pub mod config; +/// Subtypes of [CoreReport][core_report::CoreReport] +pub mod core_report; /// Subtypes of [Deployment][deployment::Deployment]. pub mod deployment; /// Networks, Images, Containers. From 3e10ea548bec4f720538ab006e962e926c14cb1e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 25 Jul 2026 14:00:43 -0700 Subject: [PATCH 196/203] Add / fix shell escaping for periphery commands --- bin/periphery/src/api/compose.rs | 41 +++++++++++++++++++------- bin/periphery/src/api/container/mod.rs | 3 ++ bin/periphery/src/api/swarm/service.rs | 2 ++ bin/periphery/src/docker/config.rs | 23 ++++++++------- bin/periphery/src/docker/mod.rs | 13 ++++++-- bin/periphery/src/docker/secret.rs | 37 +++++++++++------------ bin/periphery/src/helpers.rs | 10 +++++-- 7 files changed, 83 insertions(+), 46 deletions(-) diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index a7a5e63695..30c1359d7f 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -1,4 +1,7 @@ -use std::{borrow::Cow, fmt::Write, path::PathBuf, time::Duration}; +use std::{ + borrow::Cow, fmt::Write, path::PathBuf, sync::LazyLock, + time::Duration, +}; use anyhow::{Context, anyhow}; use command::{ @@ -86,8 +89,13 @@ impl Resolve for GetComposeLogSearch { Default::default() }; let command = format!( - "{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}", - services.join(" ") + "{docker_compose} -p {} logs --tail 5000{timestamps} {} 2>&1 | {grep}", + escape(project.into()), + services + .iter() + .map(|service| escape(service.into())) + .collect::>() + .join(" ") ); Ok( run_komodo_shell_command( @@ -887,16 +895,21 @@ impl Resolve for ComposeRun { let project_name = stack.project_name(true); + // Comes straight off the request and interpolated into a + // command string which is handed to `bash -c`, so must be escaped. + let service = escape(Cow::Borrowed(&service)); + // Parse wrapper configuration let compose_cmd_wrapper = parse_multiline_command(&stack.config.compose_cmd_wrapper); // If wrapper_include is empty but wrapper is set, use default ["up"] for backward compatibility - let default_include = vec![String::from("up")]; + static DEFAULT_INCLUDE: LazyLock> = + LazyLock::new(|| vec![String::from("up")]); let wrapper_include = if stack.config.compose_cmd_wrapper_include.is_empty() && !compose_cmd_wrapper.is_empty() { - &default_include + &DEFAULT_INCLUDE } else { &stack.config.compose_cmd_wrapper_include }; @@ -949,17 +962,25 @@ impl Resolve for ComposeRun { run_flags.push_str(" --service-ports"); } if let Some(dir) = workdir.as_ref() { - run_flags.push_str(&format!(" --workdir {dir}")); + run_flags + .push_str(&format!(" --workdir {}", escape(dir.into()))); } if let Some(user) = user.as_ref() { - run_flags.push_str(&format!(" --user {user}")); + run_flags.push_str(&format!(" --user {}", escape(user.into()))); } if let Some(entrypoint) = entrypoint.as_ref() { - run_flags.push_str(&format!(" --entrypoint {entrypoint}")); + run_flags.push_str(&format!( + " --entrypoint {}", + escape(entrypoint.into()) + )); } if let Some(env) = env { for (k, v) in env { - run_flags.push_str(&format!(" -e {}={} ", k, v)); + // Escaped as a single KEY=VALUE token, which is what compose expects. + run_flags.push_str(&format!( + " -e {}", + escape(format!("{k}={v}").into()) + )); } } @@ -969,7 +990,7 @@ impl Resolve for ComposeRun { .map(|argv| { let joined = argv .iter() - .map(|s| escape(Cow::Borrowed(s)).into_owned()) + .map(|s| escape(s.into()).into_owned()) .collect::>() .join(" "); format!(" {joined}") diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index 8e0afb416d..aaee7402fb 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -15,6 +15,7 @@ use komodo_client::entities::{ }; use mogh_resolver::Resolve; use periphery_client::api::container::*; +use shell_escape::unix::escape; use crate::{ docker::{stats::get_container_stats, stop_container_command}, @@ -94,6 +95,7 @@ impl Resolve for GetContainerLogSearch { } else { Default::default() }; + let name = escape(name.into()); let command = format!( "docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}" ); @@ -321,6 +323,7 @@ impl Resolve for RemoveContainer { args: &crate::api::Args, ) -> anyhow::Result { let RemoveContainer { name, signal, time } = self; + let name = escape(name.into()); let stop_command = stop_container_command(&name, signal, time); let command = format!("{stop_command} && docker container rm {name}"); diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 75dcdec450..a6086b6b8e 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -23,6 +23,7 @@ use periphery_client::api::swarm::{ InspectSwarmService, RemoveSwarmServices, RollbackSwarmService, UpdateSwarmService, }; +use shell_escape::unix::escape; use tracing::Instrument; use crate::{ @@ -132,6 +133,7 @@ impl Resolve for GetSwarmServiceLogSearch { Default::default() }; let grep = format_log_grep(&terms, combinator, invert); + let service = escape(service.into()); let command = format!( "docker service logs --tail 5000{timestamps}{no_task_ids}{no_resolve}{details} {service} 2>&1 | {grep}", ); diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 042145b3d6..98fe942938 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -16,6 +16,7 @@ use komodo_client::entities::{ random_string, }; use periphery_client::api::swarm::CreateSwarmConfig; +use shell_escape::unix::escape; use super::*; @@ -116,23 +117,25 @@ pub async fn create_swarm_config( template_driver, }: &CreateSwarmConfig, ) -> anyhow::Result { - let mut command = String::from("docker config create"); + let mut flags = String::new(); for label in labels { - write!(&mut command, " --label {label}")?; + write!(&mut flags, " --label {}", escape(label.into()))?; } if let Some(driver) = template_driver { - write!(&mut command, " --template-driver {driver}")?; + write!( + &mut flags, + " --template-driver {}", + escape(driver.into()) + )?; } - write!( - &mut command, - r#" {name} - <<'EOF' -{} -EOF"#, - data.trim() - )?; + let command = format!( + "printf '%s\\n' {} | docker config create{flags} {} -", + escape(data.trim().into()), + escape(name.into()) + ); let log = run_komodo_shell_command( "Create Config", diff --git a/bin/periphery/src/docker/mod.rs b/bin/periphery/src/docker/mod.rs index 34ba6f5a58..fd9789a1b2 100644 --- a/bin/periphery/src/docker/mod.rs +++ b/bin/periphery/src/docker/mod.rs @@ -8,6 +8,7 @@ use komodo_client::entities::{ docker::{task::*, *}, update::Log, }; +use shell_escape::unix::escape; pub mod compose; pub mod config; @@ -53,9 +54,15 @@ pub async fn docker_login( None => crate::helpers::registry_token(domain, account)?, }; - let log = run_shell_command(&format!( - "echo {registry_token} | docker login {domain} --username '{account}' --password-stdin", - ), CommandOptions::default()) + let log = run_shell_command( + &format!( + "echo {} | docker login {} --username {} --password-stdin", + escape(registry_token.into()), + escape(domain.into()), + escape(account.into()), + ), + CommandOptions::default(), + ) .await; if log.success() { diff --git a/bin/periphery/src/docker/secret.rs b/bin/periphery/src/docker/secret.rs index 9c53ed1c34..0e55eef33f 100644 --- a/bin/periphery/src/docker/secret.rs +++ b/bin/periphery/src/docker/secret.rs @@ -18,6 +18,7 @@ use komodo_client::entities::{ update::Log, }; use periphery_client::api::swarm::CreateSwarmSecret; +use shell_escape::unix::escape; use super::DockerClient; @@ -119,36 +120,32 @@ pub async fn create_swarm_secret( template_driver, }: &CreateSwarmSecret, ) -> anyhow::Result { - let mut command = String::from("docker secret create"); + let mut flags = String::new(); if let Some(driver) = driver { - write!(&mut command, " --driver {driver}")?; + write!(&mut flags, " --driver {}", escape(driver.into()))?; } for label in labels { - write!(&mut command, " --label {label}")?; + write!(&mut flags, " --label {}", escape(label.into()))?; } if let Some(driver) = template_driver { - write!(&mut command, " --template-driver {driver}")?; + write!( + &mut flags, + " --template-driver {}", + escape(driver.into()) + )?; } - let mut sanitized_command = command.clone(); - - write!( - &mut command, - r#" {name} - <<'EOF' -{} -EOF"#, - data.trim() - )?; - - write!( - &mut sanitized_command, - r#" {name} - <<'EOF' - -EOF"# - )?; + let name = escape(name.into()); + let command = format!( + "printf '%s\\n' {} | docker secret create{flags} {name} -", + escape(data.trim().into()) + ); + let sanitized_command = format!( + "printf '%s\\n' | docker secret create{flags} {name} -" + ); let mut log = run_komodo_shell_command( "Create Secret", diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index d95cf35a2e..620caf0d42 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -19,6 +19,7 @@ use komodo_client::{ parsers::QUOTE_PATTERN, }; use periphery_client::api::git::PeripheryRepoExecutionResponse; +use shell_escape::unix::escape; use crate::config::periphery_config; @@ -121,12 +122,15 @@ pub fn format_log_grep( let maybe_invert = if invert { " -v" } else { Default::default() }; match combinator { SearchCombinator::Or => { - format!("grep{maybe_invert} -E '{}'", terms.join("|")) + format!( + "grep{maybe_invert} -E {}", + escape(terms.join("|").into()) + ) } SearchCombinator::And => { format!( - "grep{maybe_invert} -P '^(?=.*{})'", - terms.join(")(?=.*") + "grep{maybe_invert} -P {}", + escape(format!("^(?=.*{})", terms.join(")(?=.*")).into()) ) } } From 40f4bc4bdc8e4439d79de56cadbab60ba30081e1 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 25 Jul 2026 17:08:01 -0700 Subject: [PATCH 197/203] safe stdin token and prevent args passed through resource names on shell --- bin/periphery/src/api/compose.rs | 17 +-- bin/periphery/src/api/container/mod.rs | 25 +++-- bin/periphery/src/api/docker.rs | 10 +- bin/periphery/src/api/swarm/mod.rs | 5 +- bin/periphery/src/api/swarm/service.rs | 9 +- bin/periphery/src/api/swarm/stack.rs | 5 +- bin/periphery/src/docker/config.rs | 9 +- bin/periphery/src/docker/mod.rs | 20 ++-- bin/periphery/src/docker/secret.rs | 5 +- bin/periphery/src/docker/stack.rs | 4 +- bin/periphery/src/docker/stats.rs | 6 +- lib/command/src/lib.rs | 146 +++++++++++++++++++++---- lib/command/src/options.rs | 13 +++ 13 files changed, 208 insertions(+), 66 deletions(-) diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index 30c1359d7f..15ae236052 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -54,7 +54,7 @@ impl Resolve for GetComposeLog { Default::default() }; let command = format!( - "{docker_compose} -p {project} logs --tail {tail}{timestamps} {}", + "{docker_compose} -p {project} logs --tail {tail}{timestamps} -- {}", services.join(" ") ); Ok( @@ -89,7 +89,7 @@ impl Resolve for GetComposeLogSearch { Default::default() }; let command = format!( - "{docker_compose} -p {} logs --tail 5000{timestamps} {} 2>&1 | {grep}", + "{docker_compose} -p {} logs --tail 5000{timestamps} -- {} 2>&1 | {grep}", escape(project.into()), services .iter() @@ -353,7 +353,8 @@ impl Resolve for ComposePull { let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let file_args = stack.compose_file_paths().join(" -f "); @@ -525,7 +526,8 @@ impl Resolve for ComposeUp { let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let file_args = stack.compose_file_paths().join(" -f "); @@ -916,7 +918,7 @@ impl Resolve for ComposeRun { if pull.unwrap_or_default() { let pull_command = format!( - "{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull {service}", + "{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull -- {service}", ); let (pull_command, wrapped) = match maybe_wrap_command( pull_command, @@ -998,7 +1000,7 @@ impl Resolve for ComposeRun { .unwrap_or_default(); let run_command = format!( - "{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} {service}{command_args}", + "{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} -- {service}{command_args}", ); let (run_command, _) = match maybe_wrap_command( run_command, @@ -1092,7 +1094,8 @@ async fn compose_down( let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let log = run_komodo_standard_command( "Compose Down", diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index aaee7402fb..893f192dd7 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -62,8 +62,9 @@ impl Resolve for GetContainerLog { } else { Default::default() }; + // `--` so a name beginning with `-` is not parsed as a flag. let command = - format!("docker logs {name} --tail {tail}{timestamps}"); + format!("docker logs --tail {tail}{timestamps} -- {name}"); Ok( run_komodo_standard_command( "Get container log", @@ -172,7 +173,7 @@ impl Resolve for StartContainer { Ok( run_komodo_standard_command( "Docker Start", - format!("docker start {}", self.name), + format!("docker start -- {}", self.name), CommandOptions::default(), ) .await, @@ -199,7 +200,7 @@ impl Resolve for RestartContainer { Ok( run_komodo_standard_command( "Docker Restart", - format!("docker restart {}", self.name), + format!("docker restart -- {}", self.name), CommandOptions::default(), ) .await, @@ -226,7 +227,7 @@ impl Resolve for PauseContainer { Ok( run_komodo_standard_command( "Docker Pause", - format!("docker pause {}", self.name), + format!("docker pause -- {}", self.name), CommandOptions::default(), ) .await, @@ -251,7 +252,7 @@ impl Resolve for UnpauseContainer { Ok( run_komodo_standard_command( "Docker Unpause", - format!("docker unpause {}", self.name), + format!("docker unpause -- {}", self.name), CommandOptions::default(), ) .await, @@ -326,7 +327,7 @@ impl Resolve for RemoveContainer { let name = escape(name.into()); let stop_command = stop_container_command(&name, signal, time); let command = - format!("{stop_command} && docker container rm {name}"); + format!("{stop_command} && docker container rm -- {name}"); let log = run_komodo_shell_command( "Docker Stop and Remove", command, @@ -336,7 +337,7 @@ impl Resolve for RemoveContainer { if log.stderr.contains("unknown flag: --signal") { let stop_command = stop_container_command(&name, None, time); let command = - format!("{stop_command} && docker container rm {name}"); + format!("{stop_command} && docker container rm -- {name}"); let mut log = run_komodo_shell_command( "Docker Stop and Remove", command, @@ -379,7 +380,7 @@ impl Resolve for RenameContainer { curr_name, new_name, } = self; - let command = format!("docker rename {curr_name} {new_name}"); + let command = format!("docker rename -- {curr_name} {new_name}"); Ok( run_komodo_standard_command( "Docker Rename", @@ -449,7 +450,7 @@ impl Resolve for StartAllContainers { { return None; } - let command = format!("docker start {name}"); + let command = format!("docker start -- {name}"); Some(async move { run_komodo_standard_command( &command.clone(), @@ -495,7 +496,7 @@ impl Resolve for RestartAllContainers { { return None; } - let command = format!("docker restart {name}"); + let command = format!("docker restart -- {name}"); Some(async move { run_komodo_standard_command( &command.clone(), @@ -541,7 +542,7 @@ impl Resolve for PauseAllContainers { { return None; } - let command = format!("docker pause {name}"); + let command = format!("docker pause -- {name}"); Some(async move { run_komodo_standard_command( &command.clone(), @@ -587,7 +588,7 @@ impl Resolve for UnpauseAllContainers { { return None; } - let command = format!("docker unpause {name}"); + let command = format!("docker unpause -- {name}"); Some(async move { run_komodo_standard_command( &command.clone(), diff --git a/bin/periphery/src/api/docker.rs b/bin/periphery/src/api/docker.rs index ce58a0eef8..93da4332f9 100644 --- a/bin/periphery/src/api/docker.rs +++ b/bin/periphery/src/api/docker.rs @@ -132,7 +132,7 @@ impl Resolve for PullImage { anyhow::Ok( run_komodo_standard_command( "Docker Pull", - format!("docker pull {name}"), + format!("docker pull -- {name}"), CommandOptions::default(), ) .await, @@ -164,7 +164,7 @@ impl Resolve for DeleteImage { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker image rm {}", self.name); + let command = format!("docker image rm -- {}", self.name); Ok( run_komodo_standard_command( "Delete Image", @@ -243,7 +243,7 @@ impl Resolve for CreateNetwork { Some(driver) => format!(" -d {driver}"), None => String::new(), }; - let command = format!("docker network create{driver} {name}"); + let command = format!("docker network create{driver} -- {name}"); Ok( run_komodo_standard_command( "Create Network", @@ -271,7 +271,7 @@ impl Resolve for DeleteNetwork { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker network rm {}", self.name); + let command = format!("docker network rm -- {}", self.name); Ok( run_komodo_standard_command( "Delete Network", @@ -344,7 +344,7 @@ impl Resolve for DeleteVolume { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker volume rm {}", self.name); + let command = format!("docker volume rm -- {}", self.name); Ok( run_komodo_standard_command( "Delete Volume", diff --git a/bin/periphery/src/api/swarm/mod.rs b/bin/periphery/src/api/swarm/mod.rs index 6817c8fe0f..cbc025f60e 100644 --- a/bin/periphery/src/api/swarm/mod.rs +++ b/bin/periphery/src/api/swarm/mod.rs @@ -112,7 +112,8 @@ impl Resolve for UpdateSwarmNode { } } - command += " "; + // `--` so a node beginning with `-` is not parsed as a flag. + command += " -- "; command += &self.node; Ok( @@ -145,6 +146,8 @@ impl Resolve for RemoveSwarmNodes { if self.force { command += " --force" } + // `--` so a node beginning with `-` is not parsed as a flag. + command += " --"; for node in self.nodes { command += " "; command += &node; diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index a6086b6b8e..151a6620a8 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -84,7 +84,7 @@ impl Resolve for GetSwarmServiceLog { Default::default() }; let command = format!( - "docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} {service}", + "docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} -- {service}", ); Ok( run_komodo_standard_command( @@ -163,6 +163,8 @@ impl Resolve for RemoveSwarmServices { args: &crate::api::Args, ) -> anyhow::Result { let mut command = String::from("docker service rm"); + // `--` so a service beginning with `-` is not parsed as a flag. + command += " --"; for service in self.services { command += " "; command += &service; @@ -195,7 +197,7 @@ impl Resolve for RollbackSwarmService { Ok( run_komodo_standard_command( "Rollback Swarm Service", - format!("docker service rollback {}", self.service), + format!("docker service rollback -- {}", self.service), CommandOptions::default(), ) .await, @@ -432,7 +434,8 @@ impl Resolve for UpdateSwarmService { push_extra_args(&mut command, &extra_args)?; - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let span = info_span!("ExecuteDockerServiceCreate"); let log = run_komodo_standard_command( diff --git a/bin/periphery/src/api/swarm/stack.rs b/bin/periphery/src/api/swarm/stack.rs index 93837e6b81..8ce8db0933 100644 --- a/bin/periphery/src/api/swarm/stack.rs +++ b/bin/periphery/src/api/swarm/stack.rs @@ -92,6 +92,8 @@ impl Resolve for RemoveSwarmStacks { if !self.detach { command += " --detach=false" } + // `--` so a stack beginning with `-` is not parsed as a flag. + command += " --"; for stack in self.stacks { command += " "; command += &stack; @@ -304,7 +306,8 @@ impl Resolve for DeploySwarmStack { command += " --with-registry-auth"; } push_extra_args(&mut command, &stack.config.extra_args)?; - write!(&mut command, " {project_name}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {project_name}")?; // Apply compose cmd wrapper if configured let (command, _) = match maybe_wrap_command( diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 98fe942938..5d3cf3e0ac 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -70,7 +70,9 @@ pub async fn inspect_swarm_config( ) -> anyhow::Result { let res = run_komodo_standard_command( "Inspect Swarm Config", - format!(r#"docker config inspect "{config}""#), + // `--` so a name beginning with `-` is not parsed as a flag. + // The quotes alone give no protection, as they are stripped when lexed. + format!(r#"docker config inspect -- "{config}""#), CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -151,6 +153,8 @@ pub async fn remove_swarm_configs( configs: impl Iterator, ) -> Log { let mut command = String::from("docker config rm"); + // `--` so a name beginning with `-` is not parsed as a flag. + command += " --"; for config in configs { command += " "; command += config; @@ -331,7 +335,8 @@ async fn switch_service_config( write!(&mut command, ",mode={mode}")?; } - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let log = run_komodo_standard_command( "Switch Service Config", diff --git a/bin/periphery/src/docker/mod.rs b/bin/periphery/src/docker/mod.rs index fd9789a1b2..d526837fb5 100644 --- a/bin/periphery/src/docker/mod.rs +++ b/bin/periphery/src/docker/mod.rs @@ -1,7 +1,7 @@ use anyhow::{Context, anyhow}; use bollard::Docker; use command::{ - CommandOptions, run_komodo_standard_command, run_shell_command, + CommandOptions, run_komodo_standard_command, run_standard_command, }; use komodo_client::entities::{ TerminationSignal, @@ -54,14 +54,17 @@ pub async fn docker_login( None => crate::helpers::registry_token(domain, account)?, }; - let log = run_shell_command( + // The token is written to the child's stdin rather than interpolated into + // the command, so it never reaches the process arguments, where any user + // on the host could read it out of `ps`. This runs without a shell, and + // `--` keeps a domain beginning with `-` from being parsed as a flag. + let log = run_standard_command( &format!( - "echo {} | docker login {} --username {} --password-stdin", - escape(registry_token.into()), - escape(domain.into()), + "docker login --username {} --password-stdin -- {}", escape(account.into()), + escape(domain.into()), ), - CommandOptions::default(), + CommandOptions::default().stdin(registry_token), ) .await; @@ -85,7 +88,7 @@ pub async fn docker_login( #[instrument("PullImage")] pub async fn pull_image(image: &str) -> Log { - let command = format!("docker pull {image}"); + let command = format!("docker pull -- {image}"); run_komodo_standard_command( "Docker Pull", command, @@ -105,7 +108,8 @@ pub fn stop_container_command( let time = time .map(|time| format!(" --time {time}")) .unwrap_or_default(); - format!("docker stop{signal}{time} {container_name}") + // `--` so a name beginning with `-` is not parsed as a flag. + format!("docker stop{signal}{time} -- {container_name}") } fn convert_object_version( diff --git a/bin/periphery/src/docker/secret.rs b/bin/periphery/src/docker/secret.rs index 0e55eef33f..01194d844d 100644 --- a/bin/periphery/src/docker/secret.rs +++ b/bin/periphery/src/docker/secret.rs @@ -163,6 +163,8 @@ pub async fn remove_swarm_secrets( secrets: impl Iterator, ) -> Log { let mut command = String::from("docker secret rm"); + // `--` so a name beginning with `-` is not parsed as a flag. + command += " --"; for secret in secrets { command += " "; command += secret; @@ -346,7 +348,8 @@ async fn switch_service_secret( write!(&mut command, ",mode={mode}")?; } - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let log = run_komodo_standard_command( "Switch Service Secret", diff --git a/bin/periphery/src/docker/stack.rs b/bin/periphery/src/docker/stack.rs index 749e53658b..eb9775ef70 100644 --- a/bin/periphery/src/docker/stack.rs +++ b/bin/periphery/src/docker/stack.rs @@ -132,7 +132,7 @@ pub async fn list_swarm_stack_service_ids( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Services", - format!("docker stack services --format json {stack}"), + format!("docker stack services --format json -- {stack}"), CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -162,7 +162,7 @@ pub async fn list_swarm_stack_tasks( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Tasks", - format!("docker stack ps --format json --no-trunc {stack}"), + format!("docker stack ps --format json --no-trunc -- {stack}"), CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index f2f6a5435f..a4e0749867 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -59,12 +59,14 @@ pub async fn get_container_stats( \"Name\":\"{{ .Name }}\", \ \"NetIO\":\"{{ .NetIO }}\",\ \"PIDs\":\"{{ .PIDs }}\"}'"; + // `--` follows the flags, so a name beginning with `-` is not + // parsed as a flag and the flags are not parsed as container names. let container_name = match container_name { - Some(name) => format!(" {name}"), + Some(name) => format!(" -- {name}"), None => "".to_string(), }; let command = - format!("docker stats{container_name} --no-stream {format}"); + format!("docker stats --no-stream {format}{container_name}"); let output = run_standard_command( &command, CommandOptions::default().timeout(Duration::from_secs(10)), diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index c42fd23a80..7cdf921088 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -120,7 +120,8 @@ pub fn output_into_log( /// Commands are run directly, and cannot include '&&'. /// -/// See [CommandOptions] for the available `timeout` / `cancel` controls. +/// See [CommandOptions] for the available `timeout` / `cancel` / `stdin` +/// controls. pub async fn run_standard_command( command: &str, options: CommandOptions<'_>, @@ -137,12 +138,7 @@ pub async fn run_standard_command( let mut cmd = Command::new(&lexed[0]); - cmd - .args(&lexed[1..]) - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + cmd.args(&lexed[1..]).kill_on_drop(true); run_command(cmd, options).await } @@ -167,17 +163,15 @@ fn shell() -> &'static str { /// Commands are wrapped in 'sh -c', and can include '&&'. /// -/// See [CommandOptions] for the available `timeout` / `cancel` controls. +/// See [CommandOptions] for the available `timeout` / `cancel` / `stdin` +/// controls. pub async fn run_shell_command( command: &str, options: CommandOptions<'_>, ) -> CommandOutput { let mut cmd = Command::new(shell()); - cmd - .args(["-c", command]) - .kill_on_drop(true) - .stdin(Stdio::null()); + cmd.args(["-c", command]).kill_on_drop(true); run_command(cmd, options).await } @@ -201,6 +195,7 @@ async fn run_command( path, timeout, cancel, + stdin, } = options; // Attach the path to cmd as current dir @@ -213,27 +208,56 @@ async fn run_command( } } - // Fast path: nothing to wait on besides the command itself. - if timeout.is_none() && cancel.is_none() { + // Anything to write needs a pipe, otherwise the command gets no input. + cmd.stdin(if stdin.is_some() { + Stdio::piped() + } else { + Stdio::null() + }); + + // Fast path: nothing to write, and nothing to wait + // on besides the command itself. + if timeout.is_none() && cancel.is_none() && stdin.is_none() { return CommandOutput::from(cmd.output().await); } + // `output()` configures stdout/stderr automatically, but since + // we spawn manually we set them here too. + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + // Place the child in a new process group so the whole group can be - // signalled together. `output()` configures stdout/stderr automatically, - // but since we spawn manually we set them here too. - cmd - .process_group(0) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - let child = match cmd.spawn() { + // signalled together. Only needed when there is something to signal on. + if timeout.is_some() || cancel.is_some() { + cmd.process_group(0); + } + + let mut child = match cmd.spawn() { Ok(child) => child, Err(e) => return CommandOutput::from_err(e), }; + // Written from a task, so that a payload larger than the pipe buffer + // cannot deadlock against a child which is filling up its stdout. + if let Some(stdin) = stdin + && let Some(mut pipe) = child.stdin.take() + { + let stdin = stdin.to_string(); + tokio::spawn(async move { + use tokio::io::AsyncWriteExt; + let _ = pipe.write_all(stdin.as_bytes()).await; + // Dropping the pipe closes the child's stdin, so it stops reading. + let _ = pipe.shutdown().await; + }); + } + // Because of `process_group(0)`, the child's pid equals its pgid. let pid = child.id(); + // Nothing to signal on, so just wait for it to finish. + if timeout.is_none() && cancel.is_none() { + return CommandOutput::from(child.wait_with_output().await); + } + // Resolve to `()` only when the relevant control fires; otherwise stay // pending forever so it never wins the `select!`. let on_timeout = async { @@ -326,6 +350,84 @@ mod tests { ); } + /// Neither runner configures stdout/stderr itself — the fast path relies + /// on `output()` doing it, and the spawn path on [run_command]. Both must + /// still capture, rather than leaking output to the parent. + #[tokio::test] + async fn output_is_captured_on_both_spawn_paths() { + // Fast path: no stdin, timeout or cancel, so `cmd.output()`. + let fast = + run_standard_command("echo out", CommandOptions::default()) + .await; + assert!(fast.success(), "{fast:?}"); + assert_eq!(fast.stdout.trim(), "out"); + + // Spawn path, reached here by setting a timeout. + let spawned = run_shell_command( + "echo out; echo err >&2", + CommandOptions::default().timeout(Duration::from_secs(10)), + ) + .await; + assert!(spawned.success(), "{spawned:?}"); + assert_eq!(spawned.stdout.trim(), "out"); + assert_eq!(spawned.stderr.trim(), "err"); + } + + /// Data set as [CommandOptions::stdin] must reach the child on its + /// standard input. + #[tokio::test] + async fn stdin_is_delivered_to_child() { + let payload = "some-secret-token\nwith two lines"; + let out = run_standard_command( + "cat", + CommandOptions::default().stdin(payload), + ) + .await; + + assert!(out.success(), "cat failed: {out:?}"); + assert_eq!(out.stdout, payload); + } + + /// The whole point of [CommandOptions::stdin]: a secret passed this way + /// must not be visible in the process arguments, where any user on the + /// host could read it with `ps`. The shell equivalent, which interpolates + /// the secret into the command, is checked alongside it to confirm the + /// test would actually catch a regression. + #[tokio::test] + async fn stdin_secret_is_not_in_process_args() { + let secret = "komodo-stdin-secret-marker"; + + let visible_in_ps = |marker: &'static str| async move { + // Give the child a moment to actually be running. + tokio::time::sleep(Duration::from_millis(300)).await; + let out = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + !String::from_utf8_lossy(&out.stdout).trim().is_empty() + }; + + let (_, via_stdin) = tokio::join!( + run_standard_command( + "sleep 1", + CommandOptions::default().stdin(secret) + ), + visible_in_ps(secret) + ); + assert!(!via_stdin, "secret passed via stdin showed up in ps"); + + // Control: interpolating it into the command does expose it. + let interpolated = format!("echo {secret} | sleep 1"); + let (_, via_command) = tokio::join!( + run_shell_command(&interpolated, CommandOptions::default()), + visible_in_ps(secret) + ); + assert!( + via_command, + "control failed: interpolated secret should be visible in ps" + ); + } + /// Cancelling the token mid-run must kill the whole process group, just /// like a timeout does, and return promptly with a cancellation error. #[tokio::test] diff --git a/lib/command/src/options.rs b/lib/command/src/options.rs index 5c78cf7be8..84a10a857d 100644 --- a/lib/command/src/options.rs +++ b/lib/command/src/options.rs @@ -17,6 +17,14 @@ pub struct CommandOptions<'a> { /// Kill the command (and its process group) when this token is /// cancelled, allowing cancellation from elsewhere. pub cancel: Option, + /// Write this to the command's standard input. Without it, + /// the command gets no input at all. + /// + /// Use this to hand a secret to a command which reads it from stdin, + /// such as `docker login --password-stdin`. Unlike interpolating the + /// secret into the command, it never appears in the process arguments, + /// where any user on the host could read it out of `ps`. + pub stdin: Option<&'a str>, } impl<'a> CommandOptions<'a> { @@ -24,6 +32,11 @@ impl<'a> CommandOptions<'a> { self.path = path.into(); self } + + pub fn stdin(mut self, stdin: impl Into>) -> Self { + self.stdin = stdin.into(); + self + } } impl CommandOptions<'_> { From debe6c782a326878a3270df182f39d5eeaf0274a Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 25 Jul 2026 17:10:08 -0700 Subject: [PATCH 198/203] deploy 2.3.0-dev-47 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4727d07baf..dce576bb12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "komodo_client", "libc", @@ -1716,7 +1716,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "async-compression", @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "bytes", @@ -2048,7 +2048,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "formatting", @@ -2200,7 +2200,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "mogh_error", ] @@ -2450,7 +2450,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "command", @@ -3224,7 +3224,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "komodo_client", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "bcrypt", @@ -3422,7 +3422,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "async_timing_util", @@ -3464,7 +3464,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "arc-swap", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "arc-swap", @@ -4654,7 +4654,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "encoding", @@ -7037,7 +7037,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "axum", @@ -8069,7 +8069,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index f72a084ae4..cd8e594bce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-46" +version = "2.3.0-dev-47" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From 930f747e4f5e55b9edbb5652224c0ea4ef9047cc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 26 Jul 2026 23:36:23 -0700 Subject: [PATCH 199/203] Delete only batch executions just use delete button --- ui/src/components/docker/batch-executions.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ui/src/components/docker/batch-executions.tsx b/ui/src/components/docker/batch-executions.tsx index 0f05420300..57fd505044 100644 --- a/ui/src/components/docker/batch-executions.tsx +++ b/ui/src/components/docker/batch-executions.tsx @@ -106,6 +106,23 @@ function DockerBatchExecutionsDropdownMenu({ base: "target", xs: 250, }); + + if (executions.length === 0) { + return ( + + ); + } + return ( From d504012b01db27cfe5f4b3a1a5cac13aec80ecfc Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 26 Jul 2026 23:55:55 -0700 Subject: [PATCH 200/203] add New / batch executions to dashboard tables --- ui/src/pages/dashboard/tables.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 7df20d9d16..30c4b452c4 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -110,6 +110,7 @@ function TableSection({
} + gap="0.2rem" titleNode={ } - actions={ - - {show && ( - - )} + titleRight={ + + + + } > From ed4c1fadeba747d4f9975c936b4096fd7be16ada Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 26 Jul 2026 23:57:19 -0700 Subject: [PATCH 201/203] rust 1.97.1 --- bin/binaries.Dockerfile | 2 +- bin/chef.binaries.Dockerfile | 2 +- bin/cli/aio.Dockerfile | 2 +- bin/core/aio.Dockerfile | 2 +- bin/periphery/aio.Dockerfile | 2 +- example/alerter/Dockerfile | 2 +- example/update_logger/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/binaries.Dockerfile b/bin/binaries.Dockerfile index b4633243f0..4188bb0f8e 100644 --- a/bin/binaries.Dockerfile +++ b/bin/binaries.Dockerfile @@ -1,7 +1,7 @@ ## Builds the Komodo Core, Periphery, and Util binaries ## for a specific architecture. Requires OpenSSL 3 or later. -FROM rust:1.96.1-bookworm AS builder +FROM rust:1.97.1-bookworm AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/chef.binaries.Dockerfile b/bin/chef.binaries.Dockerfile index 7c79f58ddf..89acc13a7e 100644 --- a/bin/chef.binaries.Dockerfile +++ b/bin/chef.binaries.Dockerfile @@ -3,7 +3,7 @@ ## Uses chef for dependency caching to help speed up back-to-back builds. -FROM lukemathwalker/cargo-chef:latest-rust-1.96.1-bookworm AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.97.1-bookworm AS chef WORKDIR /builder # Plan just the RECIPE to see if things have changed diff --git a/bin/cli/aio.Dockerfile b/bin/cli/aio.Dockerfile index 5c3f5544bb..d1564d5446 100644 --- a/bin/cli/aio.Dockerfile +++ b/bin/cli/aio.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.1-trixie AS builder +FROM rust:1.97.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index 6c326cf689..2cbcc7b5f9 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -1,7 +1,7 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. # Build Core -FROM rust:1.96.1-trixie AS core-builder +FROM rust:1.97.1-trixie AS core-builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/periphery/aio.Dockerfile b/bin/periphery/aio.Dockerfile index fa57040f66..0e045fa8da 100644 --- a/bin/periphery/aio.Dockerfile +++ b/bin/periphery/aio.Dockerfile @@ -1,6 +1,6 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. -FROM rust:1.96.1-trixie AS builder +FROM rust:1.97.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/example/alerter/Dockerfile b/example/alerter/Dockerfile index 3a35e4c41c..20e51f0e0a 100644 --- a/example/alerter/Dockerfile +++ b/example/alerter/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.1-trixie as builder +FROM rust:1.97.1-trixie as builder WORKDIR /builder COPY . . diff --git a/example/update_logger/Dockerfile b/example/update_logger/Dockerfile index b2073aedf1..26533c8bb9 100644 --- a/example/update_logger/Dockerfile +++ b/example/update_logger/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.96.1-trixie as builder +FROM rust:1.97.1-trixie as builder WORKDIR /builder COPY . . From fbdef2d4b484fb461ebd9255bd1c96ae3c9f2d54 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 26 Jul 2026 23:58:18 -0700 Subject: [PATCH 202/203] deploy 2.3.0-dev-48 --- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dce576bb12..46a59ae0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "command" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "komodo_client", "libc", @@ -1716,7 +1716,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "async-compression", @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "bytes", @@ -2048,7 +2048,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "formatting", @@ -2200,7 +2200,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "mogh_error", ] @@ -2450,7 +2450,7 @@ dependencies = [ [[package]] name = "git" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "command", @@ -3224,7 +3224,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "komodo_client", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "bcrypt", @@ -3422,7 +3422,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "async_timing_util", @@ -3464,7 +3464,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "arc-swap", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "arc-swap", @@ -4654,7 +4654,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "encoding", @@ -7037,7 +7037,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "axum", @@ -8069,7 +8069,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index cd8e594bce..286aa7dac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "2.3.0-dev-47" +version = "2.3.0-dev-48" edition = "2024" license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" From c5baa4ea18c60f421737a95a1760c34738204e19 Mon Sep 17 00:00:00 2001 From: The Dancing Developer <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:38:13 +0000 Subject: [PATCH 203/203] fix(ui): select exact confirmation text on double click --- .../components/confirm-modal-with-disable.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ui/src/components/confirm-modal-with-disable.tsx b/ui/src/components/confirm-modal-with-disable.tsx index 368b922c57..a0d9940409 100644 --- a/ui/src/components/confirm-modal-with-disable.tsx +++ b/ui/src/components/confirm-modal-with-disable.tsx @@ -10,5 +10,26 @@ export default function ConfirmModalWithDisable({ ...props }: ConfirmModalWithDisableProps) { const disabled = useRead("GetCoreInfo", {}).data?.disable_confirm_dialog; - return ; + return ( + { + const target = event.target; + const confirmation = + target instanceof Element ? target.closest("b") : null; + + if (confirmation?.textContent !== props.confirmText) return; + + event.preventDefault(); + const selection = window.getSelection(); + if (!selection) return; + + const range = document.createRange(); + range.selectNodeContents(confirmation); + selection.removeAllRanges(); + selection.addRange(range); + }} + {...props} + /> + ); }