mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-19 19:10:42 -06:00
1.19.3 (#792)
* start. 1.19.3 * deploy 1.19.3-dev-1 * repo state from db includes BuildRepo success * clean up version mismatch text * feat(containers): debounced search input and added filter by server name (#796) * Fix cleaning Alerter resource whitelist / blacklist on resource delete re #581 * fmt * Fix signup button not working correctly (#801) * Improve route protection and authentication flow (#798) * Improve route protection and authentication flow * Cleanup * fix: inconsistent behaviour of new resource create button (#800) * fix monaco crashing with absolute path config files * deploy 1.19.3-dev-2 * proofread config * Fix #427 * deploy 1.19.3-dev-3 * poll logs use println * Sync: Only show commit / execute when viewing pending tab * Improve sync UX * deploy 1.19.3-dev-4 * bold link * remove claims about database resource usage. * 1.19.3 --------- Co-authored-by: mbecker20 <max@mogh.tech> Co-authored-by: Antonio Sarro <tech@antoniosarro.dev> Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
This commit is contained in:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -907,7 +907,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cache"
|
name = "cache"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1074,7 +1074,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "command"
|
name = "command"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"komodo_client",
|
"komodo_client",
|
||||||
"run_command",
|
"run_command",
|
||||||
@@ -1102,7 +1102,7 @@ checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"colored",
|
"colored",
|
||||||
"indexmap 2.11.0",
|
"indexmap 2.11.0",
|
||||||
@@ -1342,7 +1342,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "database"
|
name = "database"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -1641,7 +1641,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "environment"
|
name = "environment"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"formatting",
|
"formatting",
|
||||||
@@ -1651,7 +1651,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "environment_file"
|
name = "environment_file"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.16",
|
||||||
]
|
]
|
||||||
@@ -1741,7 +1741,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "formatting"
|
name = "formatting"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serror",
|
"serror",
|
||||||
]
|
]
|
||||||
@@ -1903,7 +1903,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git"
|
name = "git"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cache",
|
"cache",
|
||||||
@@ -2511,7 +2511,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolate"
|
name = "interpolate"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"komodo_client",
|
"komodo_client",
|
||||||
@@ -2642,7 +2642,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "komodo_cli"
|
name = "komodo_cli"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2667,7 +2667,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "komodo_client"
|
name = "komodo_client"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async_timing_util",
|
"async_timing_util",
|
||||||
@@ -2702,7 +2702,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "komodo_core"
|
name = "komodo_core"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@@ -2772,7 +2772,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "komodo_periphery"
|
name = "komodo_periphery"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
@@ -2894,7 +2894,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "logger"
|
name = "logger"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"komodo_client",
|
"komodo_client",
|
||||||
@@ -3637,7 +3637,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "periphery_client"
|
name = "periphery_client"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"komodo_client",
|
"komodo_client",
|
||||||
@@ -4168,7 +4168,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "response"
|
name = "response"
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.19.2"
|
version = "1.19.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|||||||
@@ -549,20 +549,20 @@ async fn poll_update_until_complete(
|
|||||||
} else {
|
} else {
|
||||||
format!("{}/updates/{}", cli_config().host, update.id)
|
format!("{}/updates/{}", cli_config().host, update.id)
|
||||||
};
|
};
|
||||||
info!("Link: '{}'", link.bold());
|
println!("Link: '{}'", link.bold());
|
||||||
|
|
||||||
let client = super::komodo_client().await?;
|
let client = super::komodo_client().await?;
|
||||||
|
|
||||||
let timer = tokio::time::Instant::now();
|
let timer = tokio::time::Instant::now();
|
||||||
let update = client.poll_update_until_complete(&update.id).await?;
|
let update = client.poll_update_until_complete(&update.id).await?;
|
||||||
if update.success {
|
if update.success {
|
||||||
info!(
|
println!(
|
||||||
"FINISHED in {}: {}",
|
"FINISHED in {}: {}",
|
||||||
format!("{:.1?}", timer.elapsed()).bold(),
|
format!("{:.1?}", timer.elapsed()).bold(),
|
||||||
"EXECUTION SUCCESSFUL".green(),
|
"EXECUTION SUCCESSFUL".green(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
eprintln!(
|
||||||
"FINISHED in {}: {}",
|
"FINISHED in {}: {}",
|
||||||
format!("{:.1?}", timer.elapsed()).bold(),
|
format!("{:.1?}", timer.elapsed()).bold(),
|
||||||
"EXECUTION FAILED".red(),
|
"EXECUTION FAILED".red(),
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ pub async fn send_alert(
|
|||||||
match alert.level {
|
match alert.level {
|
||||||
SeverityLevel::Ok => {
|
SeverityLevel::Ok => {
|
||||||
format!(
|
format!(
|
||||||
"{level} | **{name}** ({region}) | Server version now matches core version ✅\n{link}"
|
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
format!(
|
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}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,12 +275,12 @@ fn standard_alert_content(alert: &Alert) -> String {
|
|||||||
match alert.level {
|
match alert.level {
|
||||||
SeverityLevel::Ok => {
|
SeverityLevel::Ok => {
|
||||||
format!(
|
format!(
|
||||||
"{level} | {name} ({region}) | Server version now matches core version ✅\n{link}"
|
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
format!(
|
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}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ pub async fn send_alert(
|
|||||||
let text = match alert.level {
|
let text = match alert.level {
|
||||||
SeverityLevel::Ok => {
|
SeverityLevel::Ok => {
|
||||||
format!(
|
format!(
|
||||||
"{level} | {name} ({region}) | Server version now matches core version ✅"
|
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
format!(
|
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}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -853,9 +853,15 @@ pub async fn delete<T: KomodoResource>(
|
|||||||
);
|
);
|
||||||
update.push_simple_log("Deleted Toml", toml);
|
update.push_simple_log("Deleted Toml", toml);
|
||||||
|
|
||||||
if let Err(e) = T::post_delete(&resource, &mut update).await {
|
tokio::join!(
|
||||||
update.push_error_log("post delete", format_serror(&e.into()));
|
async {
|
||||||
}
|
if let Err(e) = T::post_delete(&resource, &mut update).await {
|
||||||
|
update
|
||||||
|
.push_error_log("post delete", format_serror(&e.into()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete_from_alerters::<T>(&resource.id)
|
||||||
|
);
|
||||||
|
|
||||||
refresh_all_resources_cache().await;
|
refresh_all_resources_cache().await;
|
||||||
|
|
||||||
@@ -865,6 +871,26 @@ pub async fn delete<T: KomodoResource>(
|
|||||||
Ok(resource)
|
Ok(resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_from_alerters<T: KomodoResource>(id: &str) {
|
||||||
|
let target_bson = doc! {
|
||||||
|
"type": T::resource_type().as_ref(),
|
||||||
|
"id": id,
|
||||||
|
};
|
||||||
|
if let Err(e) = db_client()
|
||||||
|
.alerters
|
||||||
|
.update_many(Document::new(), doc! {
|
||||||
|
"$pull": {
|
||||||
|
"config.resources": &target_bson,
|
||||||
|
"config.except_resources": target_bson,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Failed to clear deleted resource from alerter whitelist / blacklist")
|
||||||
|
{
|
||||||
|
warn!("{e:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =======
|
// =======
|
||||||
|
|
||||||
#[instrument(level = "debug")]
|
#[instrument(level = "debug")]
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ async fn get_repo_state_from_db(id: &str) -> RepoState {
|
|||||||
"$or": [
|
"$or": [
|
||||||
{ "operation": "CloneRepo" },
|
{ "operation": "CloneRepo" },
|
||||||
{ "operation": "PullRepo" },
|
{ "operation": "PullRepo" },
|
||||||
|
{ "operation": "BuildRepo" },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.with_options(
|
.with_options(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "komodo_client",
|
"name": "komodo_client",
|
||||||
"version": "1.19.2",
|
"version": "1.19.3",
|
||||||
"description": "Komodo client package",
|
"description": "Komodo client package",
|
||||||
"homepage": "https://komo.do",
|
"homepage": "https://komo.do",
|
||||||
"main": "dist/lib.js",
|
"main": "dist/lib.js",
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
## All fields with a "Default" provided are optional. If they are
|
## All fields with a "Default" provided are optional. If they are
|
||||||
## left out of the file, the "Default" value will be used.
|
## 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`.
|
## 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.
|
## Most fields can also be configured using environment variables.
|
||||||
## Environment variables will override values set in this file.
|
## 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
|
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||||
## - JSON: https://it-tools.tech/toml-to-json
|
## - JSON: https://it-tools.tech/toml-to-json
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
## Most fields can also be configured using cli arguments and environment variables.
|
## 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).
|
## 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
|
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||||
## - JSON: https://it-tools.tech/toml-to-json
|
## - JSON: https://it-tools.tech/toml-to-json
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
## Most fields can also be configured using environment variables.
|
## Most fields can also be configured using environment variables.
|
||||||
## Environment variables will override values set in this file.
|
## 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
|
## - YAML: https://it-tools.tech/toml-to-yaml
|
||||||
## - JSON: https://it-tools.tech/toml-to-json
|
## - JSON: https://it-tools.tech/toml-to-json
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ To run Komodo, you will need Docker. See [the docker install docs](https://docs.
|
|||||||
### Deploy with Docker Compose
|
### Deploy with Docker Compose
|
||||||
|
|
||||||
- [**Using MongoDB**](./mongo.mdx)
|
- [**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)
|
- [**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
|
:::info
|
||||||
**FerretDB v1** users:
|
**FerretDB v1** users:
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ interface SectionProps {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
// otherwise items-start
|
// otherwise items-start
|
||||||
itemsCenterTitleRow?: boolean;
|
itemsCenterTitleRow?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Section = ({
|
export const Section = ({
|
||||||
@@ -180,8 +181,9 @@ export const Section = ({
|
|||||||
actions,
|
actions,
|
||||||
children,
|
children,
|
||||||
itemsCenterTitleRow,
|
itemsCenterTitleRow,
|
||||||
|
className,
|
||||||
}: SectionProps) => (
|
}: SectionProps) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className={cn("flex flex-col gap-4", className)}>
|
||||||
{(title || icon || titleRight || titleOther || actions) && (
|
{(title || icon || titleRight || titleOther || actions) && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -222,6 +224,7 @@ export const NewLayout = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [open, set] = useState(false);
|
const [open, set] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -248,9 +251,14 @@ export const NewLayout = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await onConfirm();
|
try {
|
||||||
setLoading(false);
|
await onConfirm();
|
||||||
set(false);
|
set(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating resource:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!enabled || loading}
|
disabled={!enabled || loading}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export const MonacoEditor = ({
|
|||||||
language={language}
|
language={language}
|
||||||
value={value}
|
value={value}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
defaultPath={filename ? `file:///${filename}` : undefined}
|
defaultPath={defaultPath(filename)}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(v) => onValueChange?.(v ?? "")}
|
onChange={(v) => onValueChange?.(v ?? "")}
|
||||||
onMount={(editor) => setEditor(editor)}
|
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 MIN_DIFF_HEIGHT = 100;
|
||||||
const MAX_DIFF_HEIGHT = 400;
|
const MAX_DIFF_HEIGHT = 400;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
|
|||||||
import { file_contents_empty, sync_no_changes } from "@lib/utils";
|
import { file_contents_empty, sync_no_changes } from "@lib/utils";
|
||||||
import { usePermissions } from "@lib/hooks";
|
import { usePermissions } from "@lib/hooks";
|
||||||
import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react";
|
import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react";
|
||||||
import { useFullResourceSync, usePendingView } from ".";
|
import { useFullResourceSync, useResourceSyncTabsView } from ".";
|
||||||
|
|
||||||
export const RefreshSync = ({ id }: { id: string }) => {
|
export const RefreshSync = ({ id }: { id: string }) => {
|
||||||
const inv = useInvalidate();
|
const inv = useInvalidate();
|
||||||
@@ -34,11 +34,10 @@ export const ExecuteSync = ({ id }: { id: string }) => {
|
|||||||
{ refetchInterval: 5000 }
|
{ refetchInterval: 5000 }
|
||||||
).data?.syncing;
|
).data?.syncing;
|
||||||
const sync = useFullResourceSync(id);
|
const sync = useFullResourceSync(id);
|
||||||
const [_pendingView] = usePendingView();
|
const { view } = useResourceSyncTabsView(sync);
|
||||||
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pendingView === "Commit" ||
|
view !== "Execute" ||
|
||||||
!sync ||
|
!sync ||
|
||||||
sync_no_changes(sync) ||
|
sync_no_changes(sync) ||
|
||||||
!sync.info?.remote_contents
|
!sync.info?.remote_contents
|
||||||
@@ -73,11 +72,12 @@ export const ExecuteSync = ({ id }: { id: string }) => {
|
|||||||
export const CommitSync = ({ id }: { id: string }) => {
|
export const CommitSync = ({ id }: { id: string }) => {
|
||||||
const { mutate, isPending } = useWrite("CommitSync");
|
const { mutate, isPending } = useWrite("CommitSync");
|
||||||
const sync = useFullResourceSync(id);
|
const sync = useFullResourceSync(id);
|
||||||
|
const { view } = useResourceSyncTabsView(sync);
|
||||||
const { canWrite } = usePermissions({ type: "ResourceSync", id });
|
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 =
|
const freshSync =
|
||||||
!sync.config?.files_on_host &&
|
!sync.config?.files_on_host &&
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { atomWithStorage, useLocalStorage, useRead, useUser } from "@lib/hooks";
|
import { atomWithStorage, useRead, useUser } from "@lib/hooks";
|
||||||
import { RequiredResourceComponents } from "@types";
|
import { RequiredResourceComponents } from "@types";
|
||||||
import { Card } from "@ui/card";
|
import { Card } from "@ui/card";
|
||||||
import { Clock, FolderSync } from "lucide-react";
|
import { Clock, FolderSync } from "lucide-react";
|
||||||
@@ -45,23 +45,16 @@ const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
|
|||||||
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
|
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pendingViewAtom = atomWithStorage<"Execute" | "Commit">(
|
type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit";
|
||||||
"sync-view-v1",
|
const syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(
|
||||||
"Execute"
|
"sync-tabs-v4",
|
||||||
|
"Config"
|
||||||
);
|
);
|
||||||
export const usePendingView = () => {
|
|
||||||
return useAtom(pendingViewAtom) as [
|
|
||||||
"Execute" | "Commit",
|
|
||||||
(view: "Execute" | "Commit") => void,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfigInfoPending = ({ id }: { id: string }) => {
|
export const useResourceSyncTabsView = (
|
||||||
const [_view, setView] = useLocalStorage<"Config" | "Info" | "Pending">(
|
sync: Types.ResourceSync | undefined
|
||||||
"sync-tabs-v3",
|
) => {
|
||||||
"Config"
|
const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);
|
||||||
);
|
|
||||||
const sync = useFullResourceSync(id);
|
|
||||||
|
|
||||||
const hideInfo = sync?.config?.files_on_host
|
const hideInfo = sync?.config?.files_on_host
|
||||||
? false
|
? false
|
||||||
@@ -75,13 +68,28 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
|||||||
const view =
|
const view =
|
||||||
_view === "Info" && hideInfo
|
_view === "Info" && hideInfo
|
||||||
? "Config"
|
? "Config"
|
||||||
: _view === "Pending" && !showPending
|
: (_view === "Execute" || _view === "Commit") && !showPending
|
||||||
? sync?.config?.files_on_host ||
|
? sync?.config?.files_on_host ||
|
||||||
sync?.config?.repo ||
|
sync?.config?.repo ||
|
||||||
sync?.config?.linked_repo
|
sync?.config?.linked_repo
|
||||||
? "Info"
|
? "Info"
|
||||||
: "Config"
|
: "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 = (
|
const title = (
|
||||||
<TabsList className="justify-start w-fit">
|
<TabsList className="justify-start w-fit">
|
||||||
@@ -96,12 +104,21 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
|||||||
Info
|
Info
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="Pending"
|
value="Execute"
|
||||||
className="w-[110px]"
|
className="w-[110px]"
|
||||||
disabled={!showPending}
|
disabled={!showPending}
|
||||||
>
|
>
|
||||||
Pending
|
Execute
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
{sync?.config?.managed && (
|
||||||
|
<TabsTrigger
|
||||||
|
value="Commit"
|
||||||
|
className="w-[110px]"
|
||||||
|
disabled={!showPending}
|
||||||
|
>
|
||||||
|
Commit
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@@ -112,7 +129,10 @@ const ConfigInfoPending = ({ id }: { id: string }) => {
|
|||||||
<TabsContent value="Info">
|
<TabsContent value="Info">
|
||||||
<ResourceSyncInfo id={id} titleOther={title} />
|
<ResourceSyncInfo id={id} titleOther={title} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="Pending">
|
<TabsContent value="Execute">
|
||||||
|
<ResourceSyncPending id={id} titleOther={title} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="Commit">
|
||||||
<ResourceSyncPending id={id} titleOther={title} />
|
<ResourceSyncPending id={id} titleOther={title} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -164,7 +184,7 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
GroupActions: () => (
|
GroupActions: () => (
|
||||||
<GroupActions type="ResourceSync" actions={["RunSync"]} />
|
<GroupActions type="ResourceSync" actions={["RunSync", "CommitSync"]} />
|
||||||
),
|
),
|
||||||
|
|
||||||
Table: ({ resources }) => (
|
Table: ({ resources }) => (
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import { cn, sanitizeOnlySpan } from "@lib/utils";
|
|||||||
import { ConfirmButton } from "@components/util";
|
import { ConfirmButton } from "@components/util";
|
||||||
import { SquarePlay } from "lucide-react";
|
import { SquarePlay } from "lucide-react";
|
||||||
import { usePermissions } from "@lib/hooks";
|
import { usePermissions } from "@lib/hooks";
|
||||||
import { useFullResourceSync, usePendingView } from ".";
|
import { useFullResourceSync, useResourceSyncTabsView } from ".";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
|
|
||||||
import { ResourceDiff } from "komodo_client/dist/types";
|
import { ResourceDiff } from "komodo_client/dist/types";
|
||||||
|
|
||||||
export const ResourceSyncPending = ({
|
export const ResourceSyncPending = ({
|
||||||
@@ -24,31 +23,16 @@ export const ResourceSyncPending = ({
|
|||||||
const syncing = useRead("GetResourceSyncActionState", { sync: id }).data
|
const syncing = useRead("GetResourceSyncActionState", { sync: id }).data
|
||||||
?.syncing;
|
?.syncing;
|
||||||
const sync = useFullResourceSync(id);
|
const sync = useFullResourceSync(id);
|
||||||
|
const { view } = useResourceSyncTabsView(sync);
|
||||||
const { canExecute } = usePermissions({ type: "ResourceSync", id });
|
const { canExecute } = usePermissions({ type: "ResourceSync", id });
|
||||||
const [_pendingView, setPendingView] = usePendingView();
|
|
||||||
const pendingView = sync?.config?.managed ? _pendingView : "Execute";
|
|
||||||
const { mutate, isPending } = useExecute("RunSync");
|
const { mutate, isPending } = useExecute("RunSync");
|
||||||
const loading = isPending || syncing;
|
const loading = isPending || syncing;
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section titleOther={titleOther} className="min-h-[500px]">
|
||||||
titleOther={titleOther}
|
<div className="flex items-center gap-4 pl-1 py-2 flex-wrap">
|
||||||
>
|
<div className="text-muted-foreground">{view} Mode:</div>
|
||||||
<div className="flex items-center gap-4 py-2 flex-wrap">
|
|
||||||
{sync?.config?.managed && (
|
|
||||||
<Tabs value={pendingView} onValueChange={setPendingView as any}>
|
|
||||||
<TabsList className="justify-start w-fit">
|
|
||||||
<TabsTrigger value="Execute" className="w-[110px]">
|
|
||||||
Execute
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="Commit" className="w-[110px]">
|
|
||||||
Commit
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
<div className="text-muted-foreground">{pendingView} Mode:</div>
|
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
{pendingView === "Execute" && (
|
{view === "Execute" && (
|
||||||
<>
|
<>
|
||||||
Update resources in the
|
Update resources in the
|
||||||
<div className="font-bold">UI</div>
|
<div className="font-bold">UI</div>
|
||||||
@@ -56,7 +40,7 @@ export const ResourceSyncPending = ({
|
|||||||
<div className="font-bold">file changes.</div>
|
<div className="font-bold">file changes.</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{pendingView === "Commit" && (
|
{view === "Commit" && (
|
||||||
<>
|
<>
|
||||||
Update resources in the
|
Update resources in the
|
||||||
<div className="font-bold">file</div>
|
<div className="font-bold">file</div>
|
||||||
@@ -89,7 +73,7 @@ export const ResourceSyncPending = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{/* Pending Deploy */}
|
{/* Pending Deploy */}
|
||||||
{pendingView === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
|
{view === "Execute" && sync?.info?.pending_deploy?.to_deploy ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -118,13 +102,10 @@ export const ResourceSyncPending = ({
|
|||||||
<div className="flex items-center gap-4 font-mono">
|
<div className="flex items-center gap-4 font-mono">
|
||||||
<div
|
<div
|
||||||
className={text_color_class_by_intention(
|
className={text_color_class_by_intention(
|
||||||
diff_type_intention(
|
diff_type_intention(update.data.type, view === "Commit")
|
||||||
update.data.type,
|
|
||||||
pendingView === "Commit"
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pendingView === "Commit"
|
{view === "Commit"
|
||||||
? reverse_pending_type(update.data.type)
|
? reverse_pending_type(update.data.type)
|
||||||
: update.data.type}{" "}
|
: update.data.type}{" "}
|
||||||
{update.target.type}
|
{update.target.type}
|
||||||
@@ -139,7 +120,7 @@ export const ResourceSyncPending = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{canExecute && pendingView === "Execute" && (
|
{canExecute && view === "Execute" && (
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
title="Execute Change"
|
title="Execute Change"
|
||||||
icon={<SquarePlay className="w-4 h-4" />}
|
icon={<SquarePlay className="w-4 h-4" />}
|
||||||
@@ -168,7 +149,7 @@ export const ResourceSyncPending = ({
|
|||||||
)}
|
)}
|
||||||
{update.data.type === "Update" && (
|
{update.data.type === "Update" && (
|
||||||
<>
|
<>
|
||||||
{pendingView === "Execute" && (
|
{view === "Execute" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={update.data.data.current}
|
original={update.data.data.current}
|
||||||
modified={update.data.data.proposed}
|
modified={update.data.data.proposed}
|
||||||
@@ -176,7 +157,7 @@ export const ResourceSyncPending = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pendingView === "Commit" && (
|
{view === "Commit" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={update.data.data.proposed}
|
original={update.data.data.proposed}
|
||||||
modified={update.data.data.current}
|
modified={update.data.data.current}
|
||||||
@@ -205,13 +186,11 @@ export const ResourceSyncPending = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"font-mono pb-2",
|
"font-mono pb-2",
|
||||||
text_color_class_by_intention(
|
text_color_class_by_intention(
|
||||||
diff_type_intention(data.type, pendingView === "Commit")
|
diff_type_intention(data.type, view === "Commit")
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pendingView === "Commit"
|
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
|
||||||
? reverse_pending_type(data.type)
|
|
||||||
: data.type}{" "}
|
|
||||||
Variable
|
Variable
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -224,7 +203,7 @@ export const ResourceSyncPending = ({
|
|||||||
)}
|
)}
|
||||||
{data.type === "Update" && (
|
{data.type === "Update" && (
|
||||||
<>
|
<>
|
||||||
{pendingView === "Execute" && (
|
{view === "Execute" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={data.data.current}
|
original={data.data.current}
|
||||||
modified={data.data.proposed}
|
modified={data.data.proposed}
|
||||||
@@ -232,7 +211,7 @@ export const ResourceSyncPending = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pendingView === "Commit" && (
|
{view === "Commit" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={data.data.proposed}
|
original={data.data.proposed}
|
||||||
modified={data.data.current}
|
modified={data.data.current}
|
||||||
@@ -261,13 +240,11 @@ export const ResourceSyncPending = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"font-mono pb-2",
|
"font-mono pb-2",
|
||||||
text_color_class_by_intention(
|
text_color_class_by_intention(
|
||||||
diff_type_intention(data.type, pendingView === "Commit")
|
diff_type_intention(data.type, view === "Commit")
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pendingView === "Commit"
|
{view === "Commit" ? reverse_pending_type(data.type) : data.type}{" "}
|
||||||
? reverse_pending_type(data.type)
|
|
||||||
: data.type}{" "}
|
|
||||||
User Group
|
User Group
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -280,7 +257,7 @@ export const ResourceSyncPending = ({
|
|||||||
)}
|
)}
|
||||||
{data.type === "Update" && (
|
{data.type === "Update" && (
|
||||||
<>
|
<>
|
||||||
{pendingView === "Execute" && (
|
{view === "Execute" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={data.data.current}
|
original={data.data.current}
|
||||||
modified={data.data.proposed}
|
modified={data.data.proposed}
|
||||||
@@ -288,7 +265,7 @@ export const ResourceSyncPending = ({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pendingView === "Commit" && (
|
{view === "Commit" && (
|
||||||
<MonacoDiffEditor
|
<MonacoDiffEditor
|
||||||
original={data.data.proposed}
|
original={data.data.proposed}
|
||||||
modified={data.data.current}
|
modified={data.data.current}
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import { useRead } from "@lib/hooks";
|
|||||||
import { Types } from "komodo_client";
|
import { Types } from "komodo_client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useStatsGranularity } from "./hooks";
|
import { useStatsGranularity } from "./hooks";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2, OctagonAlert } from "lucide-react";
|
||||||
import { AxisOptions, Chart } from "react-charts";
|
import { AxisOptions, Chart } from "react-charts";
|
||||||
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
|
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
|
||||||
import { useTheme } from "@ui/theme";
|
import { useTheme } from "@ui/theme";
|
||||||
import { fmt_utc_date } from "@lib/formatting";
|
import { fmt_utc_date } from "@lib/formatting";
|
||||||
|
|
||||||
type StatType = "Cpu" | "Memory" | "Disk" | "Network Ingress" | "Network Egress" | "Load Average";
|
type StatType =
|
||||||
|
| "Cpu"
|
||||||
|
| "Memory"
|
||||||
|
| "Disk"
|
||||||
|
| "Network Ingress"
|
||||||
|
| "Network Egress"
|
||||||
|
| "Load Average";
|
||||||
|
|
||||||
type StatDatapoint = { date: number; value: number };
|
type StatDatapoint = { date: number; value: number };
|
||||||
|
|
||||||
@@ -35,15 +41,15 @@ export const StatChart = ({
|
|||||||
if (type === "Load Average") {
|
if (type === "Load Average") {
|
||||||
const one = records.map((s) => ({
|
const one = records.map((s) => ({
|
||||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||||
value: (s.load_average?.one ?? 0),
|
value: s.load_average?.one ?? 0,
|
||||||
}));
|
}));
|
||||||
const five = records.map((s) => ({
|
const five = records.map((s) => ({
|
||||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||||
value: (s.load_average?.five ?? 0),
|
value: s.load_average?.five ?? 0,
|
||||||
}));
|
}));
|
||||||
const fifteen = records.map((s) => ({
|
const fifteen = records.map((s) => ({
|
||||||
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
||||||
value: (s.load_average?.fifteen ?? 0),
|
value: s.load_average?.fifteen ?? 0,
|
||||||
}));
|
}));
|
||||||
return [
|
return [
|
||||||
{ label: "1m", data: one },
|
{ label: "1m", data: one },
|
||||||
@@ -65,9 +71,13 @@ export const StatChart = ({
|
|||||||
<div className="w-full max-w-full h-full flex items-center justify-center">
|
<div className="w-full max-w-full h-full flex items-center justify-center">
|
||||||
<Loader2 className="w-8 h-8 animate-spin" />
|
<Loader2 className="w-8 h-8 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : seriesData.length > 0 ? (
|
) : (
|
||||||
<InnerStatChart type={type} stats={seriesData.flatMap((s) => s.data)} seriesData={seriesData} />
|
<InnerStatChart
|
||||||
) : null}
|
type={type}
|
||||||
|
stats={seriesData.flatMap((s) => s.data)}
|
||||||
|
seriesData={seriesData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -113,10 +123,12 @@ export const InnerStatChart = ({
|
|||||||
cursor: (_value?: Date) => false,
|
cursor: (_value?: Date) => false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, []);
|
}, [min, max, diff]);
|
||||||
|
|
||||||
// Determine the dynamic scaling for network-related types
|
// 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 maxStatValue = Math.max(...(allValues.length ? allValues : [0]));
|
||||||
|
|
||||||
const { unit, maxUnitValue } = useMemo(() => {
|
const { unit, maxUnitValue } = useMemo(() => {
|
||||||
@@ -133,7 +145,10 @@ export const InnerStatChart = ({
|
|||||||
}
|
}
|
||||||
if (type === "Load Average") {
|
if (type === "Load Average") {
|
||||||
// Leave unitless; set max slightly above observed
|
// 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
|
return { unit: "", maxUnitValue: 100 }; // Default for CPU, memory, disk
|
||||||
}, [type, maxStatValue]);
|
}, [type, maxStatValue]);
|
||||||
@@ -161,6 +176,16 @@ export const InnerStatChart = ({
|
|||||||
],
|
],
|
||||||
[type, maxUnitValue, unit]
|
[type, maxUnitValue, unit]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ((seriesData?.[0]?.data.length ?? 0) < 2) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex gap-4 justify-center items-center">
|
||||||
|
<OctagonAlert className="w-6 h-6" />
|
||||||
|
<h1>Not enough data yet, choose a smaller interval.</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<Chart
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { Section } from "@components/layouts";
|
import { Section } from "@components/layouts";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||||
import { Progress } from "@ui/progress";
|
import { Progress } from "@ui/progress";
|
||||||
import {
|
import { Cpu, Database, Loader2, MemoryStick, Search } from "lucide-react";
|
||||||
Cpu,
|
|
||||||
Database,
|
|
||||||
Loader2,
|
|
||||||
MemoryStick,
|
|
||||||
Search,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useLocalStorage, usePermissions, useRead } from "@lib/hooks";
|
import { useLocalStorage, usePermissions, useRead } from "@lib/hooks";
|
||||||
import { Types } from "komodo_client";
|
import { Types } from "komodo_client";
|
||||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||||
@@ -336,6 +330,11 @@ export const ServerStats = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
|
<StatChart
|
||||||
|
server_id={id}
|
||||||
|
type="Load Average"
|
||||||
|
className="w-full h-[250px]"
|
||||||
|
/>
|
||||||
<StatChart server_id={id} type="Cpu" className="w-full h-[250px]" />
|
<StatChart server_id={id} type="Cpu" className="w-full h-[250px]" />
|
||||||
<StatChart
|
<StatChart
|
||||||
server_id={id}
|
server_id={id}
|
||||||
@@ -347,11 +346,6 @@ export const ServerStats = ({
|
|||||||
type="Disk"
|
type="Disk"
|
||||||
className="w-full h-[250px]"
|
className="w-full h-[250px]"
|
||||||
/>
|
/>
|
||||||
<StatChart
|
|
||||||
server_id={id}
|
|
||||||
type="Load Average"
|
|
||||||
className="w-full h-[250px]"
|
|
||||||
/>
|
|
||||||
<StatChart
|
<StatChart
|
||||||
server_id={id}
|
server_id={id}
|
||||||
type="Network Ingress"
|
type="Network Ingress"
|
||||||
@@ -501,16 +495,28 @@ const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | undefined }) => {
|
const LOAD_AVERAGE = ({
|
||||||
|
id,
|
||||||
|
stats,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
stats: Types.SystemStats | undefined;
|
||||||
|
}) => {
|
||||||
if (!stats?.load_average) return null;
|
if (!stats?.load_average) return null;
|
||||||
const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {};
|
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 textColor = (load: number) => {
|
||||||
const p = pct(load);
|
const p = pct(load);
|
||||||
if (p === undefined) return "text-muted-foreground";
|
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 (
|
return (
|
||||||
@@ -524,15 +530,18 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
|
|||||||
{/* Current Load */}
|
{/* Current Load */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-baseline justify-between">
|
<div className="flex items-baseline justify-between">
|
||||||
<span className={`text-3xl font-bold tabular-nums ${textColor(one)}`}>{one.toFixed(2)}</span>
|
<span
|
||||||
|
className={`text-3xl font-bold tabular-nums ${textColor(one)}`}
|
||||||
|
>
|
||||||
|
{one.toFixed(2)}
|
||||||
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{cores && cores > 0 ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores` : "N/A"}
|
{cores && cores > 0
|
||||||
|
? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`
|
||||||
|
: "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress value={pct(one) ?? 0} className="h-2" />
|
||||||
value={pct(one) ?? 0}
|
|
||||||
className="h-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time Intervals */}
|
{/* Time Intervals */}
|
||||||
@@ -546,14 +555,13 @@ const LOAD_AVERAGE = ({ id, stats }: { id: string; stats: Types.SystemStats | un
|
|||||||
<div className="space-y-1" key={label as string}>
|
<div className="space-y-1" key={label as string}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{label}</span>
|
<span className="text-muted-foreground">{label}</span>
|
||||||
<span className={`font-medium tabular-nums ${textColor(value as number)}`}>
|
<span
|
||||||
|
className={`font-medium tabular-nums ${textColor(value as number)}`}
|
||||||
|
>
|
||||||
{(value as number).toFixed(2)}
|
{(value as number).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress value={pct(value as number) ?? 0} className="h-1" />
|
||||||
value={(pct(value as number) ?? 0)}
|
|
||||||
className="h-1"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -805,3 +805,24 @@ export const useContainerPortsMap = (ports: Types.Port[]) => {
|
|||||||
return map;
|
return map;
|
||||||
}, [ports]);
|
}, [ports]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that debounces a value, delaying its update until after
|
||||||
|
* a specified period of inactivity. This is useful for performance optimization
|
||||||
|
* in scenarios like search inputs, form validation, or API calls.
|
||||||
|
*/
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@@ -6,33 +6,66 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
} from "@components/util";
|
} from "@components/util";
|
||||||
import { container_state_intention } from "@lib/color";
|
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 { DataTable, SortableHeader } from "@ui/data-table";
|
||||||
import { Input } from "@ui/input";
|
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";
|
import { Fragment, useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
export default function ContainersPage() {
|
export default function ContainersPage() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const searchSplit = search
|
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const debouncedSearch = useDebounce(search, 300);
|
||||||
|
|
||||||
|
const searchSplit = debouncedSearch
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((term) => term);
|
.filter((term) => term);
|
||||||
|
|
||||||
const servers = useRead("ListServers", {}).data;
|
const servers = useRead("ListServers", {}).data;
|
||||||
|
const serverOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
servers?.map((server) => ({
|
||||||
|
label: server.name,
|
||||||
|
value: server.id,
|
||||||
|
})) || [],
|
||||||
|
[servers]
|
||||||
|
);
|
||||||
|
|
||||||
const serverName = useCallback(
|
const serverName = useCallback(
|
||||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||||
[servers]
|
[servers]
|
||||||
);
|
);
|
||||||
|
|
||||||
const _containers = useRead("ListAllDockerContainers", {}).data;
|
const _containers = useRead("ListAllDockerContainers", {}).data;
|
||||||
|
|
||||||
const containers = useMemo(
|
const containers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
_containers?.filter((c) => {
|
_containers?.filter((c) => {
|
||||||
if (searchSplit.length === 0) return true;
|
if (searchSplit.length > 0) {
|
||||||
const lower = c.name.toLowerCase();
|
const lower = c.name.toLowerCase();
|
||||||
return searchSplit.every((search) => lower.includes(search));
|
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 (
|
return (
|
||||||
<Page
|
<Page
|
||||||
title="Containers"
|
title="Containers"
|
||||||
@@ -44,18 +77,45 @@ export default function ContainersPage() {
|
|||||||
icon={<Box className="w-8 h-8" />}
|
icon={<Box className="w-8 h-8" />}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div></div>
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{/* Server Filter Multi-Select */}
|
||||||
|
<div className="w-[280px]">
|
||||||
|
<MultiSelect
|
||||||
|
options={serverOptions}
|
||||||
|
value={selectedServers}
|
||||||
|
onChange={setSelectedServers}
|
||||||
|
placeholder="Filter by server..."
|
||||||
|
className="w-full h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reset Server Filter Button */}
|
||||||
|
{selectedServers.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearAllServers}
|
||||||
|
className="h-10 px-3"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Input */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="search..."
|
placeholder="search..."
|
||||||
className="pl-8 w-[200px] lg:w-[300px]"
|
className="pl-8 w-[200px] lg:w-[300px] py-0 h-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={containers ?? []}
|
data={containers ?? []}
|
||||||
tableKey="containers-page-v1"
|
tableKey="containers-page-v1"
|
||||||
@@ -189,29 +249,6 @@ export default function ContainersPage() {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// accessorKey: "volumes.0",
|
|
||||||
// minSize: 300,
|
|
||||||
// header: ({ column }) => (
|
|
||||||
// <SortableHeader column={column} title="Volumes" />
|
|
||||||
// ),
|
|
||||||
// cell: ({ row }) => (
|
|
||||||
// <div className="flex items-center gap-x-2 flex-wrap">
|
|
||||||
// {row.original.volumes.map((volume, i) => (
|
|
||||||
// <Fragment key={volume}>
|
|
||||||
// <DockerResourceLink
|
|
||||||
// type="volume"
|
|
||||||
// server_id={row.original.server_id!}
|
|
||||||
// name={volume}
|
|
||||||
// />
|
|
||||||
// {i !== row.original.volumes.length - 1 && (
|
|
||||||
// <div className="text-muted-foreground">|</div>
|
|
||||||
// )}
|
|
||||||
// </Fragment>
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
useLoginOptions,
|
useLoginOptions,
|
||||||
useUserInvalidate,
|
useUserInvalidate,
|
||||||
} from "@lib/hooks";
|
} from "@lib/hooks";
|
||||||
import { type FormEvent } from "react";
|
import { useRef } from "react";
|
||||||
import { ThemeToggle } from "@ui/theme";
|
import { ThemeToggle } from "@ui/theme";
|
||||||
import { KOMODO_BASE_URL } from "@main";
|
import { KOMODO_BASE_URL } from "@main";
|
||||||
import { KeyRound, X } from "lucide-react";
|
import { KeyRound, X } from "lucide-react";
|
||||||
@@ -40,6 +40,7 @@ export default function Login() {
|
|||||||
const options = useLoginOptions().data;
|
const options = useLoginOptions().data;
|
||||||
const userInvalidate = useUserInvalidate();
|
const userInvalidate = useUserInvalidate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
// If signing in another user, need to redirect away from /login manually
|
// If signing in another user, need to redirect away from /login manually
|
||||||
const maybeNavigate = location.pathname.startsWith("/login")
|
const maybeNavigate = location.pathname.startsWith("/login")
|
||||||
@@ -63,13 +64,13 @@ export default function Login() {
|
|||||||
const message = e?.response?.data?.error as string | undefined;
|
const message = e?.response?.data?.error as string | undefined;
|
||||||
if (message) {
|
if (message) {
|
||||||
toast({
|
toast({
|
||||||
title: `Failed to login user. '${message}'`,
|
title: `Failed to sign up user. '${message}'`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Failed to login user. See console log for details.",
|
title: "Failed to sign up user. See console log for details.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -97,17 +98,24 @@ export default function Login() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
const getFormCredentials = () => {
|
||||||
e.preventDefault();
|
if (!formRef.current) return undefined;
|
||||||
const fd = new FormData(e.currentTarget);
|
const fd = new FormData(formRef.current);
|
||||||
const username = String(fd.get("username") ?? "");
|
const username = String(fd.get("username") ?? "");
|
||||||
const password = String(fd.get("password") ?? "");
|
const password = String(fd.get("password") ?? "");
|
||||||
const action = String(fd.get("action") ?? "login");
|
return { username, password };
|
||||||
if (action === "signup") {
|
};
|
||||||
signup({ username, password });
|
|
||||||
} else {
|
const handleLogin = () => {
|
||||||
login({ username, password });
|
const creds = getFormCredentials();
|
||||||
}
|
if (!creds) return;
|
||||||
|
login(creds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignUp = () => {
|
||||||
|
const creds = getFormCredentials();
|
||||||
|
if (!creds) return;
|
||||||
|
signup(creds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const no_auth_configured =
|
const no_auth_configured =
|
||||||
@@ -176,7 +184,7 @@ export default function Login() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
{options?.local && (
|
{options?.local && (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
ref={formRef}
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
>
|
>
|
||||||
<CardContent className="flex flex-col justify-center w-full gap-4">
|
<CardContent className="flex flex-col justify-center w-full gap-4">
|
||||||
@@ -204,9 +212,9 @@ export default function Login() {
|
|||||||
{show_sign_up && (
|
{show_sign_up && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="submit"
|
type="button"
|
||||||
name="action"
|
|
||||||
value="signup"
|
value="signup"
|
||||||
|
onClick={handleSignUp}
|
||||||
disabled={signupPending}
|
disabled={signupPending}
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
@@ -214,9 +222,9 @@ export default function Login() {
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
type="submit"
|
type="button"
|
||||||
name="action"
|
|
||||||
value="login"
|
value="login"
|
||||||
|
onClick={handleLogin}
|
||||||
disabled={loginPending}
|
disabled={loginPending}
|
||||||
>
|
>
|
||||||
Log In
|
Log In
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { LOGIN_TOKENS, useAuth, useUser } from "@lib/hooks";
|
|||||||
import UpdatePage from "@pages/update";
|
import UpdatePage from "@pages/update";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
// Lazy import pages
|
// Lazy import pages
|
||||||
const Resources = lazy(() => import("@pages/resources"));
|
const Resources = lazy(() => import("@pages/resources"));
|
||||||
@@ -63,8 +63,6 @@ const useExchangeToken = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const { data: user, error } = useUser();
|
|
||||||
|
|
||||||
// Handle exchange token loop to avoid showing login flash
|
// Handle exchange token loop to avoid showing login flash
|
||||||
const exchangeTokenPending = useExchangeToken();
|
const exchangeTokenPending = useExchangeToken();
|
||||||
if (exchangeTokenPending) {
|
if (exchangeTokenPending) {
|
||||||
@@ -75,13 +73,6 @@ export const Router = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only how login once error indicating logged out state actually recieved
|
|
||||||
if (error) return <Login />;
|
|
||||||
// Don't display anything if !error and !user. This is loading state.
|
|
||||||
if (!user) return null;
|
|
||||||
// Don't try displaying pages if user disabled, will fail to load with many errors.
|
|
||||||
if (!user.enabled) return <UserDisabled />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
@@ -93,34 +84,36 @@ export const Router = () => {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="/" element={<Layout />}>
|
<Route element={<RequireAuth />}>
|
||||||
<Route path="" element={<Home />} />
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="" element={<Home />} />
|
||||||
<Route path="tree" element={<Tree />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
<Route path="containers" element={<ContainersPage />} />
|
<Route path="tree" element={<Tree />} />
|
||||||
<Route path="resources" element={<AllResources />} />
|
<Route path="containers" element={<ContainersPage />} />
|
||||||
<Route path="schedules" element={<SchedulesPage />} />
|
<Route path="resources" element={<AllResources />} />
|
||||||
<Route path="alerts" element={<AlertsPage />} />
|
<Route path="schedules" element={<SchedulesPage />} />
|
||||||
<Route path="user-groups/:id" element={<UserGroupPage />} />
|
<Route path="alerts" element={<AlertsPage />} />
|
||||||
<Route path="users/:id" element={<UserPage />} />
|
<Route path="user-groups/:id" element={<UserGroupPage />} />
|
||||||
<Route path="updates">
|
<Route path="users/:id" element={<UserPage />} />
|
||||||
<Route path="" element={<UpdatesPage />} />
|
<Route path="updates">
|
||||||
<Route path=":id" element={<UpdatePage />} />
|
<Route path="" element={<UpdatesPage />} />
|
||||||
</Route>
|
<Route path=":id" element={<UpdatePage />} />
|
||||||
<Route path=":type">
|
</Route>
|
||||||
<Route path="" element={<Resources />} />
|
<Route path=":type">
|
||||||
<Route path=":id" element={<Resource />} />
|
<Route path="" element={<Resources />} />
|
||||||
<Route
|
<Route path=":id" element={<Resource />} />
|
||||||
path=":id/service/:service"
|
<Route
|
||||||
element={<StackServicePage />}
|
path=":id/service/:service"
|
||||||
/>
|
element={<StackServicePage />}
|
||||||
<Route
|
/>
|
||||||
path=":id/container/:container"
|
<Route
|
||||||
element={<ContainerPage />}
|
path=":id/container/:container"
|
||||||
/>
|
element={<ContainerPage />}
|
||||||
<Route path=":id/network/:network" element={<NetworkPage />} />
|
/>
|
||||||
<Route path=":id/image/:image" element={<ImagePage />} />
|
<Route path=":id/network/:network" element={<NetworkPage />} />
|
||||||
<Route path=":id/volume/:volume" element={<VolumePage />} />
|
<Route path=":id/image/:image" element={<ImagePage />} />
|
||||||
|
<Route path=":id/volume/:volume" element={<VolumePage />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -131,4 +124,29 @@ export const Router = () => {
|
|||||||
// return <RouterProvider router={ROUTER} />;
|
// return <RouterProvider router={ROUTER} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RequireAuth = () => {
|
||||||
|
const { data: user, error } = useUser();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (location.pathname === "/") {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
const backto = encodeURIComponent(location.pathname + location.search);
|
||||||
|
return <Navigate to={`/login?backto=${backto}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen flex justify-center items-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.enabled) return <UserDisabled />;
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
161
frontend/src/ui/multi-select.tsx
Normal file
161
frontend/src/ui/multi-select.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Check, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Badge } from "@/ui/badge";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/ui/command";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover";
|
||||||
|
import { Skeleton } from "@/ui/skeleton";
|
||||||
|
|
||||||
|
interface MultiSelectProps {
|
||||||
|
options?: { label: string; value: string }[];
|
||||||
|
value: string[];
|
||||||
|
onChange: (selected: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MultiSelect({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Select items...",
|
||||||
|
className,
|
||||||
|
isLoading = false,
|
||||||
|
disabled = false,
|
||||||
|
}: MultiSelectProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleUnselect = (item: string) => {
|
||||||
|
onChange(value.filter((i) => i !== item));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (item: string) => {
|
||||||
|
if (value.includes(item)) {
|
||||||
|
handleUnselect(item);
|
||||||
|
} else {
|
||||||
|
onChange([...value, item]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full", className)}>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full transition-all items-center justify-between rounded-md border border-input bg-background text-sm",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground"
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between flex-1 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex gap-1 flex-1 py-2 px-3 overflow-x-auto"
|
||||||
|
style={{
|
||||||
|
scrollbarWidth: "thin",
|
||||||
|
scrollbarColor: "hsl(var(--border)) transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value.length === 0 ? (
|
||||||
|
<span className="text-muted-foreground truncate">
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
value.map((item) => {
|
||||||
|
const option = options?.find((opt) => opt.value === item);
|
||||||
|
return (
|
||||||
|
<Badge key={item} variant="default" className="text-xs">
|
||||||
|
{option?.label}
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="ml-1 hover:bg-destructive transition-all hover:text-destructive-foreground rounded-full p-0.5"
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
e.key === "Enter" && handleUnselect(item)
|
||||||
|
}
|
||||||
|
onClick={() => handleUnselect(item)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<hr className="border-l border-border bg-red-300 h-6 mx-0.5 my-auto" />
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen((prev) => !prev);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
"p-1 mx-1.5 my-auto h-full outline-none",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
"hover:bg-accent/50 rounded-sm cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput autoFocus={false} placeholder="Search items..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="p-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-2">
|
||||||
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={index}
|
||||||
|
className="h-4 w-full mb-1 last:mb-0"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-sm py-4 text-muted-foreground">
|
||||||
|
No items found.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options?.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
onSelect={() => handleSelect(option.value)}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
value.includes(option.value)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {MultiSelect}
|
||||||
15
frontend/src/ui/skeleton.tsx
Normal file
15
frontend/src/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
@@ -55,8 +55,9 @@ pub async fn copy(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !buffer.is_empty() {
|
if !buffer.is_empty() {
|
||||||
bulk_update_retry_too_big(&target_db, &collection, &buffer, true).await.context("Failed to flush documents")?;
|
bulk_update_retry_too_big(&target_db, &collection, &buffer, true)
|
||||||
|
.await
|
||||||
|
.context("Failed to flush documents")?;
|
||||||
}
|
}
|
||||||
anyhow::Ok(count)
|
anyhow::Ok(count)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user