Compare commits

...

52 Commits

Author SHA1 Message Date
mbecker20
4aebd5ed92 fix confirm update showing entries which have not changed 2026-03-15 16:44:51 -07:00
mbecker20
a9352ad90b improve deployment network, restart, termination signal config 2026-03-15 15:26:47 -07:00
mbecker20
37bd221609 improve procedure config UI including run stack service 2026-03-15 15:20:26 -07:00
mbecker20
bf5975bb93 fix repo Links config 2026-03-15 01:36:18 -07:00
mbecker20
d7706a32c1 Fix Pull repo 2026-03-14 17:34:35 -07:00
mbecker20
dea9c49772 add missing Repo header Info 2026-03-14 17:15:12 -07:00
mbecker20
95666e69d8 sidebar more compact 2026-03-13 04:17:20 -07:00
mbecker20
cbc008fc89 improve tabs 2026-03-13 04:02:19 -07:00
mbecker20
445fa42a01 improve batch execs 2026-03-13 03:05:19 -07:00
mbecker20
2c2b0eda8f better api key create 2026-03-13 02:25:06 -07:00
mbecker20
15eefa3385 Fix api key modal too thin 2026-03-13 01:49:20 -07:00
mbecker20
cb9bc80346 confirm modal better responsiveness 2026-03-13 01:36:40 -07:00
mbecker20
8c682c091f better responsive confirm save width 2026-03-13 01:17:23 -07:00
mbecker20
ca021a3979 km cli needs to install crypto provider for tls ws 2026-03-10 16:24:51 -07:00
mbecker20
1480c3b020 better maxheight for mobile friendly tabs 2026-03-09 03:13:40 -07:00
mbecker20
42f452fdda rename resource invalidates 2026-03-08 22:38:41 -07:00
mbecker20
8ded07dfe5 improve mobile friendly tabs width, and onboardng key copy 2026-03-08 21:53:00 -07:00
mbecker20
d51c1cb1e7 fix init admin user 2026-03-08 07:48:20 -07:00
mbecker20
b5f184b286 proper base64url decode 2026-03-05 11:37:15 -08:00
mbecker20
294ef20019 deploy 2.0.0-dev-124 2026-03-05 11:28:54 -08:00
mbecker20
3efcaa0740 confirm action / save modals don't need ConfirmButton 2026-03-05 11:06:03 -08:00
mbecker20
b5b680dd1d Deploy / DeployStack updates invalidate corresponding list query 2026-03-05 10:40:19 -08:00
mbecker20
bf5ac8b6ac deploy 2.0.0-dev-123 2026-03-05 10:32:39 -08:00
mbecker20
dcccc878c8 mogh auth 1.2.11 2026-03-05 10:27:37 -08:00
mbecker20
1c87fba8f5 mogh auth server 1.2.10 2026-03-05 10:04:25 -08:00
mbecker20
2fc35b3c2d debug level core <> periphery auth identifiers logs 2026-03-05 00:10:30 -08:00
mbecker20
2012fd1dd9 deploy 2.0.0-dev-122 2026-03-04 19:37:13 -08:00
mbecker20
550c0339d6 read request are trace 2026-03-04 19:36:34 -08:00
mbecker20
50cf2f2d50 fmt 2026-03-04 16:31:13 -08:00
mbecker20
b223fefec6 notification green contents written success 2026-03-04 16:19:44 -08:00
mbecker20
7fe56f72ae setters use maps instead of mutations 2026-03-04 16:14:57 -08:00
mbecker20
0a9bc397ca config sidebar save 2026-03-04 15:58:09 -08:00
mbecker20
a03fceba7f thinner topbar 2026-03-04 10:46:45 -08:00
mbecker20
1bf1574c2a rename for clarity 2026-03-03 12:40:25 -08:00
mbecker20
83d90e0a16 improve disk usage hover card styling 2026-03-02 20:39:50 -08:00
mbecker20
304ffbf01d Add hoverable disk info 2026-03-02 20:32:34 -08:00
mbecker20
07787d6fa1 "never" -> "Never" 2026-03-02 19:59:30 -08:00
mbecker20
5d04142a99 fix sidebar margin right 2026-02-27 14:21:15 -08:00
mbecker20
aecba3be9f refine lg screen size view 2026-02-27 14:19:28 -08:00
mbecker20
4c4e5b62e0 taller data table and blue omnisearch 2026-02-27 13:10:46 -08:00
mbecker20
62f0ca9093 tweaks for lg size 2026-02-27 03:39:01 -08:00
mbecker20
3d43e2419f Update / Alert table filter selector formatted 2026-02-27 02:06:35 -08:00
mbecker20
d4081c2d6b single delete terminal 2026-02-26 18:52:39 -08:00
mbecker20
d397cb4ea4 fix terminal height - same as logs 2026-02-26 18:35:03 -08:00
mbecker20
0c96e24cd4 fix some wrapping stuff in tables and tag text disappear 2026-02-26 18:15:39 -08:00
mbecker20
1c90e768ef fix login github / google icon color 2026-02-26 16:55:01 -08:00
mbecker20
be73f20fd5 improve login styling 2026-02-26 16:47:15 -08:00
mbecker20
330178dbb8 standard api key modal size 2026-02-26 16:30:03 -08:00
mbecker20
c8c01307a0 improve profile delete styling 2026-02-26 16:27:42 -08:00
mbecker20
2e80adff2d deploy 2.0.0-dev-121 2026-02-26 16:13:04 -08:00
mbecker20
ef5a0982cb post link redirect should be to profile 2026-02-26 15:56:37 -08:00
mbecker20
9ffa40022d topbar user dropdown shows user avatar if available 2026-02-26 15:49:03 -08:00
111 changed files with 1191 additions and 554 deletions

167
Cargo.lock generated
View File

