diff --git a/Cargo.lock b/Cargo.lock index 4438e2b02..30f79f7b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "cache" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "tokio", @@ -1074,7 +1074,7 @@ dependencies = [ [[package]] name = "command" -version = "1.19.2" +version = "1.19.3" dependencies = [ "komodo_client", "run_command", @@ -1102,7 +1102,7 @@ checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" [[package]] name = "config" -version = "1.19.2" +version = "1.19.3" dependencies = [ "colored", "indexmap 2.11.0", @@ -1342,7 +1342,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "database" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "async-compression", @@ -1641,7 +1641,7 @@ dependencies = [ [[package]] name = "environment" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "formatting", @@ -1651,7 +1651,7 @@ dependencies = [ [[package]] name = "environment_file" -version = "1.19.2" +version = "1.19.3" dependencies = [ "thiserror 2.0.16", ] @@ -1741,7 +1741,7 @@ dependencies = [ [[package]] name = "formatting" -version = "1.19.2" +version = "1.19.3" dependencies = [ "serror", ] @@ -1903,7 +1903,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "cache", @@ -2511,7 +2511,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "komodo_client", @@ -2642,7 +2642,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "chrono", @@ -2667,7 +2667,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "async_timing_util", @@ -2702,7 +2702,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "arc-swap", @@ -2772,7 +2772,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "arc-swap", @@ -2894,7 +2894,7 @@ dependencies = [ [[package]] name = "logger" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "komodo_client", @@ -3637,7 +3637,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "komodo_client", @@ -4168,7 +4168,7 @@ dependencies = [ [[package]] name = "response" -version = "1.19.2" +version = "1.19.3" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 97083911f..6ff486fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "1.19.2" +version = "1.19.3" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/cli/src/command/execute.rs b/bin/cli/src/command/execute.rs index eb763ddfe..f0510ee83 100644 --- a/bin/cli/src/command/execute.rs +++ b/bin/cli/src/command/execute.rs @@ -549,20 +549,20 @@ async fn poll_update_until_complete( } else { format!("{}/updates/{}", cli_config().host, update.id) }; - info!("Link: '{}'", link.bold()); + println!("Link: '{}'", link.bold()); let client = super::komodo_client().await?; let timer = tokio::time::Instant::now(); let update = client.poll_update_until_complete(&update.id).await?; if update.success { - info!( + println!( "FINISHED in {}: {}", format!("{:.1?}", timer.elapsed()).bold(), "EXECUTION SUCCESSFUL".green(), ); } else { - warn!( + eprintln!( "FINISHED in {}: {}", format!("{:.1?}", timer.elapsed()).bold(), "EXECUTION FAILED".red(), diff --git a/bin/core/src/alert/discord.rs b/bin/core/src/alert/discord.rs index 45ce19c18..427227d48 100644 --- a/bin/core/src/alert/discord.rs +++ b/bin/core/src/alert/discord.rs @@ -29,12 +29,12 @@ pub async fn send_alert( match alert.level { SeverityLevel::Ok => { format!( - "{level} | **{name}** ({region}) | Server version now matches core version ✅\n{link}" + "{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}" ) } _ => { format!( - "{level} | **{name}** ({region}) | Version mismatch detected ⚠️\nServer: **{server_version}** | Core: **{core_version}**\n{link}" + "{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}" ) } } diff --git a/bin/core/src/alert/mod.rs b/bin/core/src/alert/mod.rs index caf9a0243..9eba5dab7 100644 --- a/bin/core/src/alert/mod.rs +++ b/bin/core/src/alert/mod.rs @@ -275,12 +275,12 @@ fn standard_alert_content(alert: &Alert) -> String { match alert.level { SeverityLevel::Ok => { format!( - "{level} | {name} ({region}) | Server version now matches core version ✅\n{link}" + "{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}" ) } _ => { format!( - "{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}\n{link}" + "{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}" ) } } diff --git a/bin/core/src/alert/slack.rs b/bin/core/src/alert/slack.rs index 4fa43815c..d62d92e1a 100644 --- a/bin/core/src/alert/slack.rs +++ b/bin/core/src/alert/slack.rs @@ -34,12 +34,12 @@ pub async fn send_alert( let text = match alert.level { SeverityLevel::Ok => { format!( - "{level} | {name} ({region}) | Server version now matches core version ✅" + "{level} | *{name}*{region} | Periphery version now matches Core version ✅" ) } _ => { format!( - "{level} | {name} ({region}) | Version mismatch detected ⚠️\nServer: {server_version} | Core: {core_version}" + "{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}" ) } }; diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 6585cf899..06dfe9ac4 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -853,9 +853,15 @@ pub async fn delete( ); update.push_simple_log("Deleted Toml", toml); - if let Err(e) = T::post_delete(&resource, &mut update).await { - update.push_error_log("post delete", format_serror(&e.into())); - } + tokio::join!( + async { + if let Err(e) = T::post_delete(&resource, &mut update).await { + update + .push_error_log("post delete", format_serror(&e.into())); + } + }, + delete_from_alerters::(&resource.id) + ); refresh_all_resources_cache().await; @@ -865,6 +871,26 @@ pub async fn delete( Ok(resource) } +async fn delete_from_alerters(id: &str) { + let target_bson = doc! { + "type": T::resource_type().as_ref(), + "id": id, + }; + if let Err(e) = db_client() + .alerters + .update_many(Document::new(), doc! { + "$pull": { + "config.resources": &target_bson, + "config.except_resources": target_bson, + } + }) + .await + .context("Failed to clear deleted resource from alerter whitelist / blacklist") + { + warn!("{e:#}"); + } +} + // ======= #[instrument(level = "debug")] diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 9d44196ed..07108f3a3 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -300,6 +300,7 @@ async fn get_repo_state_from_db(id: &str) -> RepoState { "$or": [ { "operation": "CloneRepo" }, { "operation": "PullRepo" }, + { "operation": "BuildRepo" }, ], }) .with_options( diff --git a/client/core/ts/package.json b/client/core/ts/package.json index a130594a7..e8e196fa8 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -1,6 +1,6 @@ { "name": "komodo_client", - "version": "1.19.2", + "version": "1.19.3", "description": "Komodo client package", "homepage": "https://komo.do", "main": "dist/lib.js", diff --git a/config/core.config.toml b/config/core.config.toml index 7396a7a7e..5abe9fa12 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -9,14 +9,14 @@ ## All fields with a "Default" provided are optional. If they are ## left out of the file, the "Default" value will be used. -## This file is bundled into the official image, `ghcr.io/moghtech/komodo`, +## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`, ## as the default config at `/config/.default.config.toml`. -## Komodo can start with no external config file mounted. +## Komodo Core can start with no external config file mounted. ## Most fields can also be configured using environment variables. ## Environment variables will override values set in this file. -## Can also use JSON or YAML if preffered. You can convert here: +## Can also use JSON or YAML if preferred. You can convert here: ## - YAML: https://it-tools.tech/toml-to-yaml ## - JSON: https://it-tools.tech/toml-to-json diff --git a/config/komodo.cli.toml b/config/komodo.cli.toml index 05ba38e33..ff2b6eeb5 100644 --- a/config/komodo.cli.toml +++ b/config/komodo.cli.toml @@ -9,7 +9,7 @@ ## Most fields can also be configured using cli arguments and environment variables. ## These will will override values set in this file. (cli args > env > config files). -## You can also use JSON or YAML if preffered. You can convert here: +## You can also use JSON or YAML if preferred. You can convert here: ## - YAML: https://it-tools.tech/toml-to-yaml ## - JSON: https://it-tools.tech/toml-to-json diff --git a/config/periphery.config.toml b/config/periphery.config.toml index 3145c014e..cce5396eb 100644 --- a/config/periphery.config.toml +++ b/config/periphery.config.toml @@ -16,7 +16,7 @@ ## Most fields can also be configured using environment variables. ## Environment variables will override values set in this file. -## You can also use JSON or YAML if preffered. You can convert here: +## You can also use JSON or YAML if preferred. You can convert here: ## - YAML: https://it-tools.tech/toml-to-yaml ## - JSON: https://it-tools.tech/toml-to-json diff --git a/docsite/docs/setup/index.mdx b/docsite/docs/setup/index.mdx index 9a918ab88..d53d3e292 100644 --- a/docsite/docs/setup/index.mdx +++ b/docsite/docs/setup/index.mdx @@ -5,10 +5,12 @@ To run Komodo, you will need Docker. See [the docker install docs](https://docs. ### Deploy with Docker Compose - [**Using MongoDB**](./mongo.mdx) - - Lower CPU usage, Higher RAM usage. - - Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59). - [**Using FerretDB** (Postgres)](./ferretdb.mdx) - - Lower RAM usage, Higher CPU usage. + +:::info +Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59). +Users with these systems should use FerretDB instead. +::: :::info **FerretDB v1** users: diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx index 7f6c06268..a63a2155f 100644 --- a/frontend/src/components/layouts.tsx +++ b/frontend/src/components/layouts.tsx @@ -170,6 +170,7 @@ interface SectionProps { actions?: ReactNode; // otherwise items-start itemsCenterTitleRow?: boolean; + className?: string; } export const Section = ({ @@ -180,8 +181,9 @@ export const Section = ({ actions, children, itemsCenterTitleRow, + className, }: SectionProps) => ( -
+
{(title || icon || titleRight || titleOther || actions) && (
{ const [open, set] = useState(false); const [loading, setLoading] = useState(false); + return ( { setLoading(true); - await onConfirm(); - setLoading(false); - set(false); + try { + await onConfirm(); + set(false); + } catch (error) { + console.error("Error creating resource:", error); + } finally { + setLoading(false); + } }} disabled={!enabled || loading} > diff --git a/frontend/src/components/monaco.tsx b/frontend/src/components/monaco.tsx index 86fc9962e..6e24481a4 100644 --- a/frontend/src/components/monaco.tsx +++ b/frontend/src/components/monaco.tsx @@ -172,7 +172,7 @@ export const MonacoEditor = ({ language={language} value={value} theme={theme} - defaultPath={filename ? `file:///${filename}` : undefined} + defaultPath={defaultPath(filename)} options={options} onChange={(v) => onValueChange?.(v ?? "")} onMount={(editor) => setEditor(editor)} @@ -181,6 +181,14 @@ export const MonacoEditor = ({ ); }; +const defaultPath = (filename?: string) => { + if (!filename) return undefined; + // Extract only the filename part of path, + // avoiding critical issue when path starts with '/' + const split = filename.split("/"); + return split[split.length - 1]; +}; + const MIN_DIFF_HEIGHT = 100; const MAX_DIFF_HEIGHT = 400; diff --git a/frontend/src/components/resources/resource-sync/actions.tsx b/frontend/src/components/resources/resource-sync/actions.tsx index d3532620a..bd466e5f2 100644 --- a/frontend/src/components/resources/resource-sync/actions.tsx +++ b/frontend/src/components/resources/resource-sync/actions.tsx @@ -7,7 +7,7 @@ import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks"; import { file_contents_empty, sync_no_changes } from "@lib/utils"; import { usePermissions } from "@lib/hooks"; import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react"; -import { useFullResourceSync, usePendingView } from "."; +import { useFullResourceSync, useResourceSyncTabsView } from "."; export const RefreshSync = ({ id }: { id: string }) => { const inv = useInvalidate(); @@ -34,11 +34,10 @@ export const ExecuteSync = ({ id }: { id: string }) => { { refetchInterval: 5000 } ).data?.syncing; const sync = useFullResourceSync(id); - const [_pendingView] = usePendingView(); - const pendingView = sync?.config?.managed ? _pendingView : "Execute"; + const { view } = useResourceSyncTabsView(sync); if ( - pendingView === "Commit" || + view !== "Execute" || !sync || sync_no_changes(sync) || !sync.info?.remote_contents @@ -73,11 +72,12 @@ export const ExecuteSync = ({ id }: { id: string }) => { export const CommitSync = ({ id }: { id: string }) => { const { mutate, isPending } = useWrite("CommitSync"); const sync = useFullResourceSync(id); + const { view } = useResourceSyncTabsView(sync); const { canWrite } = usePermissions({ type: "ResourceSync", id }); - const [_pendingView] = usePendingView(); - const pendingView = sync?.config?.managed ? _pendingView : "Execute"; - if (pendingView === "Execute" || !canWrite || !sync) return null; + if (view !== "Commit" || !canWrite || !sync) { + return null; + } const freshSync = !sync.config?.files_on_host && diff --git a/frontend/src/components/resources/resource-sync/index.tsx b/frontend/src/components/resources/resource-sync/index.tsx index f8f1e07ec..6eae5b1c2 100644 --- a/frontend/src/components/resources/resource-sync/index.tsx +++ b/frontend/src/components/resources/resource-sync/index.tsx @@ -1,4 +1,4 @@ -import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks"; +import { atomWithStorage, useRead, useUser } from "@lib/hooks"; import { RequiredResourceComponents } from "@types"; import { Card } from "@ui/card"; import { Clock, FolderSync } from "lucide-react"; @@ -45,23 +45,16 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => { return ; }; -const pendingViewAtom = atomWithStorage<"Execute" | "Commit">( - "sync-view-v1", - "Execute" +type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit"; +const syncTabsViewAtom = atomWithStorage( + "sync-tabs-v4", + "Config" ); -export const usePendingView = () => { - return useAtom(pendingViewAtom) as [ - "Execute" | "Commit", - (view: "Execute" | "Commit") => void, - ]; -}; -const ConfigInfoPending = ({ id }: { id: string }) => { - const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">( - "sync-tabs-v3", - "Config" - ); - const sync = useFullResourceSync(id); +export const useResourceSyncTabsView = ( + sync: Types.ResourceSync | undefined +) => { + const [_view, setView] = useAtom(syncTabsViewAtom); const hideInfo = sync?.config?.files_on_host ? false @@ -75,13 +68,28 @@ const ConfigInfoPending = ({ id }: { id: string }) => { const view = _view === "Info" && hideInfo ? "Config" - : _view === "Pending" && !showPending + : (_view === "Execute" || _view === "Commit") && !showPending ? sync?.config?.files_on_host || sync?.config?.repo || sync?.config?.linked_repo ? "Info" : "Config" - : _view; + : _view === "Commit" && !sync?.config?.managed + ? "Execute" + : _view; + + return { + view, + setView, + hideInfo, + showPending, + }; +}; + +const ConfigInfoPending = ({ id }: { id: string }) => { + const sync = useFullResourceSync(id); + const { view, setView, hideInfo, showPending } = + useResourceSyncTabsView(sync); const title = ( @@ -96,12 +104,21 @@ const ConfigInfoPending = ({ id }: { id: string }) => { Info - Pending + Execute + {sync?.config?.managed && ( + + Commit + + )} ); return ( @@ -112,7 +129,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => { - + + + + @@ -164,7 +184,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = { }, GroupActions: () => ( - + ), Table: ({ resources }) => ( diff --git a/frontend/src/components/resources/resource-sync/pending.tsx b/frontend/src/components/resources/resource-sync/pending.tsx index 278bee1f1..cc06b8785 100644 --- a/frontend/src/components/resources/resource-sync/pending.tsx +++ b/frontend/src/components/resources/resource-sync/pending.tsx @@ -10,8 +10,7 @@ import { cn, sanitizeOnlySpan } from "@lib/utils"; import { ConfirmButton } from "@components/util"; import { SquarePlay } from "lucide-react"; import { usePermissions } from "@lib/hooks"; -import { useFullResourceSync, usePendingView } from "."; -import { Tabs, TabsList, TabsTrigger } from "@ui/tabs"; +import { useFullResourceSync, useResourceSyncTabsView } from "."; import { ResourceDiff } from "komodo_client/dist/types"; export const ResourceSyncPending = ({ @@ -24,31 +23,16 @@ export const ResourceSyncPending = ({ const syncing = useRead("GetResourceSyncActionState", { sync: id }).data ?.syncing; const sync = useFullResourceSync(id); + const { view } = useResourceSyncTabsView(sync); const { canExecute } = usePermissions({ type: "ResourceSync", id }); - const [_pendingView, setPendingView] = usePendingView(); - const pendingView = sync?.config?.managed ? _pendingView : "Execute"; const { mutate, isPending } = useExecute("RunSync"); const loading = isPending || syncing; return ( -
-
- {sync?.config?.managed && ( - - - - Execute - - - Commit - - - - )} -
{pendingView} Mode:
+
+
+
{view} Mode:
- {pendingView === "Execute" && ( + {view === "Execute" && ( <> Update resources in the
UI
@@ -56,7 +40,7 @@ export const ResourceSyncPending = ({
file changes.
)} - {pendingView === "Commit" && ( + {view === "Commit" && ( <> Update resources in the
file
@@ -89,7 +73,7 @@ export const ResourceSyncPending = ({ ) : undefined} {/* Pending Deploy */} - {pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? ( + {view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
- {pendingView === "Commit" + {view === "Commit" ? reverse_pending_type(update.data.type) : update.data.type}{" "} {update.target.type} @@ -139,7 +120,7 @@ export const ResourceSyncPending = ({ /> )}
- {canExecute && pendingView === "Execute" && ( + {canExecute && view === "Execute" && ( } @@ -168,7 +149,7 @@ export const ResourceSyncPending = ({ )} {update.data.type === "Update" && ( <> - {pendingView === "Execute" && ( + {view === "Execute" && ( )} - {pendingView === "Commit" && ( + {view === "Commit" && ( - {pendingView === "Commit" - ? reverse_pending_type(data.type) - : data.type}{" "} + {view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "} Variable
@@ -224,7 +203,7 @@ export const ResourceSyncPending = ({ )} {data.type === "Update" && ( <> - {pendingView === "Execute" && ( + {view === "Execute" && ( )} - {pendingView === "Commit" && ( + {view === "Commit" && ( - {pendingView === "Commit" - ? reverse_pending_type(data.type) - : data.type}{" "} + {view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "} User Group @@ -280,7 +257,7 @@ export const ResourceSyncPending = ({ )} {data.type === "Update" && ( <> - {pendingView === "Execute" && ( + {view === "Execute" && ( )} - {pendingView === "Commit" && ( + {view === "Commit" && ( ({ date: convertTsMsToLocalUnixTsInMs(s.ts), - value: (s.load_average?.one ?? 0), + value: s.load_average?.one ?? 0, })); const five = records.map((s) => ({ date: convertTsMsToLocalUnixTsInMs(s.ts), - value: (s.load_average?.five ?? 0), + value: s.load_average?.five ?? 0, })); const fifteen = records.map((s) => ({ date: convertTsMsToLocalUnixTsInMs(s.ts), - value: (s.load_average?.fifteen ?? 0), + value: s.load_average?.fifteen ?? 0, })); return [ { label: "1m", data: one }, @@ -65,9 +71,13 @@ export const StatChart = ({
- ) : seriesData.length > 0 ? ( - s.data)} seriesData={seriesData} /> - ) : null} + ) : ( + s.data)} + seriesData={seriesData} + /> + )}
); }; @@ -113,10 +123,12 @@ export const InnerStatChart = ({ cursor: (_value?: Date) => false, }, }; - }, []); + }, [min, max, diff]); // Determine the dynamic scaling for network-related types - const allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) => s.data.map((d) => d.value)); + const allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) => + s.data.map((d) => d.value) + ); const maxStatValue = Math.max(...(allValues.length ? allValues : [0])); const { unit, maxUnitValue } = useMemo(() => { @@ -133,7 +145,10 @@ export const InnerStatChart = ({ } if (type === "Load Average") { // Leave unitless; set max slightly above observed - return { unit: "", maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2 }; + return { + unit: "", + maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2, + }; } return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk }, [type, maxStatValue]); @@ -161,6 +176,16 @@ export const InnerStatChart = ({ ], [type, maxUnitValue, unit] ); + + if ((seriesData?.[0]?.data.length ?? 0) < 2) { + return ( +
+ +

Not enough data yet, choose a smaller interval.

+
+ ); + } + return (
+ - { ); }; -const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | undefined }) => { +const LOAD_AVERAGE = ({ + id, + stats, +}: { + id: string; + stats: Types.SystemStats | undefined; +}) => { if (!stats?.load_average) return null; const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {}; - const cores = useRead("GetSystemInformation", { server: id }).data?.core_count; + const cores = useRead("GetSystemInformation", { server: id }).data + ?.core_count; - const pct = (load: number) => (cores && cores > 0) ? Math.min((load / cores) * 100, 100) : undefined; + const pct = (load: number) => + cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined; const textColor = (load: number) => { const p = pct(load); if (p === undefined) return "text-muted-foreground"; - return p <= 50 ? "text-green-600" : p <= 80 ? "text-yellow-600" : "text-red-600"; + return p <= 50 + ? "text-green-600" + : p <= 80 + ? "text-yellow-600" + : "text-red-600"; }; return ( @@ -524,15 +530,18 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un {/* Current Load */}
- {one.toFixed(2)} + + {one.toFixed(2)} + - {cores && cores > 0 ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores` : "N/A"} + {cores && cores > 0 + ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores` + : "N/A"}
- +
{/* Time Intervals */} @@ -546,14 +555,13 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
{label} - + {(value as number).toFixed(2)}
- +
))}
diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index a7d05df97..434490389 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -805,3 +805,24 @@ export const useContainerPortsMap = (ports: Types.Port[]) => { return map; }, [ports]); }; + +/** + * A custom React hook that debounces a value, delaying its update until after + * a specified period of inactivity. This is useful for performance optimization + * in scenarios like search inputs, form validation, or API calls. + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} \ No newline at end of file diff --git a/frontend/src/pages/containers.tsx b/frontend/src/pages/containers.tsx index 084815197..089e3eca3 100644 --- a/frontend/src/pages/containers.tsx +++ b/frontend/src/pages/containers.tsx @@ -6,33 +6,66 @@ import { StatusBadge, } from "@components/util"; import { container_state_intention } from "@lib/color"; -import { useRead } from "@lib/hooks"; +import { useDebounce, useRead } from "@lib/hooks"; import { DataTable, SortableHeader } from "@ui/data-table"; import { Input } from "@ui/input"; -import { Box, Search } from "lucide-react"; +import { MultiSelect } from "@ui/multi-select"; +import { Box, Search, RotateCcw } from "lucide-react"; +import { Button } from "@ui/button"; import { Fragment, useCallback, useMemo, useState } from "react"; export default function ContainersPage() { const [search, setSearch] = useState(""); - const searchSplit = search + const [selectedServers, setSelectedServers] = useState([]); + + const debouncedSearch = useDebounce(search, 300); + + const searchSplit = debouncedSearch .toLowerCase() .split(" ") .filter((term) => term); + const servers = useRead("ListServers", {}).data; + const serverOptions = useMemo( + () => + servers?.map((server) => ({ + label: server.name, + value: server.id, + })) || [], + [servers] + ); + const serverName = useCallback( (id: string) => servers?.find((server) => server.id === id)?.name, [servers] ); + const _containers = useRead("ListAllDockerContainers", {}).data; + const containers = useMemo( () => _containers?.filter((c) => { - if (searchSplit.length === 0) return true; - const lower = c.name.toLowerCase(); - return searchSplit.every((search) => lower.includes(search)); + if (searchSplit.length > 0) { + const lower = c.name.toLowerCase(); + const searchMatch = searchSplit.every((search) => + lower.includes(search) + ); + if (!searchMatch) return false; + } + + if (selectedServers.length > 0) { + return selectedServers.includes(c.server_id!); + } + + return true; }), - [_containers, searchSplit] + [_containers, searchSplit, selectedServers] ); + + const clearAllServers = useCallback(() => { + setSelectedServers([]); + }, []); + return ( } >
-
-
+
+
+ {/* Server Filter Multi-Select */} +
+ +
+ + {/* Reset Server Filter Button */} + {selectedServers.length > 0 && ( + + )} +
+ + {/* Search Input */}
setSearch(e.target.value)} placeholder="search..." - className="pl-8 w-[200px] lg:w-[300px]" + className="pl-8 w-[200px] lg:w-[300px] py-0 h-10" />
+ ), }, - // { - // accessorKey: "volumes.0", - // minSize: 300, - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => ( - //
- // {row.original.volumes.map((volume, i) => ( - // - // - // {i !== row.original.volumes.length - 1 && ( - //
|
- // )} - //
- // ))} - //
- // ), - // }, ]} />
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 6bbc3d0a5..eb9fcd0b6 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -15,7 +15,7 @@ import { useLoginOptions, useUserInvalidate, } from "@lib/hooks"; -import { type FormEvent } from "react"; +import { useRef } from "react"; import { ThemeToggle } from "@ui/theme"; import { KOMODO_BASE_URL } from "@main"; import { KeyRound, X } from "lucide-react"; @@ -40,6 +40,7 @@ export default function Login() { const options = useLoginOptions().data; const userInvalidate = useUserInvalidate(); const { toast } = useToast(); + const formRef = useRef(null); // If signing in another user, need to redirect away from /login manually const maybeNavigate = location.pathname.startsWith("/login") @@ -63,13 +64,13 @@ export default function Login() { const message = e?.response?.data?.error as string | undefined; if (message) { toast({ - title: `Failed to login user. '${message}'`, + title: `Failed to sign up user. '${message}'`, variant: "destructive", }); console.error(e); } else { toast({ - title: "Failed to login user. See console log for details.", + title: "Failed to sign up user. See console log for details.", variant: "destructive", }); console.error(e); @@ -97,17 +98,24 @@ export default function Login() { }, }); - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - const fd = new FormData(e.currentTarget); + const getFormCredentials = () => { + if (!formRef.current) return undefined; + const fd = new FormData(formRef.current); const username = String(fd.get("username") ?? ""); const password = String(fd.get("password") ?? ""); - const action = String(fd.get("action") ?? "login"); - if (action === "signup") { - signup({ username, password }); - } else { - login({ username, password }); - } + return { username, password }; + }; + + const handleLogin = () => { + const creds = getFormCredentials(); + if (!creds) return; + login(creds); + }; + + const handleSignUp = () => { + const creds = getFormCredentials(); + if (!creds) return; + signup(creds); }; const no_auth_configured = @@ -176,7 +184,7 @@ export default function Login() { {options?.local && (
@@ -204,9 +212,9 @@ export default function Login() { {show_sign_up && (