@@ -234,9 +234,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.14"
version = "1.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2"
checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -254,7 +254,7 @@ dependencies = [
"fastrand",
"hex",
"http 1.4.0",
"ring",
"sha1",
"time",
"tokio",
"tracing",
@@ -264,9 +264,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.13"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0"
checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -299,9 +299,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.7.1"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75"
checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -324,9 +324,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.214.0"
version = "1.216.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5011a07318505db9ffe5a03e667653b44ee85d392f0e5f36ebd1fff2f2cbfa8"
checksum = "0bcafb17cfb3978dbf5b5a637d101a6212bc080cb33b5881da65f87967aedf67"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -349,9 +349,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.95.0"
version = "1.96.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c5ff27c6ba2cbd95e6e26e2e736676fdf6bcf96495b187733f521cfe4ce448"
checksum = "f64a6eded248c6b453966e915d32aeddb48ea63ad17932682774eb026fbef5b1"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -373,9 +373,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.97.0"
version = "1.98.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d186f1e5a3694a188e5a0640b3115ccc6e084d104e16fd6ba968dca072ffef8"
checksum = "db96d720d3c622fcbe08bae1c4b04a72ce6257d8b0584cb5418da00ae20a344f"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -397,9 +397,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.99.0"
version = "1.100.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449"
checksum = "fafbdda43b93f57f699c5dfe8328db590b967b8a820a13ccdd6687355dfcc7ca"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -422,9 +422,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.4.1"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9"
checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
@@ -444,9 +444,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "1.2.13"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13"
checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc"
dependencies = [
"futures-util",
"pin-project-lite",
@@ -455,9 +455,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.63.5"
version = "0.63.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7"
checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
@@ -476,9 +476,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.11"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00ccbb08c10f6bcf912f398188e42ee2eab5f1767ce215a02a73bc5df1bbdd95"
checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -506,27 +506,27 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.62.4"
version = "0.62.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb"
checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-observability"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b"
checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.14"
version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0"
checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd"
dependencies = [
"aws-smithy-types",
"urlencoding",
@@ -534,9 +534,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.10.2"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf"
checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -559,9 +559,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.11.5"
version = "1.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516"
checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -576,9 +576,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.4.5"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3"
checksum = "d2b1117b3b2bbe166d11199b540ceed0d0f7676e36e7b962b5a437a9971eac75"
dependencies = [
"base64-simd",
"bytes",
@@ -602,18 +602,18 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.14"
version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.3.13"
version = "1.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96"
checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -787,13 +787,13 @@ dependencies = [
[[package]]
name = "bcrypt"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a0f5948f30df5f43ac29d310b7476793be97c50787e6ef4a63d960a0d0be827"
checksum = "523ab528ce3a7ada6597f8ccf5bd8d85ebe26d5edf311cad4d1d3cfb2d357ac6"
dependencies = [
"base64 0.22.1",
"blowfish",
"getrandom 0.3.4",
"getrandom 0.4.1",
"subtle",
"zeroize",
]
@@ -1149,7 +1149,7 @@ dependencies = [
[[package]]
name = "command"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"komodo_client",
"shlex",
@@ -1489,7 +1489,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "database"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"async-compression",
@@ -1759,7 +1759,7 @@ dependencies = [
[[package]]
name = "encoding"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"bytes",
@@ -1801,7 +1801,7 @@ dependencies = [
[[package]]
name = "environment"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"formatting",
@@ -1930,7 +1930,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"mogh_error",
]
@@ -2109,7 +2109,7 @@ dependencies = [
[[package]]
name = "git"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"command",
@@ -2709,7 +2709,7 @@ dependencies = [
[[package]]
name = "interpolate"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"komodo_client",
@@ -2835,7 +2835,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"chrono",
@@ -2852,6 +2852,7 @@ dependencies = [
"mogh_logger",
"mogh_pki",
"mogh_secret_file",
"rustls 0.23.37",
"serde",
"serde_json",
"serde_qs",
@@ -2863,7 +2864,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2902,7 +2903,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"arc-swap",
@@ -2960,7 +2961,7 @@ dependencies = [
"svi",
"tokio",
"tokio-util",
"toml",
"toml 1.0.4+spec-1.1.0",
"toml_pretty",
"tracing",
"transport",
@@ -2975,7 +2976,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"arc-swap",
@@ -3224,9 +3225,9 @@ dependencies = [
[[package]]
name = "mogh_auth_server"
version = "1.2.6"
version = "1.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0053e21d85b214a3288c50433cf56cd07ac1dacda66201f5e93205810423daec"
checksum = "e748cc16d2f7d9a86fbab3eff5f2e847df205db8ff5aa27728e45271f96d327b"
dependencies = [
"anyhow",
"arc-swap",
@@ -3280,7 +3281,7 @@ dependencies = [
"serde_json",
"serde_yaml_ng",
"thiserror 2.0.18",
"toml",
"toml 0.9.11+spec-1.1.0",
"wildcard",
]
@@ -3392,9 +3393,9 @@ dependencies = [
[[package]]
name = "mogh_server"
version = "1.4.4"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3292c49e7483691c6eef791fa96aa2120df97b69241514313e53207945c3d69"
checksum = "66ab833484ee2242003819a2b948421dc3469e7d153db71bd7579d5de2660b81"
dependencies = [
"anyhow",
"axum",
@@ -4030,7 +4031,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "periphery_client"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"encoding",
@@ -4082,9 +4083,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
@@ -5406,9 +5407,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.38.2"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efc19935b4b66baa6f654ac7924c192f55b175c00a7ab72410fc24284dacda8"
checksum = "d03c61d2a49c649a15c407338afe7accafde9dac869995dccb73e5f7ef7d9034"
dependencies = [
"libc",
"memchr",
@@ -5573,9 +5574,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
@@ -5670,7 +5671,22 @@ dependencies = [
"indexmap 2.13.0",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml"
version = "1.0.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94c3321114413476740df133f0d8862c61d87c8d26f04c6841e033c8c80db47"
dependencies = [
"indexmap 2.13.0",
"serde_core",
"serde_spanned",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
@@ -5686,10 +5702,19 @@ dependencies = [
]
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
@@ -5983,7 +6008,7 @@ dependencies = [
[[package]]
name = "transport"
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
dependencies = [
"anyhow",
"axum",

View File

@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "2.0.0-dev-120"
version = "2.0.0-dev-124"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -37,7 +37,7 @@ mogh_error = { version = "1.0.3", default-features = false }
derive_default_builder = "0.1.8"
async_timing_util = "1.1.0"
mogh_auth_client = "1.2.2"
mogh_auth_server = "1.2.6"
mogh_auth_server = "1.2.12"
mogh_secret_file = "1.0.1"
mogh_validations = "1.0.1"
mogh_rate_limit = "1.0.1"
@@ -46,7 +46,7 @@ mongo_indexed = "2.0.2"
mogh_resolver = "1.0.0"
mogh_config = "1.0.2"
mogh_logger = "1.3.1"
mogh_server = "1.4.4"
mogh_server = "1.4.5"
toml_pretty = "2.0.0"
mogh_cache = "1.1.1"
mogh_pki = "1.1.0"
@@ -55,10 +55,10 @@ svi = "1.2.0"
# ASYNC
reqwest = { version = "0.13.2", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
tokio-stream = { version = "0.1.18", features = ["sync"] }
pin-project-lite = "0.2.16"
pin-project-lite = "0.2.17"
futures-util = "0.3.32"
arc-swap = "1.8.2"
@@ -77,7 +77,7 @@ indexmap = { version = "2.13.0", features = ["serde"] }
serde = { version = "1.0.227", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "0.9.11"
toml = "1.0.4"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.149"
serde_qs = "1.0.0"
@@ -100,7 +100,7 @@ uuid = { version = "1.21.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
data-encoding = "2.10.0"
urlencoding = "2.1.3"
bcrypt = "0.18.0"
bcrypt = "0.19.0"
hmac = "0.12.1"
sha1 = "0.10.6"
sha2 = "0.10.9"
@@ -113,13 +113,13 @@ portable-pty = "0.9.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.20.1"
sysinfo = "0.38.2"
sysinfo = "0.38.3"
shlex = "1.3.0"
# CLOUD
aws-config = "1.8.14"
aws-sdk-ec2 = "1.214.0"
aws-credential-types = "1.2.13"
aws-config = "1.8.15"
aws-sdk-ec2 = "1.216.0"
aws-credential-types = "1.2.14"
## CRON
english-to-cron = "0.1.7"

View File

@@ -33,6 +33,7 @@ colored.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
chrono.workspace = true
rustls.workspace = true
tokio.workspace = true
serde.workspace = true
clap.workspace = true

View File

@@ -12,6 +12,9 @@ mod config;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("Failed to install default crypto provider");
mogh_logger::init(&config::cli_config().cli_logging)?;
let args = config::cli_args();
let env = config::cli_env();

View File

@@ -293,7 +293,7 @@ async fn handler(
let req_id = Uuid::new_v4();
let variant: ReadRequestVariant = (&request).into();
debug!(
trace!(
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
@@ -301,7 +301,7 @@ async fn handler(
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
debug!(
trace!(
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
e.error
);

View File

@@ -148,7 +148,7 @@ impl AuthImpl for KomodoAuthImpl {
fn post_link_redirect(&self) -> &str {
static POST_LINK_REDIRECT: LazyLock<String> =
LazyLock::new(|| format!("{}/settings", core_config().host));
LazyLock::new(|| format!("{}/profile", core_config().host));
&POST_LINK_REDIRECT
}

View File

@@ -70,6 +70,14 @@ impl PeripheryConnectionArgs<'_> {
}
};
debug!(
host = identifiers.host(),
query = core_connection_query(),
sec_websocket_accept = accept.to_str().unwrap_or_default(),
resource_id = connection.args.id,
"[PERIPHERY AUTH] Zero trust identifiers"
);
let identifiers = identifiers.build(
accept.as_bytes(),
core_connection_query().as_bytes(),

View File

@@ -129,6 +129,14 @@ async fn existing_server_handler(
return;
};
debug!(
host = identifiers.host.to_str().unwrap_or_default(),
query,
sec_websocket_accept = identifiers.accept,
resource_id = &server.id,
"[PERIPHERY AUTH] Zero trust identifiers"
);
let span = info_span!(
"PeripheryLogin",
server_id = server.id,

View File

@@ -33,6 +33,7 @@ use komodo_client::{
user::{action_user, system_user},
},
};
use mogh_auth_server::api::login::local::sign_up_local_user;
use mogh_resolver::Resolve;
use uuid::Uuid;
@@ -41,6 +42,7 @@ use crate::{
execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
auth::KomodoAuthImpl,
config::core_config,
helpers::update::init_execution_update,
network, resource,
@@ -301,7 +303,7 @@ async fn ensure_init_user_and_resources() {
// Assumes if there are any existing users, procedures, or tags,
// the default procedures do not need to be set up.
let Ok((None, None, None)) = tokio::try_join!(
let Ok((None, procedures, tags)) = tokio::try_join!(
db.users.find_one(Document::new()),
db.procedures.find_one(Document::new()),
db.tags.find_one(Document::new()),
@@ -314,20 +316,16 @@ async fn ensure_init_user_and_resources() {
// Init admin user if set in config.
if let Some(username) = &config.init_admin_username {
info!("Creating init admin user...");
// if let Err(e) = (SignUpLocalUser {
// username: username.clone(),
// password: config.init_admin_password.clone(),
// })
// .resolve(&AuthArgs {
// headers: Default::default(),
// ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
// session: None,
// })
// .await
// {
// error!("Failed to create init admin user | {:#}", e.error);
// return;
// }
if let Err(e) = sign_up_local_user(
&KomodoAuthImpl,
username.to_string(),
&config.init_admin_password,
)
.await
{
error!("Failed to create init admin user | {:#}", e.error);
return;
}
match db
.users
.find_one(doc! { "username": username })
@@ -347,7 +345,10 @@ async fn ensure_init_user_and_resources() {
}
}
if config.disable_init_resources {
if config.disable_init_resources
|| procedures.is_some()
|| tags.is_some()
{
info!("System resources init {}", "DISABLED".red());
return;
}

View File

@@ -94,6 +94,13 @@ pub async fn handler(
already_logged_connection_error = false;
debug!(
host = identifiers.host(),
query,
sec_websocket_accept = accept.to_str().unwrap_or_default(),
"[CORE AUTH] Zero trust identifiers"
);
let identifiers =
identifiers.build(accept.as_bytes(), query.as_bytes());

View File

@@ -109,6 +109,13 @@ async fn handler(
let query = format!("core={}", urlencoding::encode(&core));
debug!(
host = identifiers.host.to_str().unwrap_or_default(),
query,
sec_websocket_accept = identifiers.accept,
"[CORE AUTH] Zero trust identifiers"
);
if let Err(e) =
handle_login(&mut socket, identifiers.build(query.as_bytes()))
.await

View File

@@ -1049,7 +1049,7 @@ impl mogh_server::session::SessionConfig for &CoreConfig {
"KOMODO_HOST"
}
fn expiry_seconds(&self) -> i64 {
60
60 * 3
}
fn allow_cross_site(&self) -> bool {
self.session_allow_cross_site

View File

@@ -203,7 +203,9 @@ impl Default for UserConfig {
impl UserConfig {
pub fn sanitize(&mut self) {
if let UserConfig::Local { password } = self {
password.clear();
if !password.is_empty() {
*password = "#".repeat(8);
}
}
}
}

View File

@@ -13,7 +13,7 @@
"build": "tsc"
},
"dependencies": {
"mogh_auth_client": "^1.2.0"
"mogh_auth_client": "^1.2.1"
},
"devDependencies": {
"typescript": "^5.6.3"

View File

@@ -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.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.2.0.tgz#d840abb85c967133570f247e0d671ac4054db3ff"
integrity sha512-d60lSNzbOJcZwwSeyn7jLBmNI0FQTpXtOjR/OJj3M5KnUpmofpjlgGqLYGbVsZkoVipqohhi7aM2uVHEiicCVQ==
mogh_auth_client@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.2.1.tgz#c8b5e9da101dc8da7b30586e5c5463f5f7a95edb"
integrity sha512-8uUjgqagwbMW8BKtTRzfQ4txpw+54hqLszbIvcYf99murVIQu5+NsRZ8/vjP/fOMawgXJXCVsR6O2lamm1BCnQ==
dependencies:
jwt-decode "^4.0.0"

View File

@@ -295,8 +295,8 @@ impl AddressConnectionIdentifiers {
/// Used to extract owned connection identifier
/// in server side connection handler.
pub struct HeaderConnectionIdentifiers {
host: HeaderValue,
accept: String,
pub host: HeaderValue,
pub accept: String,
}
impl HeaderConnectionIdentifiers {
@@ -310,7 +310,7 @@ impl HeaderConnectionIdentifiers {
let key = headers
.remove("sec-websocket-key")
.context("Headers do not contain Sec-Websocket-Key")?;
let accept = compute_accept(key.as_bytes());
let accept = compute_sec_websocket_accept(key.as_bytes());
Ok(HeaderConnectionIdentifiers { host, accept })
}
@@ -334,7 +334,7 @@ impl HeaderConnectionIdentifiers {
}
}
pub fn compute_accept(sec_websocket_key: &[u8]) -> String {
pub fn compute_sec_websocket_accept(sec_websocket_key: &[u8]) -> String {
// This is standard GUID to compute Sec-Websocket-Accept
const GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
let mut sha1 = sha1::Sha1::new();

View File

@@ -5,7 +5,7 @@
width="438.549px" height="438.549px" viewBox="0 0 438.549 438.549"
xml:space="preserve">
<g>
<path fill="#fceade" d="M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365
<path fill="#000" d="M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365
c-39.781,0-76.472,9.804-110.063,29.408c-33.596,19.605-60.192,46.204-79.8,79.8C9.803,148.168,0,184.854,0,224.63
c0,47.78,13.94,90.745,41.827,128.906c27.884,38.164,63.906,64.572,108.063,79.227c5.14,0.954,8.945,0.283,11.419-1.996
c2.475-2.282,3.711-5.14,3.711-8.562c0-0.571-0.049-5.708-0.144-15.417c-0.098-9.709-0.144-18.179-0.144-25.406l-6.567,1.136

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -3,7 +3,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 210 210" xml:space="preserve">
<path fill="#fceade" d="M0,105C0,47.103,47.103,0,105,0c23.383,0,45.515,7.523,64.004,21.756l-24.4,31.696C133.172,44.652,119.477,40,105,40
<path fill="#000" d="M0,105C0,47.103,47.103,0,105,0c23.383,0,45.515,7.523,64.004,21.756l-24.4,31.696C133.172,44.652,119.477,40,105,40
c-35.841,0-65,29.159-65,65s29.159,65,65,65c28.867,0,53.398-18.913,61.852-45H105V85h105v20c0,57.897-47.103,105-105,105
S0,162.897,0,105z"/>
</svg>

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 703 B

View File

@@ -8,7 +8,7 @@ import LoadingScreen from "@/ui/loading-screen";
import UpdateDetails from "@/components/updates/details";
import AlertDetails from "@/components/alerts/details";
export const TOPBAR_HEIGHT = 70;
export const TOPBAR_HEIGHT = 62;
const App = () => {
const [opened, { toggle, close }] = useDisclosure();

View File

@@ -14,10 +14,10 @@ const Sidebar = ({ close }: { close: () => void }) => {
const location = useLocation().pathname;
const linkProps = { nav, location };
return (
<Stack justify="space-between" gap="md" h="100%" m="xl" mt="24">
<Stack justify="space-between" gap="md" h="96%" m="xl" mt="24" mr="md">
{/* TOP AREA (scrolling) */}
<ScrollArea>
<Stack gap="0.25rem">
<Stack gap="0.15rem" mr="md">
<SidebarLink
label="Dashboard"
icon={<ICONS.Dashboard size="1rem" />}
@@ -43,7 +43,7 @@ const Sidebar = ({ close }: { close: () => void }) => {
Resources
</Text>
}
my="xs"
my="0.1rem"
/>
{SIDEBAR_RESOURCES.map((type) => {
@@ -65,7 +65,7 @@ const Sidebar = ({ close }: { close: () => void }) => {
Notifications
</Text>
}
my="xs"
my="0.1rem"
/>
<SidebarLink

View File

@@ -23,7 +23,7 @@ export default function TopbarAlerts() {
<Menu
opened={opened}
position="bottom"
offset={20}
offset={16}
onOpen={open}
onClose={close}
>

View File

@@ -0,0 +1,8 @@
.spotlight-action[data-selected] {
// hexColorByIntention("Neutral")
background-color: #3B82F6;
// ALTERNATE
// background-color: var(--mantine-color-accent-9);
// color: inherit;
}

View File

@@ -3,6 +3,7 @@ import { Spotlight, spotlight } from "@mantine/spotlight";
import { useOmniSearch } from "./hooks";
import { ICONS } from "@/theme/icons";
import { useShiftKeyListener } from "@/lib/hooks";
import classes from "./index.module.scss";
export default function OmniSearch({}: {}) {
const { search, setSearch, actions } = useOmniSearch();
@@ -48,7 +49,11 @@ export default function OmniSearch({}: {}) {
{actions.map((group) => (
<Spotlight.ActionsGroup key={group.group} label={group.group}>
{group.actions.map((action) => (
<Spotlight.Action key={action.id} {...action} />
<Spotlight.Action
key={action.id}
className={classes["spotlight-action"]}
{...action}
/>
))}
</Spotlight.ActionsGroup>
))}

View File

@@ -35,7 +35,7 @@ export default function TopbarUpdates() {
<Menu
opened={opened}
position="bottom"
offset={20}
offset={16}
onOpen={() => {
open();
setLastSeenUpdate({});

View File

@@ -35,18 +35,32 @@ export default function UserDropdown() {
}
};
const user = useUser().data;
const avatar = (user?.config.data as { avatar?: string }).avatar;
const userInvalidate = useUserInvalidate();
const accounts = MoghAuth.LOGIN_TOKENS.accounts();
const nav = useNavigate();
return (
<Menu offset={20} opened={open} onChange={setOpen}>
<Menu offset={13} opened={open} onChange={setOpen}>
<Menu.Target>
<Button
variant="subtle"
size="lg"
leftSection={<User size="1.3rem" />}
pl="0.5rem"
pr={{ base: "-20", lg: "0.5rem" }}
leftSection={
avatar ? (
<img
src={avatar}
alt="avatar"
style={{
width: "1.1rem",
height: "1.1rem",
}}
/>
) : (
<User size="1.3rem" />
)
}
pl="0.7rem"
pr={{ base: "-20", lg: "0.7rem" }}
>
<Username username={user?.username} />
</Button>
@@ -135,7 +149,7 @@ export default function UserDropdown() {
function Account({
login,
current_id,
current_id: currentId,
setOpen,
rerender,
viewLogout,
@@ -146,17 +160,17 @@ function Account({
rerender: () => void;
viewLogout: boolean;
}) {
const user_id = useMemo(
const userId = useMemo(
() => MoghAuth.extractUserIdFromJwt(login.jwt),
[login.jwt],
);
const { data: user } = useRead(
"GetUsername",
{ user_id: user_id! },
{ enabled: !!user_id },
{ user_id: userId! },
{ enabled: !!userId },
);
if (!user_id || !user) return;
const selected = user_id === current_id;
if (!userId || !user) return;
const selected = userId === currentId;
return (
<Flex align="center" gap="md" w="100%">
<Button
@@ -177,7 +191,7 @@ function Account({
setOpen(false);
return;
}
MoghAuth.LOGIN_TOKENS.change(user_id);
MoghAuth.LOGIN_TOKENS.change(userId);
location.reload();
}}
>
@@ -185,7 +199,7 @@ function Account({
<img
src={user.avatar}
alt="avatar"
style={{ width: "1.3rem", height: "1.3rem", marginRight: "0.5rem" }}
style={{ width: "1.1rem", height: "1.1rem", marginRight: "0.5rem" }}
/>
)}
{!user.avatar && (
@@ -198,7 +212,7 @@ function Account({
<ActionIcon
color="red"
onClick={() => {
MoghAuth.LOGIN_TOKENS.remove(user_id);
MoghAuth.LOGIN_TOKENS.remove(userId);
if (selected) {
location.reload();
} else {

View File

@@ -35,7 +35,7 @@ export default function WebsocketStatus() {
{/* The hovercard can open unexpectedly on mobile so is hidden */}
<Box hiddenFrom="sm">{Target}</Box>
<Box visibleFrom="sm">
<HoverCard offset={20}>
<HoverCard offset={16}>
<HoverCard.Target>{Target}</HoverCard.Target>
<HoverCard.Dropdown>
<Text>Websocket Status</Text>

View File

@@ -15,7 +15,7 @@ import { Types } from "komodo_client";
import { useState } from "react";
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
type ExpiresOptions = "90 days" | "180 days" | "1 year" | "never";
type ExpiresOptions = "90 days" | "180 days" | "1 year" | "Never";
export interface NewApiKeyProps {
/** For service user api keys */
@@ -51,7 +51,7 @@ export default function NewApiKey({ userId }: { userId?: string }) {
"90 days": now + ONE_DAY_MS * 90,
"180 days": now + ONE_DAY_MS * 180,
"1 year": now + ONE_DAY_MS * 365,
never: 0,
Never: 0,
};
const create = () => {
const data = {
@@ -86,7 +86,6 @@ export default function NewApiKey({ userId }: { userId?: string }) {
{created && "Api Key Created"}
</Text>
}
size="lg"
>
<Stack>
{!created && (
@@ -134,12 +133,12 @@ export default function NewApiKey({ userId }: { userId?: string }) {
<Group justify="space-between" wrap="nowrap">
<Text>Key</Text>
<CopyText content={created.key} label="api key" />
<CopyText content={created.key} label="api key" w={{ base: 200, lg: 250 }} />
</Group>
<Group justify="space-between" wrap="nowrap">
<Text>Secret</Text>
<CopyText content={created.secret} label="api secret" />
<CopyText content={created.secret} label="api secret" w={{ base: 200, lg: 250 }} />
</Group>
<Group justify="end" onClick={close}>

View File

@@ -64,8 +64,6 @@ export default function ApiKeysTable({
header: "Delete",
cell: ({ row }) => (
<ConfirmButton
variant="filled"
color="red"
icon={<ICONS.Delete size="1rem" />}
onClick={() => onDelete(row.original.key)}
loading={deletePending}

View File

@@ -3,13 +3,13 @@ import { useExecute, useSelectedResources, useWrite } from "@/lib/hooks";
import { sendCopyNotification, usableResourceExecuteKey } from "@/lib/utils";
import { UsableResource } from "@/resources";
import { ICONS } from "@/theme/icons";
import ConfirmButton from "@/ui/confirm-button";
import {
Box,
Button,
Divider,
Group,
List,
Loader,
Menu,
Modal,
Stack,
@@ -19,13 +19,13 @@ import {
} from "@mantine/core";
import { Types } from "komodo_client";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
import { FC, useState } from "react";
type Request = Types.ExecuteRequest["type"] | Types.WriteRequest["type"];
export interface BatchExecutionsProps<T extends Request> {
type: UsableResource;
executions: T[];
executions: [T, FC<{ size?: string | number }>][];
}
export default function BatchExecutions<T extends Request>({
@@ -46,6 +46,7 @@ export default function BatchExecutions<T extends Request>({
<BatchExecutionsModal
type={type}
execution={execution}
icon={executions.find((e) => e[0] === execution)?.[1] ?? ICONS.Check}
onClose={() => setExecution(undefined)}
/>
</>
@@ -59,7 +60,7 @@ function BatchExecutionsDropdownMenu<T extends Request>({
disabled,
}: {
type: UsableResource;
executions: T[];
executions: [T, FC<{ size?: string | number }>][];
onSelect: (item: T) => void;
disabled: boolean;
}) {
@@ -90,9 +91,10 @@ function BatchExecutionsDropdownMenu<T extends Request>({
</Menu.Item>
)}
{executions.map((execution) => (
{executions.map(([execution, Icon]) => (
<Menu.Item
key={execution}
leftSection={<Icon size="1rem" />}
onClick={() => onSelect(execution)}
renderRoot={(props) => <Button fullWidth {...props} />}
>
@@ -127,23 +129,29 @@ function BatchExecutionsDropdownMenu<T extends Request>({
function BatchExecutionsModal({
type,
execution,
onClose,
icon: Icon,
onClose: _onClose,
}: {
type: UsableResource;
execution: Request | undefined;
icon: FC<{ size?: string | number }>;
onClose: () => void;
}) {
const [selected, setSelected] = useSelectedResources(type);
const [input, setInput] = useState("");
const onClose = () => {
setInput("");
_onClose();
};
const { mutate: execute, isPending: executePending } = useExecute(
execution! as Types.ExecuteRequest["type"],
execution as Types.ExecuteRequest["type"],
{
onSuccess: onClose,
},
);
const { mutate: write, isPending: writePending } = useWrite(
execution! as Types.WriteRequest["type"],
execution as Types.WriteRequest["type"],
{
onSuccess: onClose,
},
@@ -200,8 +208,10 @@ function BatchExecutionsModal({
)}
<Group justify="end">
<ConfirmButton
icon={<ICONS.Check size="1rem" />}
<Button
leftSection={
isPending ? <Loader size="1rem" /> : <Icon size="1rem" />
}
onClick={() => {
for (const resource of selected) {
if (execution.startsWith("Delete")) {
@@ -226,10 +236,9 @@ function BatchExecutionsModal({
? false
: input !== formatted
}
loading={isPending}
>
Confirm
</ConfirmButton>
{formatted}
</Button>
</Group>
</Stack>
</Modal>

View File

@@ -1,5 +1,5 @@
import { useContainerPortsMap } from "@/lib/hooks";
import { Group, HoverCard, Stack, Text } from "@mantine/core";
import { Group, GroupProps, HoverCard, Stack, Text } from "@mantine/core";
import { Types } from "komodo_client";
import { EthernetPort } from "lucide-react";
import { colorByIntention } from "@/lib/color";
@@ -8,7 +8,7 @@ import { fmtPortMount } from "@/lib/formatting";
import { useServerAddress } from "@/resources/server/hooks";
import DividedChildren from "@/ui/divided-children";
export interface ContainerPortsProps {
export interface ContainerPortsProps extends GroupProps {
ports: Types.Port[];
serverId: string | undefined;
}
@@ -16,6 +16,7 @@ export interface ContainerPortsProps {
export default function ContainerPorts({
ports,
serverId,
...groupProps
}: ContainerPortsProps) {
const portsMap = useContainerPortsMap(ports);
const sortedNumericPorts = Object.keys(portsMap)
@@ -42,7 +43,7 @@ export default function ContainerPorts({
}
return (
<DividedChildren>
<DividedChildren {...groupProps}>
{groupedPorts.map((group) => (
<ContainerPort
key={group.start}

View File

@@ -82,7 +82,7 @@ export default function ContainerSelector({
onClick={() => combobox.toggleDropdown()}
{...targetProps}
>
<Group gap="xs">
<Group gap="xs" wrap="nowrap">
<DOCKER_LINK_ICONS.Container serverId={serverId} name={selected} />
<Text>{name || (placeholder ?? "Select container")}</Text>
</Group>

View File

@@ -48,7 +48,6 @@ export default function ContainersSection({
<Section
titleOther={titleOther}
title={!titleOther ? "Containers" : undefined}
icon={!titleOther ? <ICONS.Container size="1.3rem" /> : undefined}
actions={
(pruneButton && !allRunning) || _search || setShow ? (
@@ -124,7 +123,7 @@ export default function ContainersSection({
),
cell: ({ row }) =>
(row.original.networks?.length ?? 0) > 0 ? (
<DividedChildren>
<DividedChildren wrap="nowrap">
{row.original.networks?.map((network) => (
<DockerResourceLink
key={network}
@@ -171,6 +170,7 @@ export default function ContainersSection({
<ContainerPorts
ports={row.original.ports ?? []}
serverId={row.original.server_id}
wrap="nowrap"
/>
),
},
@@ -181,7 +181,7 @@ export default function ContainersSection({
<SortableHeader column={column} title="Volumes" />
),
cell: ({ row }) => (
<DividedChildren>
<DividedChildren wrap="nowrap">
{row.original.volumes?.map((volume) => (
<DockerResourceLink
key={volume}

View File

@@ -8,11 +8,13 @@ export interface RepoLinkProps {
export default function RepoLink({ repo, link }: RepoLinkProps) {
return (
<a href={link} target="_blank">
<Group gap="sm">
<FolderGit size="1rem" />
<Text>{repo}</Text>
</Group>
</a>
<Group
renderRoot={(props) => <a href={link} target="_blank" {...props} />}
gap="xs"
wrap="nowrap"
>
<FolderGit size="1rem" />
<Text>{repo}</Text>
</Group>
);
}

View File

@@ -1,6 +1,7 @@
import { useRead, useSearchCombobox } from "@/lib/hooks";
import { filterBySplit } from "@/lib/utils";
import {
ActionIcon,
Button,
ButtonProps,
Combobox,
@@ -13,6 +14,11 @@ import { ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { DOCKER_LINK_ICONS } from "./docker/link";
import { ICONS } from "@/theme/icons";
import {
colorByIntention,
containerStateIntention,
swarmStateIntention,
} from "@/lib/color";
export interface StackServiceSelectorProps extends ComboboxProps {
stackId: string;
@@ -22,6 +28,7 @@ export interface StackServiceSelectorProps extends ComboboxProps {
placeholder?: string;
state?: Types.ContainerStateStatusEnum;
targetProps?: ButtonProps;
clearable?: boolean;
}
export default function StackServiceSelector({
@@ -34,22 +41,33 @@ export default function StackServiceSelector({
position = "bottom-start",
onOptionSubmit,
targetProps,
clearable = true,
...comboboxProps
}: StackServiceSelectorProps) {
const services = useRead("ListStackServices", {
stack: stackId,
}).data?.filter((service) => !state || service?.container?.state === state);
const firstService = services?.[0].service;
useEffect(() => {
firstService && onSelect?.(firstService);
}, [firstService]);
const name = services?.find((s) => s.service === selected)?.service;
const container = services?.find((s) => s.service === selected)?.container;
const selectedService = services?.find((s) => s.service === selected);
const name = selectedService?.service;
const container = selectedService?.container;
const swarmService = selectedService?.swarm_service;
const intention = !selectedService
? "None"
: swarmService?.State
? swarmStateIntention(swarmService.State)
: containerStateIntention(
container?.state ?? Types.ContainerStateStatusEnum.Empty,
);
const { search, setSearch, combobox } = useSearchCombobox();
if (!services) return null;
const filtered = filterBySplit(services, search, (item) => item.service).sort(
(a, b) => {
if (a.service > b.service) {
@@ -76,21 +94,38 @@ export default function StackServiceSelector({
>
<Combobox.Target>
<Button
maw={350}
justify="space-between"
disabled={disabled}
rightSection={<ChevronsUpDown size="0.9rem" />}
w="fit-content"
maw="100%"
rightSection={
<Group gap="xs" ml="sm" wrap="nowrap">
{clearable && (
<ActionIcon
size="sm"
variant="filled"
color="red"
onClick={(e) => {
e.stopPropagation();
onSelect?.("");
}}
disabled={disabled || !selected}
>
<ICONS.Clear size="0.8rem" />
</ActionIcon>
)}
<ChevronsUpDown size="1rem" />
</Group>
}
onClick={() => combobox.toggleDropdown()}
disabled={!stackId || disabled}
loading={!!stackId && !services}
{...targetProps}
>
<Group gap="xs">
{container && (
<DOCKER_LINK_ICONS.Container
serverId={container.server_id!}
name={container.name}
/>
)}
<Text>{name || (placeholder ?? "Select container")}</Text>
<Group gap="xs" wrap="nowrap">
<ICONS.Service size="1rem" color={colorByIntention(intention)} />
<Text className="text-ellipsis">
{name || (placeholder ?? "Select service")}
</Text>
</Group>
</Button>
</Combobox.Target>

View File

@@ -55,7 +55,7 @@ export default function TagSelector({
pl="0.4rem"
className="bordered-heavy"
justify="start"
w={{ base: "100%", xs: "fit-content" }}
w={{ base: "100%", xs: "138" }}
fw="normal"
leftSection={
<Badge

View File

@@ -22,12 +22,16 @@ export default function Tag({
onClick={onClick}
style={{ cursor: onClick ? "pointer" : undefined }}
rightSection={icon}
className="text-ellipsis"
w="fit-content"
h="fit-content"
bdrs="sm"
py={py}
tt="none"
fz={{ base: "xs", lg: "sm" }}
styles={{
label: { width: "fit-content", height: "fit-content" },
}}
{...badgeProps}
>
{tag?.name ?? <Loader size="0.6rem" />}

View File

@@ -163,7 +163,7 @@ export default function Terminal({
display: selected ? undefined : "none",
}}
>
<Box ref={termRef} h="calc(100vh - 30rem)" />
<Box ref={termRef} h="max(200px, calc(100vh - 320px))" />
</Box>
);
}

View File

@@ -127,7 +127,9 @@ export function useAuthState() {
const search = new URLSearchParams(location.search);
const _passkey = search.get("passkey");
const passkey = _passkey ? JSON.parse(_passkey) : null;
const passkey = _passkey
? JSON.parse(MoghAuth.Passkey.base64UrlDecode(_passkey))
: null;
// guard against multiple reqs sent
// maybe isPending would do this but not sure about with render loop, this for sure will.

View File

@@ -187,6 +187,10 @@ function onUpdate(
invalidate(["ListProcedures"]);
} else if (update.operation === Types.Operation.RunAction) {
invalidate(["ListActions"]);
} else if (update.operation === Types.Operation.Deploy) {
invalidate(["ListDeployments"]);
} else if (update.operation === Types.Operation.DeployStack) {
invalidate(["ListStacks"]);
}
// Do invalidations of these only if update is completed

View File

@@ -343,3 +343,44 @@ export function listsEqual(a: string[], b: string[]) {
}
return true;
}
/**
* Does deep compare of 2 items, returning `true` if equal.
*
* - Functions: Always `true`
* - Primitives: Returns direct `a === b`
* - Arrays: Returns same items and ordering (recursive)
* - Objects: Returns same keys / values (recursive)
*
* @param a Item a
* @param b Item b
* @returns a === b
*/
export function deepCompare(a: any, b: any) {
const ta = typeof a;
const tb = typeof b;
if (ta !== tb) return false;
if (ta === "function") return true;
if (ta === "object") {
const ea = Object.entries(a);
const kb = Object.keys(b);
// Length not equal -> false
if (ea.length !== kb.length) return false;
for (const [key, va] of ea) {
const vb = b[key];
// Early return when any not equal
if (!deepCompare(va, vb)) return false;
}
// If it gets through all, it's equal
return true;
}
return a === b;
}

View File

@@ -2,6 +2,7 @@ import { useAlertDetails } from "@/components/alerts/details";
import AlertLevel from "@/components/alerts/level";
import ResourceTypeSelector from "@/components/resource-type-selector";
import { alertLevelIntention } from "@/lib/color";
import { fmtUpperCamelcase } from "@/lib/formatting";
import { useRead } from "@/lib/hooks";
import { UsableResource } from "@/resources";
import ResourceLink from "@/resources/link";
@@ -114,7 +115,10 @@ export default function Alerts() {
}
setParams(p);
}}
data={alertTypes}
data={alertTypes.map((value) => ({
value,
label: fmtUpperCamelcase(value),
}))}
searchable
clearable
/>

View File

@@ -53,9 +53,9 @@ function RecentRow({ type }: { type: UsableResource }) {
...(resources?.slice(0, 8 - (recents?.length || 0)) ?? []),
];
const Components = ResourceComponents[type];
const RC = ResourceComponents[type];
const data = Components.useDashboardSummaryData?.();
const data = RC.useDashboardSummaryData?.();
if (ids.length === 0) {
return;
@@ -67,11 +67,11 @@ function RecentRow({ type }: { type: UsableResource }) {
<>
<DashboardSummary
name={name}
icon={<Components.Icon />}
icon={<RC.Icon />}
data={data}
onClick={() => nav(`/${usableResourcePath(type)}`)}
>
{Components.DashboardSummary && <Components.DashboardSummary />}
{RC.DashboardSummary && <RC.DashboardSummary />}
</DashboardSummary>
<Stack w="100%" px="lg" py="md">
<Flex align="center" gap="xs" opacity={0.6}>
@@ -115,8 +115,8 @@ function RecentCard({
id: string;
visibleFrom?: MantineBreakpoint;
}) {
const Components = ResourceComponents[type];
const resource = Components.useListItem(id);
const RC = ResourceComponents[type];
const resource = RC.useListItem(id);
const { preferences } = useDashboardPreferences();
if (!resource) {
@@ -145,7 +145,7 @@ function RecentCard({
>
<Group justify="space-between">
<Group style={{ textWrap: "nowrap" }} gap="sm">
<Components.Icon id={id} />
<RC.Icon id={id} />
<ResourceName type={type} id={id} />
{resource.template && <TemplateMarker type={type} />}
</Group>

View File

@@ -89,7 +89,6 @@ export default function ContainerTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -134,11 +134,7 @@ function ImageInner({
)}
{/* TOP LEVEL IMAGE INFO */}
<Section
title="Details"
icon={<ICONS.Info size="1.3rem" />}
>
<Section title="Details" icon={<ICONS.Info size="1.3rem" />}>
<DataTable
tableKey="image-info"
data={[image]}

View File

@@ -1,5 +1,11 @@
import { komodo_client, useLoginOptions } from "@/lib/hooks";
import { Button, Group, Stack, Text } from "@mantine/core";
import {
Button,
Group,
Stack,
Text,
useComputedColorScheme,
} from "@mantine/core";
import { MoghAuth } from "komodo_client";
import { KeyRound } from "lucide-react";
@@ -9,8 +15,9 @@ export default function LoginHeader({
secondFactorPending: boolean;
}) {
const options = useLoginOptions().data;
const theme = useComputedColorScheme();
return (
<Group gap="4rem" justify="space-between">
<Group justify="space-between">
<Group gap="sm">
<img src="/mogh-512x512.png" width={42} height={42} alt="moghtech" />
<Stack gap="0">
@@ -44,7 +51,11 @@ export default function LoginHeader({
<img
src={`/icons/${provider.toLowerCase()}.svg`}
alt={provider}
style={{ width: "1rem", height: "1rem" }}
style={{
width: "1rem",
height: "1rem",
filter: theme === "dark" ? "invert(1)" : undefined,
}}
/>
)
}

View File

@@ -137,7 +137,7 @@ export default function Login({
: (localForm.onSubmit((form) => login(form)) as any)
}
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
miw={{ lg: "500px" }}
miw={{ base: "95vw", xs: "530px" }}
maw="95vw"
>
{options?.local && !secondFactorPending && (
@@ -147,12 +147,18 @@ export default function Login({
autoFocus
label="Username"
placeholder="Enter username"
autoComplete="username"
autoCapitalize="off"
autoCorrect="off"
key={localForm.key("username")}
/>
<PasswordInput
{...localForm.getInputProps("password")}
label="Password"
placeholder="Enter password"
autoComplete="password"
autoCapitalize="off"
autoCorrect="off"
key={localForm.key("password")}
/>
<Group mt="sm" justify="end">

View File

@@ -31,13 +31,13 @@ export default function Profile() {
const [password, setPassword] = useState("");
const { mutate: updateUsername } = useManageAuth("UpdateUsername", {
onSuccess: () => {
notifications.show({ message: "Username updated." });
notifications.show({ message: "Username updated.", color: "green" });
refetchUser();
},
});
const { mutate: updatePassword } = useManageAuth("UpdatePassword", {
onSuccess: () => {
notifications.show({ message: "Password updated." });
notifications.show({ message: "Password updated.", color: "green" });
setPassword("");
refetchUser();
},
@@ -48,6 +48,7 @@ export default function Profile() {
onSuccess: () => {
notifications.show({
message: "External login skip 2fa mode updated.",
color: "green",
});
refetchUser();
},
@@ -65,7 +66,6 @@ export default function Profile() {
<Section
title="Login"
titleFz="h3"
icon={<ICONS.Key size="1.2rem" />}
withBorder
>
@@ -115,7 +115,6 @@ export default function Profile() {
<Section
title="2FA"
titleFz="h3"
icon={<ICONS.Key size="1.2rem" />}
withBorder
>

View File

@@ -95,7 +95,6 @@ export const LinkedLogins = ({
<Section
title="Providers"
titleFz="h3"
icon={<ICONS.Provider size="1.2rem" />}
withBorder
>
@@ -151,7 +150,6 @@ export const LinkedLogins = ({
}) =>
data ? (
<ConfirmModal
targetProps={{ color: "red" }}
icon={<ICONS.Unlink size="1rem" />}
onConfirm={() => unlink({ provider })}
confirmText="Unlink"

View File

@@ -13,7 +13,10 @@ export const EnrollPasskey = ({ user }: { user: Types.User }) => {
{
onSuccess: () => {
userInvalidate();
notifications.show({ message: "Unenrolled in passkey 2FA" });
notifications.show({
message: "Unenrolled in passkey 2FA",
color: "green",
});
},
},
);
@@ -23,7 +26,10 @@ export const EnrollPasskey = ({ user }: { user: Types.User }) => {
{
onSuccess: () => {
userInvalidate();
notifications.show({ message: "Enrolled in passkey authentication" });
notifications.show({
message: "Enrolled in passkey authentication",
color: "green",
});
},
},
);
@@ -62,9 +68,7 @@ export const EnrollPasskey = ({ user }: { user: Types.User }) => {
icon={<Trash size="1rem" />}
loading={unenrollPending}
onConfirm={() => unenroll({})}
variant="filled"
color="red"
w={220}
targetProps={{ variant: "filled", color: "red", c: "bw", w: 220 }}
>
Unenroll Passkey 2FA
</ConfirmModal>

View File

@@ -39,7 +39,10 @@ export const EnrollTotp = ({ user }: { user: Types.User }) => {
{
onSuccess: () => {
userInvalidate();
notifications.show({ message: "Unenrolled in TOTP 2FA." });
notifications.show({
message: "Unenrolled in TOTP 2FA.",
color: "green",
});
},
},
);
@@ -132,8 +135,7 @@ export const EnrollTotp = ({ user }: { user: Types.User }) => {
icon={<Trash size="1rem" />}
loading={unenrollPending}
onConfirm={() => unenroll({})}
color="red"
w={220}
targetProps={{ variant: "filled", color: "red", c: "bw", w: 220 }}
>
Unenroll TOTP 2FA
</ConfirmModal>

View File

@@ -26,6 +26,8 @@ export default function Settings() {
<MobileFriendlyTabs
value={view}
onValueChange={setView as any}
actions={currentView === "Variables" && <ExportToml includeVariables />}
tabsProps={{ color: "green" }}
tabs={[
{
value: "Variables",
@@ -64,9 +66,6 @@ export default function Settings() {
icon: ICONS.OnboardingKey,
},
]}
actions={currentView === "Variables" && <ExportToml includeVariables />}
tabProps={{ w: 150 }}
tabsProps={{ color: "green" }}
/>
</Stack>
);

View File

@@ -5,7 +5,7 @@ import { useInvalidate, useRead, useSetTitle, useWrite } from "@/lib/hooks";
import ResourceSelector from "@/resources/selector";
import { ICONS } from "@/theme/icons";
import { DataTable, SortableHeader } from "@/ui/data-table";
import { Badge, Group, Switch, TextInput } from "@mantine/core";
import { Badge, Group, Switch, TextInput, useMatches } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { ColumnDef } from "@tanstack/react-table";
import { Types } from "komodo_client";
@@ -25,6 +25,10 @@ export default function SettingsOnboardingKeys() {
notifications.show({ message: "Updated onboarding key", color: "green" });
},
});
const expiresSize = useMatches({
base: "sm",
xl: "md",
});
const columns: (
| ColumnDef<Types.OnboardingKey, unknown>
| false
@@ -46,6 +50,7 @@ export default function SettingsOnboardingKeys() {
})
}
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.blur()}
miw={200}
/>
),
},
@@ -74,7 +79,7 @@ export default function SettingsOnboardingKeys() {
(tag) => !row.original.tags?.includes(tag._id?.$oid!),
);
return (
<Group>
<Group wrap="nowrap" gap="sm">
<TagSelector
title="Select Tags"
tags={otherTags}
@@ -180,9 +185,10 @@ export default function SettingsOnboardingKeys() {
}) => (
<Badge
color={expires && expires <= Date.now() ? "red" : "accent"}
fz={{ base: "sm", lg: "md" }}
p={{ base: "sm", lg: "md" }}
size="md"
fz={{ base: "sm", xl: "md" }}
p={{ base: "sm", xl: "md" }}
styles={{ label: { width: "fit-content", height: "fit-content" } }}
size={expiresSize}
>
{expires ? fmtDateWithMinutes(new Date(expires)) : "Never"}
</Badge>

View File

@@ -10,12 +10,13 @@ import {
Stack,
Text,
TextInput,
useMatches,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { useState } from "react";
type ExpiresOptions = "1 day" | "7 days" | "30 days" | "never";
type ExpiresOptions = "1 day" | "7 days" | "30 days" | "Never";
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
export default function NewOnboardingKey() {
@@ -38,7 +39,7 @@ export default function NewOnboardingKey() {
"1 day": now + ONE_DAY_MS,
"7 days": now + ONE_DAY_MS * 7,
"30 days": now + ONE_DAY_MS * 90,
never: 0,
Never: 0,
};
const create = () =>
mutate({
@@ -55,6 +56,8 @@ export default function NewOnboardingKey() {
_close();
};
const size = useMatches({ base: "90%", md: 500 });
return (
<>
<Modal
@@ -66,7 +69,7 @@ export default function NewOnboardingKey() {
{created && "Onboarding Key Created"}
</Text>
}
size="lg"
size={size}
>
<Stack>
{!created && (
@@ -117,11 +120,15 @@ export default function NewOnboardingKey() {
{created && (
<>
<Text>
Use as the <b>PERIPHERY_ONBOARDING_KEY</b>
<Text size="md" my="sm">
Copy the onboarding key below. <b>It won't be shown again</b>.
</Text>
<CopyText content={created.private_key} label="private key" />
<CopyText
content={created.private_key}
label="private key"
w="90%"
/>
<Group justify="end" onClick={close}>
<Button leftSection={<ICONS.Clear />}>Close</Button>

View File

@@ -3,7 +3,13 @@ import { ICONS } from "@/theme/icons";
import ConfirmButton from "@/ui/confirm-button";
import { notifications } from "@mantine/notifications";
export default function DeleteVariable({ name, disabled }: { name: string; disabled: boolean }) {
export default function DeleteVariable({
name,
disabled,
}: {
name: string;
disabled: boolean;
}) {
const invalidate = useInvalidate();
const { mutate, isPending } = useWrite("DeleteVariable", {
onSuccess: () => {

View File

@@ -93,7 +93,6 @@ export default function StackServiceTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -77,8 +77,8 @@ export default function SwarmConfigEditSection({
Reset
</Button>
<ConfirmUpdate
previous={{ contents: data }}
content={{ contents: edit }}
original={{ contents: data }}
update={{ contents: edit }}
onConfirm={async () =>
name &&
edit !== undefined &&

View File

@@ -67,7 +67,6 @@ export default function SwarmConfigTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -59,7 +59,6 @@ export default function SwarmNodeTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -76,8 +76,8 @@ export default function SwarmSecretEditSection({
Reset
</Button>
<ConfirmUpdate
previous={{ contents: "" }}
content={{ contents: edit }}
original={{ contents: "" }}
update={{ contents: edit }}
onConfirm={async () =>
name &&
edit !== undefined &&

View File

@@ -67,7 +67,6 @@ export default function SwarmSecretTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -66,7 +66,6 @@ export default function SwarmServiceTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -69,7 +69,6 @@ export default function SwarmStackTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -54,7 +54,6 @@ export default function SwarmTaskTabs({
tabs={tabs}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -1,18 +1,17 @@
import DockerResourceLink from "@/components/docker/link";
import TargetTerminal from "@/components/terminal/target";
import { useSetTitle, useWrite } from "@/lib/hooks";
import { useSetTitle } from "@/lib/hooks";
import { ICONS } from "@/theme/icons";
import { useDeployment } from "@/resources/deployment";
import { useServer } from "@/resources/server";
import { useStack } from "@/resources/stack";
import ConfirmButton from "@/ui/confirm-button";
import Page from "@/ui/page";
import { Group, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { Types } from "komodo_client";
import { ReactNode, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import ResourceLink from "@/resources/link";
import DeleteTerminal from "./terminals/delete";
type WithTerminal = "servers" | "deployments" | "stacks" | string;
@@ -211,7 +210,12 @@ function TerminalLayout({
customDescription={
<>
<Text>Terminal</Text>|{Link}|
<DeleteTerminal terminal={terminal} target={target} />
<DeleteTerminal
terminal={terminal}
target={target}
size="xs"
navTo="/terminals"
/>
</>
}
>
@@ -219,37 +223,3 @@ function TerminalLayout({
</Page>
);
}
function DeleteTerminal({
terminal,
target,
}: {
terminal: string;
target: Types.TerminalTarget;
}) {
const nav = useNavigate();
const { mutate, isPending } = useWrite("DeleteTerminal", {
onSuccess: () => {
notifications.show({ message: `Deleted Terminal '${terminal}'` });
nav("/terminals");
},
});
return (
<ConfirmButton
variant="filled"
color="red"
icon={<ICONS.Delete size="1rem" />}
onClick={() =>
mutate({
target,
terminal,
})
}
loading={isPending}
w={120}
size="xs"
>
Delete
</ConfirmButton>
);
}

View File

@@ -1,23 +1,33 @@
import { useTerminalTargetPermissions, useWrite } from "@/lib/hooks";
import { ICONS } from "@/theme/icons";
import ConfirmButton from "@/ui/confirm-button";
import { ButtonProps } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { Types } from "komodo_client";
import { useNavigate } from "react-router-dom";
export default function DeleteTerminal({
target,
terminal,
refetch,
navTo,
...buttonProps
}: {
target: Types.TerminalTarget;
terminal: string;
refetch: () => void;
}) {
refetch?: () => void;
navTo?: string;
} & ButtonProps) {
const nav = useNavigate();
const { canWrite } = useTerminalTargetPermissions(target);
const { mutate, isPending } = useWrite("DeleteTerminal", {
onSuccess: () => {
refetch();
notifications.show({ message: `Deleted Terminal '${terminal}'` });
refetch?.();
notifications.show({
message: `Deleted Terminal '${terminal}'`,
color: "green",
});
navTo && nav(navTo);
},
});
return (
@@ -29,6 +39,7 @@ export default function DeleteTerminal({
w={120}
disabled={!canWrite}
loading={isPending}
{...buttonProps}
>
Delete
</ConfirmButton>

View File

@@ -1,7 +1,11 @@
import ResourceTypeSelector from "@/components/resource-type-selector";
import { useUpdateDetails } from "@/components/updates/details";
import UserAvatar from "@/components/user-avatar";
import { fmtDateWithMinutes, fmtOperation } from "@/lib/formatting";
import {
fmtDateWithMinutes,
fmtOperation,
fmtUpperCamelcase,
} from "@/lib/formatting";
import { useRead, useSetTitle } from "@/lib/hooks";
import { UsableResource } from "@/resources";
import ResourceLink from "@/resources/link";
@@ -94,13 +98,12 @@ export default function Updates() {
}
setParams(p);
}}
data={
type
? OPERATIONS_BY_RESOURCE[type]
: Object.values(Types.Operation).filter(
(o) => o !== Types.Operation.None,
)
}
data={(type
? OPERATIONS_BY_RESOURCE[type]
: Object.values(Types.Operation).filter(
(o) => o !== Types.Operation.None,
)
).map((value) => ({ value, label: fmtUpperCamelcase(value) }))}
searchable
clearable
/>
@@ -209,7 +212,9 @@ export default function Updates() {
{
header: "Operator",
accessorKey: "operator",
cell: ({ row }) => <UserAvatar userId={row.original.operator} fz="md" />,
cell: ({ row }) => (
<UserAvatar userId={row.original.operator} fz="md" />
),
},
]}
onRowClick={(row) => openDetails(row.id)}

View File

@@ -61,7 +61,7 @@ export const ActionComponents: RequiredResourceComponents<
New: () => <NewResource type="Action" />,
BatchExecutions: () => (
<BatchExecutions type="Action" executions={["RunAction"]} />
<BatchExecutions type="Action" executions={[["RunAction", ICONS.Run]]} />
),
Table: ActionTable,
@@ -158,7 +158,7 @@ export const ActionComponents: RequiredResourceComponents<
return (
<ConfirmModalWithDisable
icon={<ICONS.Action size="1rem" />}
icon={<ICONS.Run size="1rem" />}
confirmText={action.name}
onConfirm={async () => {
await mutateAsync({ action: id, args: {} });

View File

@@ -1,4 +1,4 @@
import { useRead } from "@/lib/hooks";
import { useExecute, useRead } from "@/lib/hooks";
import { ICONS } from "@/theme/icons";
import { RequiredResourceComponents } from "..";
import { Types } from "komodo_client";
@@ -8,6 +8,7 @@ import ResourceHeader from "../header";
import AlerterConfig from "./config";
import { hexColorByIntention } from "@/lib/color";
import BatchExecutions from "@/components/batch-executions";
import ConfirmButton from "@/ui/confirm-button";
export function useAlerter(id: string | undefined) {
return useRead("ListAlerters", {}).data?.find((r) => r.id === id);
@@ -38,7 +39,10 @@ export const AlerterComponents: RequiredResourceComponents<
New: () => <NewResource type="Alerter" />,
BatchExecutions: () => (
<BatchExecutions type="Alerter" executions={["TestAlerter"]} />
<BatchExecutions
type="Alerter"
executions={[["TestAlerter", ICONS.Test]]}
/>
),
Table: AlerterTable,
@@ -72,7 +76,23 @@ export const AlerterComponents: RequiredResourceComponents<
State: () => null,
Info: {},
Executions: {},
Executions: {
TestAlerter: ({ id }) => {
const { mutate, isPending } = useExecute("TestAlerter");
const alerter = useAlerter(id);
if (!alerter) return null;
return (
<ConfirmButton
icon={<ICONS.Test size="1rem" />}
loading={isPending}
onClick={() => mutate({ alerter: id })}
disabled={isPending}
>
Test Alerter
</ConfirmButton>
);
},
},
Config: AlerterConfig,

View File

@@ -87,7 +87,13 @@ export const BuildComponents: RequiredResourceComponents<
},
BatchExecutions: () => (
<BatchExecutions type="Build" executions={["RunBuild"]} />
<BatchExecutions
type="Build"
executions={[
["RunBuild", ICONS.Build],
["CancelBuild", ICONS.Cancel],
]}
/>
),
Table: BuildTable,

View File

@@ -67,11 +67,7 @@ export default function BuildInfo({
<Section gap="xl" titleOther={titleOther}>
{/* Errors */}
{remoteError && remoteError.length > 0 && (
<Stack
className="bordered-light"
bdrs="md"
p="xl"
>
<Stack className="bordered-light" bdrs="md" p="xl">
{/* HEADER */}
<Group justify="between">
<Group ff="monospace">
@@ -112,11 +108,7 @@ export default function BuildInfo({
{/* Update latest contents */}
{remoteContents && remoteContents.length > 0 && (
<Stack
className="bordered-light"
bdrs="md"
p="xl"
>
<Stack className="bordered-light" bdrs="md" p="xl">
{/* HEADER */}
<Group
justify="space-between"
@@ -160,8 +152,8 @@ export default function BuildInfo({
Reset
</Button>
<ConfirmUpdate
previous={{ contents: remoteContents }}
content={{ contents: edits.contents }}
original={{ contents: remoteContents }}
update={{ contents: edits.contents }}
onConfirm={async () => {
if (build) {
return await mutateAsync({
@@ -194,7 +186,9 @@ export default function BuildInfo({
)}
{lastBuild && lastBuild.logs.length > 0 && (
<Code fz="lg" fw="bold" w="fit-content">Last Build Logs</Code>
<Code fz="lg" fw="bold" w="fit-content">
Last Build Logs
</Code>
)}
{lastBuild &&
lastBuild.logs?.map((log, i) => (
@@ -216,11 +210,7 @@ export default function BuildInfo({
withBorder
>
{log.command && (
<Stack
className="bordered-light"
bdrs="md"
p="md"
>
<Stack className="bordered-light" bdrs="md" p="md">
<Text fw="bold">command</Text>
<Code
fz="sm"
@@ -234,11 +224,7 @@ export default function BuildInfo({
</Stack>
)}
{log.stdout && (
<Stack
className="bordered-light"
bdrs="md"
p="md"
>
<Stack className="bordered-light" bdrs="md" p="md">
<Text fw="bold">stdout</Text>
<Code
component="pre"
@@ -252,11 +238,7 @@ export default function BuildInfo({
</Stack>
)}
{log.stderr && (
<Stack
className="bordered-light"
bdrs="md"
p="md"
>
<Stack className="bordered-light" bdrs="md" p="md">
<Text fw="bold">stderr</Text>
<Code
component="pre"

View File

@@ -68,7 +68,7 @@ export default function DeploymentNetworkSelector({
}}
disabled={disabled}
data={networks}
w="fit-content"
w={{ base: "100%", xs: "fit-content" }}
searchable
/>
)}

View File

@@ -1,3 +1,4 @@
import { fmtUpperCamelcase } from "@/lib/formatting";
import { ConfigItem } from "@/ui/config/item";
import { Select } from "@mantine/core";
import { Types } from "komodo_client";
@@ -27,13 +28,11 @@ export default function DeploymentRestartSelector({
disabled={disabled}
placeholder="Select Mode"
data={Object.entries(Types.RestartMode).map(([label, value]) => ({
label:
label === "NoRestart"
? "don't restart"
: value.split("-").join(" "),
label: fmtUpperCamelcase(label),
value,
}))}
w="fit-content"
tt="capitalize"
w={{ base: "100%", xs: "fit-content" }}
/>
</ConfigItem>
);

View File

@@ -23,6 +23,7 @@ export function TerminationSignal({
disabled={disabled}
placeholder="Select signal"
data={Object.values(Types.TerminationSignal).reverse()}
w={{ base: "100%", xs: "fit-content" }}
/>
</ConfigItem>
);

View File

@@ -90,12 +90,12 @@ export const DeploymentComponents: RequiredResourceComponents<
<BatchExecutions
type="Deployment"
executions={[
"CheckDeploymentForUpdate",
"PullDeployment",
"Deploy",
"RestartDeployment",
"StopDeployment",
"DestroyDeployment",
["CheckDeploymentForUpdate", ICONS.UpdateAvailable],
["PullDeployment", ICONS.Pull],
["Deploy", ICONS.Deploy],
["RestartDeployment", ICONS.Restart],
["StopDeployment", ICONS.Stop],
["DestroyDeployment", ICONS.Destroy],
]}
/>
),

View File

@@ -122,7 +122,7 @@ const Image = ({
} else {
const [img] = image.split(":");
return (
<Group>
<Group wrap="nowrap">
<ICONS.Image size="1rem" />
{img}
</Group>

View File

@@ -1,6 +1,6 @@
import EntityHeader, { EntityHeaderProps } from "@/ui/entity-header";
import { UsableResource } from ".";
import { useWrite } from "@/lib/hooks";
import { useInvalidate, useWrite } from "@/lib/hooks";
import { notifications } from "@mantine/notifications";
import ResourceHeaderAction from "./header-action";
import { Types } from "komodo_client";
@@ -17,10 +17,12 @@ export default function ResourceHeader({
resource,
...props
}: ResourceHeaderProps) {
const inv = useInvalidate();
const { mutateAsync: rename, isPending: renamePending } = useWrite(
`Rename${type}`,
{
onSuccess: () => {
inv([`List${type}s`], [`Get${type}`]);
notifications.show({ message: "Renamed " + type, color: "green" });
},
},

View File

@@ -101,7 +101,11 @@ export interface RequiredResourceComponents<
BatchExecutions: React.FC;
/** Icon for the resource */
Icon: React.FC<{ id?: string; size?: string | number; noColor?: boolean }>;
Icon: React.FC<{
id?: string;
size?: string | number;
noColor?: boolean;
}>;
State: IdComponent;

View File

@@ -14,6 +14,7 @@ import {
Text,
TextInput,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { Types } from "komodo_client";
import { CheckCircle } from "lucide-react";
@@ -115,6 +116,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
setParams({ action: params.action, args: JSON.parse(args) })
}
disabled={disabled}
useMonaco
monacoLanguage="json"
/>
</Group>
@@ -131,6 +133,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -158,6 +161,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -198,6 +202,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -290,6 +295,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -317,6 +323,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -343,6 +350,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -369,6 +377,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -450,6 +459,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -470,7 +480,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
pull: undefined,
},
Component: ({ params, setParams, disabled }) => {
const [open, setOpen] = useState(false);
const [opened, { open, close }] = useDisclosure();
// local mirrors to allow cancel without committing
const [stack, setStack] = useState(params.stack ?? "");
const [service, setService] = useState(params.service ?? "");
@@ -551,128 +561,167 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
detach: detach ? true : undefined,
env,
} as any);
setOpen(false);
close();
};
return (
<>
<Button disabled={disabled}>Configure</Button>
<Button disabled={disabled} onClick={open}>
Configure
</Button>
<Modal
opened={open}
onClose={() => setOpen(false)}
opened={opened}
onClose={close}
title="Run Stack Service"
size="lg"
>
<Stack>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Group>
<Text c="dimmed">Stack</Text>
<Stack gap="lg">
<SimpleGrid cols={{ base: 1, sm: 2 }}>
<Stack gap="0">
<Text>Stack</Text>
<ResourceSelector
type="Stack"
selected={stack}
onSelect={(id) => setStack(id)}
disabled={disabled}
width="target"
targetProps={{ w: "100%" }}
/>
</Group>
<Group>
<Text c="dimmed">Service</Text>
</Stack>
<Stack gap="0">
<Text>Service</Text>
<StackServiceSelector
stackId={stack}
selected={service}
onSelect={setService}
disabled={disabled}
width="target"
targetProps={{ w: "100%" }}
/>
</Group>
</Stack>
</SimpleGrid>
<Group>
<Text c="dimmed">Command</Text>
<Stack gap="0">
<Text>Command</Text>
<TextInput
placeholder="Enter command (Required)"
value={commandText}
onChange={(e) => setCommand(e.target.value)}
disabled={disabled}
/>
</Group>
</Stack>
<SimpleGrid cols={{ base: 2, md: 4 }}>
<EnableSwitch
label="No TTY"
checked={no_tty}
onCheckedChange={setNoTty}
disabled={disabled}
/>
<EnableSwitch
label="No Dependencies"
checked={no_deps}
onCheckedChange={setNoDeps}
disabled={disabled}
/>
<EnableSwitch
label="Detach"
checked={detach}
onCheckedChange={setDetach}
disabled={disabled}
/>
<EnableSwitch
label="Service Ports"
checked={service_ports}
onCheckedChange={setServicePorts}
disabled={disabled}
/>
<EnableSwitch
label="Pull Image"
checked={pull}
onCheckedChange={setPull}
disabled={disabled}
/>
<SimpleGrid cols={{ base: 1, sm: 2 }}>
<Stack gap="0">
<Text>Working Directory</Text>
<TextInput
placeholder="/work/dir (Optional)"
value={workdir}
onChange={(e) => setWorkdir(e.target.value)}
disabled={disabled}
/>
</Stack>
<Stack gap="0">
<Text>User</Text>
<TextInput
placeholder="uid:gid or user (Optional)"
value={user}
onChange={(e) => setUser(e.target.value)}
disabled={disabled}
/>
</Stack>
</SimpleGrid>
</Stack>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Group>
<Text c="dimmed">Working Directory</Text>
<TextInput
placeholder="/work/dir"
value={workdir}
onChange={(e) => setWorkdir(e.target.value)}
disabled={disabled}
/>
</Group>
<Group>
<Text c="dimmed">User</Text>
<TextInput
placeholder="uid:gid or user"
value={user}
onChange={(e) => setUser(e.target.value)}
disabled={disabled}
/>
</Group>
<Group>
<Text c="dimmed">Entrypoint</Text>
<Stack gap="0">
<Text>Entrypoint</Text>
<TextInput
placeholder="Custom entrypoint (Optional)"
value={entrypoint}
onChange={(e) => setEntrypoint(e.target.value)}
disabled={disabled}
/>
</Group>
</SimpleGrid>
</Stack>
<Group>
<Text c="dimmed">Extra Environment Variables</Text>
<MonacoEditor
value={envText}
onValueChange={setEnvText}
language="key_value"
readOnly={disabled}
/>
</Group>
<Stack gap="0">
<Text>Extra Env</Text>
<MonacoEditor
value={envText}
onValueChange={setEnvText}
language="key_value"
readOnly={disabled}
/>
</Stack>
{!disabled && (
<Button onClick={onConfirm} leftSection={<CheckCircle />}>
Confirm
</Button>
)}
<Stack gap="0">
<Text>Options</Text>
<SimpleGrid
cols={{ base: 1, sm: 2 }}
className="accent-hover-light"
p="md"
bdrs="md"
style={{ placeItems: "center" }}
>
<EnableSwitch
label="No TTY"
checked={no_tty}
onCheckedChange={setNoTty}
disabled={disabled}
labelProps={{
w: 210,
justify: "end",
}}
/>
<EnableSwitch
label="No Dependencies"
checked={no_deps}
onCheckedChange={setNoDeps}
disabled={disabled}
labelProps={{
w: 210,
justify: "end",
}}
/>
<EnableSwitch
label="Detach"
checked={detach}
onCheckedChange={setDetach}
disabled={disabled}
labelProps={{
w: 210,
justify: "end",
}}
/>
<EnableSwitch
label="Service Ports"
checked={service_ports}
onCheckedChange={setServicePorts}
disabled={disabled}
labelProps={{
w: 210,
justify: "end",
}}
/>
<EnableSwitch
label="Pull Image"
checked={pull}
onCheckedChange={setPull}
disabled={disabled}
labelProps={{
w: 210,
justify: "end",
}}
/>
</SimpleGrid>
</Stack>
{!disabled && (
<Button onClick={onConfirm} leftSection={<CheckCircle />}>
Confirm
</Button>
)}
</Stack>
</Modal>
</>
);
@@ -701,6 +750,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -727,6 +777,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -753,6 +804,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
useMonaco
monacoLanguage="string_list"
/>
),
@@ -1042,6 +1094,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
placeholder="Configure custom alert message"
onUpdate={(message) => setParams({ message })}
disabled={disabled}
useMonaco
monacoLanguage={undefined}
/>
),

View File

@@ -15,7 +15,7 @@ export function RunProcedure({ id }: { id: string }) {
return (
<ConfirmModalWithDisable
confirmText={procedure.name}
icon={<ICONS.Start size="1rem" />}
icon={<ICONS.Run size="1rem" />}
onConfirm={() => run({ procedure: id })}
disabled={running || isPending}
loading={running || isPending}

View File

@@ -10,6 +10,10 @@ import ProcedureConfig from "./config";
import { RunProcedure } from "./executions";
import ResourceHeader from "../header";
import BatchExecutions from "@/components/batch-executions";
import { Badge, Group, Popover, Text } from "@mantine/core";
import { Clock } from "lucide-react";
import { useDisclosure } from "@mantine/hooks";
import { updateLogToHtml } from "@/lib/utils";
export function useProcedure(id: string | undefined) {
return useRead("ListProcedures", {}).data?.find((r) => r.id === id);
@@ -57,7 +61,10 @@ export const ProcedureComponents: RequiredResourceComponents<
New: () => <NewResource type="Procedure" />,
BatchExecutions: () => (
<BatchExecutions type="Procedure" executions={["RunProcedure"]} />
<BatchExecutions
type="Procedure"
executions={[["RunProcedure", ICONS.Run]]}
/>
),
Table: ProcedureTable,
@@ -91,7 +98,50 @@ export const ProcedureComponents: RequiredResourceComponents<
let state = useProcedure(id)?.info.state;
return <StatusBadge text={state} intent={procedureStateIntention(state)} />;
},
Info: {},
Info: {
Schedule: ({ id }) => {
const nextScheduledRun = useProcedure(id)?.info.next_scheduled_run;
return (
<Group gap="xs">
<Clock size="1rem" />
Next Run:
<Text fw="bold">
{nextScheduledRun
? new Date(nextScheduledRun).toLocaleString()
: "Not Scheduled"}
</Text>
</Group>
);
},
ScheduleErrors: ({ id }) => {
const [opened, { close, open }] = useDisclosure(false);
const error = useProcedure(id)?.info.schedule_error;
if (!error) {
return null;
}
return (
<Popover position="bottom-start" opened={opened}>
<Popover.Target>
<Badge color="red" onMouseEnter={open} onMouseLeave={close}>
Schedule Error
</Badge>
</Popover.Target>
<Popover.Dropdown style={{ pointerEvents: "none" }}>
<Text
component="pre"
dangerouslySetInnerHTML={{
__html: updateLogToHtml(error),
}}
fz="xs"
/>
</Popover.Dropdown>
</Popover>
);
},
},
Executions: {
RunProcedure,

View File

@@ -303,11 +303,14 @@ export default function RepoConfig({
},
{
label: "Links",
description: "Add quick links in the resource header",
contentHidden: ((update.links ?? config.links)?.length ?? 0) === 0,
labelHidden: true,
fields: {
links: (values, set) => (
<ConfigList
label="Links"
boldLabel
addLabel="Add Link"
description="Add quick links in the resource header"
field="links"
values={values ?? []}
set={set}

View File

@@ -0,0 +1,124 @@
import { useExecute, usePermissions, useRead } from "@/lib/hooks";
import { useRepo } from ".";
import { useBuilder } from "../builder";
import { Types } from "komodo_client";
import ConfirmButton from "@/ui/confirm-button";
import { ICONS } from "@/theme/icons";
export function CloneRepo({ id }: { id: string }) {
const { mutate, isPending } = useExecute("CloneRepo");
const cloning = useRead(
"GetRepoActionState",
{ repo: id },
{ refetchInterval: 5000 },
).data?.cloning;
const info = useRepo(id)?.info;
if (!info?.server_id) return null;
const hash = info?.latest_hash;
const isCloned = (hash?.length || 0) > 0;
const pending = isPending || cloning;
return (
<ConfirmButton
icon={<ICONS.CloneRepo size="1rem" />}
onClick={() => mutate({ repo: id })}
disabled={pending}
loading={pending}
>
{isCloned ? "Reclone" : "Clone"}
</ConfirmButton>
);
}
export function PullRepo({ id }: { id: string }) {
const { mutate, isPending } = useExecute("PullRepo");
const pulling = useRead(
"GetRepoActionState",
{ repo: id },
{ refetchInterval: 5000 },
).data?.pulling;
const info = useRepo(id)?.info;
if (!info?.server_id) return null;
const hash = info?.latest_hash;
const isCloned = (hash?.length || 0) > 0;
if (!isCloned) return null;
const pending = isPending || pulling;
return (
<ConfirmButton
icon={<ICONS.PullRepo size="1rem" />}
onClick={() => mutate({ repo: id })}
disabled={pending}
loading={pending}
>
Pull
</ConfirmButton>
);
}
export function BuildRepo({ id }: { id: string }) {
const { canExecute } = usePermissions({ type: "Repo", id });
const building = useRead(
"GetRepoActionState",
{ repo: id },
{ refetchInterval: 5000 },
).data?.building;
const updates = useRead("ListUpdates", {
query: {
"target.type": "Repo",
"target.id": id,
},
}).data;
const { mutate: run_mutate, isPending: runPending } = useExecute("BuildRepo");
const { mutate: cancel_mutate, isPending: cancelPending } =
useExecute("CancelRepoBuild");
const repo = useRepo(id);
const builder = useBuilder(repo?.info.builder_id);
const canCancel = builder?.info.builder_type !== "Server";
// Don't show if builder not attached
if (!builder) return null;
// make sure hidden without perms.
// not usually necessary, but this button also used in deployment actions.
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.BuildRepo,
);
const latestCancel = updates?.updates.find(
(u) => u.operation === Types.Operation.CancelRepoBuild,
);
const cancelDisabled =
!canCancel ||
cancelPending ||
(latestCancel && latestBuild
? latestCancel!.start_ts > latestBuild!.start_ts
: false);
if (building) {
return (
<ConfirmButton
variant="filled"
color="red"
icon={<ICONS.Cancel size="1rem" />}
onClick={() => cancel_mutate({ repo: id })}
disabled={cancelDisabled}
loading={cancelPending}
>
Cancel Build
</ConfirmButton>
);
} else {
return (
<ConfirmButton
icon={<ICONS.Build size="1rem" />}
onClick={() => run_mutate({ repo: id })}
disabled={runPending || building}
loading={runPending || building}
>
Build
</ConfirmButton>
);
}
}

View File

@@ -9,6 +9,12 @@ import NewResource from "@/resources/new";
import ResourceHeader from "@/resources/header";
import RepoTabs from "./tabs";
import BatchExecutions from "@/components/batch-executions";
import { BuildRepo, CloneRepo, PullRepo } from "./executions";
import { useServer } from "../server";
import { useBuilder } from "../builder";
import { Box, Group } from "@mantine/core";
import ResourceLink from "../link";
import RepoLink from "@/components/repo-link";
export function useRepo(id: string | undefined) {
return useRead("ListRepos", {}).data?.find((r) => r.id === id);
@@ -58,7 +64,11 @@ export const RepoComponents: RequiredResourceComponents<
BatchExecutions: () => (
<BatchExecutions
type="Repo"
executions={["PullRepo", "CloneRepo", "BuildRepo"]}
executions={[
["PullRepo", ICONS.PullRepo],
["CloneRepo", ICONS.CloneRepo],
["BuildRepo", ICONS.Build],
]}
/>
),
@@ -92,9 +102,52 @@ export const RepoComponents: RequiredResourceComponents<
let state = useRepo(id)?.info.state;
return <StatusBadge text={state} intent={repoStateIntention(state)} />;
},
Info: {},
Info: {
Target: ({ id }) => {
const info = useRepo(id)?.info;
const server = useServer(info?.server_id);
const builder = useBuilder(info?.builder_id);
Executions: {},
if (!info?.server_id && !info?.builder_id) return null;
return (
<>
{server?.id && (
<Box>
<ResourceLink type="Server" id={server.id} />
</Box>
)}
{builder?.id && (
<Box>
<ResourceLink type="Builder" id={builder.id} />
</Box>
)}
</>
);
},
Source: ({ id }) => {
const info = useRepo(id)?.info;
if (!info?.repo || !info?.repo_link) return null;
return <RepoLink repo={info?.repo} link={info?.repo_link} />;
},
Branch: ({ id }) => {
const branch = useRepo(id)?.info.branch;
return (
<Group gap="xs" wrap="nowrap">
<ICONS.Branch size="1rem" />
{branch}
</Group>
);
},
},
Executions: {
CloneRepo,
PullRepo,
BuildRepo,
},
Config: RepoTabs,

View File

@@ -94,7 +94,7 @@ export default function ResourceSelector({
w="fit-content"
maw="100%"
rightSection={
<Group gap="xs" ml="sm">
<Group gap="xs" ml="sm" wrap="nowrap">
{clearable && (
<ActionIcon
size="sm"

View File

@@ -79,7 +79,6 @@ export default function ServerDockerResources({
tabs={tabsNoContent}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -70,13 +70,13 @@ export const ServerComponents: RequiredResourceComponents<
<BatchExecutions
type="Server"
executions={[
"PruneContainers",
"PruneNetworks",
"PruneVolumes",
"PruneImages",
"PruneSystem",
"RestartAllContainers",
"StopAllContainers",
["PruneContainers", ICONS.Container],
["PruneNetworks", ICONS.Network],
["PruneVolumes", ICONS.Volume],
["PruneImages", ICONS.Image],
["PruneSystem", ICONS.System],
["RestartAllContainers", ICONS.Restart],
["StopAllContainers", ICONS.Stop],
]}
/>
),

View File

@@ -1,7 +1,7 @@
import { useSelectedResources } from "@/lib/hooks";
import ResourceLink from "@/resources/link";
import { DataTable, SortableHeader } from "@/ui/data-table";
import { BoxProps, Group, Text } from "@mantine/core";
import { BoxProps, Group, Stack, Text } from "@mantine/core";
import { Types } from "komodo_client";
import { useServerStats, useServerThresholds } from "@/resources/server/hooks";
import StatCell from "@/ui/stat-cell";
@@ -102,7 +102,39 @@ function DiskCell({ id }: { id: string }) {
useServerThresholds(id);
const intent: "Good" | "Warning" | "Critical" =
perc < warning ? "Good" : perc < critical ? "Warning" : "Critical";
return <StatCell value={stats ? perc : undefined} intent={intent} />;
return (
<StatCell
value={stats ? perc : undefined}
intent={intent}
infoDisabled={!stats}
info={
<Stack gap="sm">
{stats?.disks.map((disk) => (
<Group
key={disk.mount}
justify="space-between"
className="bordered-light"
p="sm"
bdrs="sm"
>
<Group>
<Text c="dimmed">Mount:</Text>{" "}
<Text ff="monospace" bg="accent.6" px="xs" bdrs="sm">
{disk.mount}
</Text>
</Group>
<Text>-</Text>
<Group gap="0.4rem">
{disk.used_gb.toFixed(1)} GB <Text c="dimmed">of</Text>{" "}
{disk.total_gb.toFixed(1)} GB <Text c="dimmed">in use</Text> (
{((100 * disk.used_gb) / disk.total_gb).toFixed(1)} %)
</Group>
</Group>
))}
</Stack>
}
/>
);
}
function LoadAvgCell({ id }: { id: string }) {

View File

@@ -59,8 +59,11 @@ export default function StackConfigFiles({
placeholder="configs/config.yaml"
value={path}
onChange={(e) => {
values[i] = { ...values[i], path: e.target.value };
set({ config_files: [...values] });
set({
config_files: values.map((v, index) =>
i === index ? { ...v, path: e.target.value } : v,
),
});
}}
w={{ base: "100%", md: 400 }}
disabled={disabled}
@@ -91,8 +94,11 @@ export default function StackConfigFiles({
value={services}
data={allServices}
onChange={(services) => {
values[i] = { ...values[i], services };
set({ config_files: [...values] });
set({
config_files: values.map((v, index) =>
i === index ? { ...v, services } : v,
),
});
}}
disabled={disabled}
searchable
@@ -102,11 +108,16 @@ export default function StackConfigFiles({
value={requires}
onChange={(requires) => {
if (!requires) return;
values[i] = {
...values[i],
requires: requires as Types.StackFileRequires,
};
set({ config_files: [...values] });
set({
config_files: values.map((v, index) =>
i === index
? {
...v,
requires: requires as Types.StackFileRequires,
}
: v,
),
});
}}
disabled={disabled}
data={Object.values(Types.StackFileRequires)}

View File

@@ -279,12 +279,16 @@ export default function StackConfig({
<TextInput
value={file.path || ""}
onChange={(e) => {
const newFiles = [...files];
newFiles[i] = {
path: e.target.value,
track: file.track ?? true,
};
set({ additional_env_files: newFiles });
set({
additional_env_files: files.map((v, index) =>
i === index
? {
path: e.target.value,
track: file.track ?? true,
}
: v,
),
});
}}
placeholder=".env"
disabled={disabled}
@@ -296,9 +300,11 @@ export default function StackConfig({
label="Track"
checked={file.track ?? true}
onCheckedChange={(track) => {
const newFiles = [...files];
newFiles[i] = { ...newFiles[i], track };
set({ additional_env_files: newFiles });
set({
additional_env_files: files.map((v, index) =>
i === index ? { ...v, track } : v,
),
});
}}
disabled={disabled}
id={`track-${i}`}

View File

@@ -96,12 +96,12 @@ export const StackComponents: RequiredResourceComponents<
<BatchExecutions
type="Stack"
executions={[
"CheckStackForUpdate",
"PullStack",
"DeployStack",
"RestartStack",
"StopStack",
"DestroyStack",
["CheckStackForUpdate", ICONS.UpdateAvailable],
["PullStack", ICONS.Pull],
["DeployStack", ICONS.Deploy],
["RestartStack", ICONS.Restart],
["StopStack", ICONS.Stop],
["DestroyStack", ICONS.Destroy],
]}
/>
),

View File

@@ -34,7 +34,7 @@ export default function StackInfo({
message: res.success
? "Contents written."
: "Failed to write contents.",
color: res.success ? undefined : "red",
color: res.success ? "green" : "red",
});
},
});
@@ -55,7 +55,7 @@ export default function StackInfo({
const defaultShowContents = !latestContents || latestContents.length < 3;
return (
<Section gap="xl" titleOther={titleOther}>
<Section titleOther={titleOther}>
{/* Errors */}
{latestErrors &&
latestErrors.length > 0 &&
@@ -158,8 +158,8 @@ export default function StackInfo({
Reset
</Button>
<ConfirmUpdate
previous={{ contents: content.contents }}
content={{ contents: edits[content.path] }}
original={{ contents: content.contents }}
update={{ contents: edits[content.path] }}
onConfirm={async () => {
if (stack) {
return await mutateAsync({

View File

@@ -92,7 +92,6 @@ export default function SwarmDockerResources({
tabs={tabsNoContent}
value={view}
onValueChange={setView as any}
tabProps={{ w: 140 }}
/>
);

View File

@@ -11,7 +11,6 @@ import ConfirmButton from "@/ui/confirm-button";
import { useFullResourceSync } from ".";
import { useResourceSyncTabsView } from "./hooks";
import { fileContentsEmpty, resourceSyncNoChanges } from "@/lib/utils";
import { NotebookPen, SquarePlay } from "lucide-react";
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
export function RefreshSync({ id }: { id: string }) {
@@ -73,7 +72,7 @@ export function ExecuteSync({ id }: { id: string }) {
return (
<ConfirmModalWithDisable
confirmText={sync.name}
icon={<SquarePlay className="w-4 h-4" />}
icon={<ICONS.Run size="1rem" />}
onConfirm={() => execute({ sync: id })}
disabled={pending}
loading={pending}
@@ -106,7 +105,7 @@ export function CommitSync({ id }: { id: string }) {
if (freshSync) {
return (
<ConfirmButton
icon={<NotebookPen className="w-4 h-4" />}
icon={<ICONS.Commit size="1rem" />}
onClick={() => commit({ sync: id })}
disabled={isPending}
loading={isPending}
@@ -118,7 +117,7 @@ export function CommitSync({ id }: { id: string }) {
return (
<ConfirmModalWithDisable
confirmText={sync.name}
icon={<NotebookPen className="w-4 h-4" />}
icon={<ICONS.Commit size="1rem" />}
onConfirm={() => commit({ sync: id })}
disabled={isPending}
loading={isPending}

View File

@@ -69,7 +69,10 @@ export const ResourceSyncComponents: RequiredResourceComponents<
BatchExecutions: () => (
<BatchExecutions
type="ResourceSync"
executions={["RunSync", "CommitSync"]}
executions={[
["RunSync", ICONS.Run],
["CommitSync", ICONS.Commit],
]}
/>
),
@@ -142,7 +145,7 @@ export const ResourceSyncComponents: RequiredResourceComponents<
Executions: {
RefreshSync,
ExecuteSync,
ExecuteSync: ExecuteSync,
CommitSync,
},

View File

@@ -35,7 +35,7 @@ export default function ResourceSyncInfo({
message: res.success
? "Contents written."
: "Failed to write contents.",
color: res.success ? undefined : "red",
color: res.success ? "green" : "red",
});
},
},
@@ -170,8 +170,8 @@ export default function ResourceSyncInfo({
Reset
</Button>
<ConfirmUpdate
previous={{ contents: content.contents }}
content={{ contents: edits[keyPath] }}
original={{ contents: content.contents }}
update={{ contents: edits[keyPath] }}
onConfirm={async () => {
if (sync) {
return await writeContents({

View File

@@ -1,5 +1,7 @@
import {
AlarmClock,
ArrowDownToDot,
ArrowDownToLine,
Ban,
Bell,
Box,
@@ -25,6 +27,7 @@ import {
Factory,
FileDown,
FileText,
FlaskConical,
FolderCode,
FolderGit,
FolderSync,
@@ -44,6 +47,7 @@ import {
MemoryStick,
Milestone,
Network,
NotebookPen,
Package,
Pause,
Play,
@@ -59,6 +63,7 @@ import {
Server,
Settings,
Square,
SquarePlay,
SquareStack,
Table,
Tag,
@@ -132,6 +137,12 @@ export const ICONS = {
Pause,
Prune: Scissors,
Refresh: RefreshCcw,
Run: SquarePlay,
Test: FlaskConical,
Cancel: Ban,
CloneRepo: ArrowDownToLine,
PullRepo: ArrowDownToDot,
Commit: NotebookPen,
// MISC
User,
Users,
@@ -167,7 +178,6 @@ export const ICONS = {
Download,
Info,
Edit,
Cancel: Ban,
NotFound: SearchX,
Unknown: CircleQuestionMark,
};

View File

@@ -23,6 +23,7 @@ import {
Select,
Switch,
Table,
Tabs,
virtualColor,
} from "@mantine/core";
import { Types } from "komodo_client";
@@ -253,6 +254,14 @@ const theme = createTheme({
},
}),
}),
Tabs: Tabs.extend({
styles: (theme) => ({
list: {
backgroundColor: theme.colors.accent[1],
borderRadius: theme.radius.sm,
},
}),
}),
},
});

View File

@@ -5,14 +5,13 @@ import { Box, Button, Group, Modal, Stack, Text } from "@mantine/core";
import { useCtrlKeyListener, useKeyListener } from "@/lib/hooks";
import { fmtSnakeCaseToUpperSpaceCase } from "@/lib/formatting";
import { ICONS } from "@/theme/icons";
import { envToText } from "@/lib/utils";
import { deepCompare, envToText } from "@/lib/utils";
import { colorByIntention } from "@/lib/color";
import ShowHideButton from "@/ui/show-hide-button";
import ConfirmButton from "@/ui/confirm-button";
export default function ConfirmUpdate<T>({
previous,
content,
original,
update,
onConfirm,
loading,
disabled,
@@ -22,8 +21,8 @@ export default function ConfirmUpdate<T>({
openKeyListener = true,
confirmKeyListener = true,
}: {
previous: T;
content: Partial<T>;
original: T;
update: Partial<T>;
onConfirm: () => Promise<unknown>;
loading?: boolean;
disabled: boolean;
@@ -56,28 +55,54 @@ export default function ConfirmUpdate<T>({
return (
<>
<Modal title="Confirm Update" opened={opened} onClose={close} size="auto">
<Stack gap="xl" w={1400} maw="95vw" my="lg">
<Stack>
{Object.entries(content).map(([key, val], i) => (
<ConfirmUpdateItem
key={i}
_key={key as any}
val={val as any}
previous={previous}
language={language}
fileContentsLanguage={fileContentsLanguage}
/>
))}
<Modal
title={<Text size="xl">Confirm Update</Text>}
opened={opened}
onClose={close}
size="auto"
styles={{ content: { overflowY: "hidden" } }}
>
<Stack
gap="xl"
w={1400}
maw={{
base: "calc(100vw - 100px)",
xs: "calc(100vw - 150px)",
sm: "calc(100vw - 200px)",
md: "calc(100vw - 250px)",
}}
my="lg"
style={{ overflowY: "hidden" }}
>
<Stack
mah="min(calc(100vh - 300px), 800px)"
style={{ overflowY: "auto" }}
>
{Object.entries(update)
.filter(([key, val]) => !deepCompare((original as any)[key], val))
.map(([key, val], i) => (
<ConfirmUpdateItem
key={i}
_key={key as any}
val={val as any}
previous={original}
language={language}
fileContentsLanguage={fileContentsLanguage}
/>
))}
</Stack>
<Group justify="flex-end">
<ConfirmButton
icon={<ICONS.Save size="1rem" />}
onClick={handleConfirm}
<Button
leftSection={<ICONS.Save size="1rem" />}
onClick={(e) => {
e.stopPropagation();
handleConfirm();
}}
w={{ base: "100%", xs: 200 }}
loading={loading}
>
Save
</ConfirmButton>
</Button>
</Group>
</Stack>
</Modal>

View File

@@ -177,8 +177,8 @@ export default function Config<T>({
Reset
</Button>
<ConfirmUpdate
previous={original}
content={update}
original={original}
update={update}
onConfirm={onConfirm}
disabled={disabled}
fileContentsLanguage={fileContentsLanguage}
@@ -207,11 +207,7 @@ export default function Config<T>({
</>
)}
{!disableSidebar && (
<Flex
w="100%"
gap="md"
direction={{ base: "column", lg: "row" }}
>
<Flex w="100%" gap="md" direction={{ base: "column", lg: "row" }}>
{/** SIDEBAR (LG) */}
<Box
visibleFrom="lg"
@@ -223,7 +219,7 @@ export default function Config<T>({
borderTopRightRadius: "var(--mantine-radius-md)",
}}
>
<Stack pos="sticky" w={175} top={94} pb={24} m="lg">
<Stack pos="sticky" w={175} top={88} pb={24} m="lg">
{/** ANCHORS */}
<ScrollArea
mah={
@@ -267,7 +263,9 @@ export default function Config<T>({
</ScrollArea>
{/** SAVE */}
<SaveOrReset fullWidth />
<Stack gap="xs">
<SaveOrReset fullWidth />
</Stack>
</Stack>
</Box>

View File

@@ -70,7 +70,7 @@ const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
clickedOnce ? (
<Check size="1rem" />
) : loading ? (
<Loader color="white" size="1rem" />
<Loader size="1rem" />
) : (
(rightSection ?? icon ?? <ICONS.Unknown size="1rem" />)
)

Some files were not shown because too many files have changed in this diff Show More