forked from github-starred/komodo
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
addb35aa69 | ||
|
|
16bf78f9ad | ||
|
|
3ed4f91d82 | ||
|
|
653fb894a2 | ||
|
|
0f9798a5f2 | ||
|
|
6776a20ec5 | ||
|
|
fb21e8586f | ||
|
|
8b2c4d604a | ||
|
|
c0b010d5ce | ||
|
|
97de34a088 | ||
|
|
6c0b76a270 | ||
|
|
eebd44ab9b | ||
|
|
783250c5ce | ||
|
|
70ff93050f | ||
|
|
1cc1813185 | ||
|
|
b4f9b87d06 | ||
|
|
26b09a767e | ||
|
|
bba6c4d8b6 | ||
|
|
ea440235c4 | ||
|
|
f9949bf988 | ||
|
|
b978db012e | ||
|
|
bc2fbdd657 | ||
|
|
a5571bcf4d | ||
|
|
683a528dd9 | ||
|
|
4a283b6052 | ||
|
|
37224ee1ad | ||
|
|
5e7445b10d | ||
|
|
1829a7da34 | ||
|
|
4a1a653bd9 | ||
|
|
840c1a87d0 | ||
|
|
c90368e2af | ||
|
|
1f9d74fadb | ||
|
|
5b261058fe | ||
|
|
cf6632ba02 | ||
|
|
c7124bd63c | ||
|
|
ba19e45607 | ||
|
|
20282ffcbb | ||
|
|
cb8ad90838 | ||
|
|
caac3fdcc4 | ||
|
|
44da282060 | ||
|
|
a2e27b09fc | ||
|
|
c1b1f397fd | ||
|
|
1d0f239594 | ||
|
|
549bc78799 | ||
|
|
9eb9b57e36 |
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -32,7 +32,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alert_logger"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.5",
|
||||
@@ -115,17 +115,6 @@ version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
@@ -2017,7 +2006,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -2094,7 +2083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2222,7 +2211,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2231,6 +2220,7 @@ dependencies = [
|
||||
"monitor_client",
|
||||
"partial_derive2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.26.2",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -2240,7 +2230,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2272,10 +2262,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_core"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"async_timing_util",
|
||||
"aws-config",
|
||||
@@ -2319,7 +2308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2350,7 +2339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_sync"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2626,18 +2615,18 @@ checksum = "ffa94c2e5674923c67d7f3dfce1279507b191e10eb064881b46ed3e1256e5ca6"
|
||||
|
||||
[[package]]
|
||||
name = "partial_derive2"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57fd88f09814d56f73ea68256ebdd2cdd75a4a3b48d41c63bbc27c46c6733588"
|
||||
checksum = "8a7b915bd76bc306b7ae6f1b5d99b0434e498ab979ecbd5df119db8a00dab972"
|
||||
dependencies = [
|
||||
"partial_derive2_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "partial_derive2_derive"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdc7cb36e2cd4e6e7a53e3d35fd3321e1bd83382e0207901bff7ffb779cf7619"
|
||||
checksum = "ddaac49c0e65bcb207999d2514b10b43d5f2ec2d0fb47b9d875c9a10536a294e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2661,7 +2650,7 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -3660,7 +3649,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenv",
|
||||
@@ -4216,7 +4205,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"logger",
|
||||
|
||||
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -23,7 +23,7 @@ derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
termination_signal = "0.1.3"
|
||||
async_timing_util = "0.1.14"
|
||||
partial_derive2 = "0.4.1"
|
||||
partial_derive2 = "0.4.2"
|
||||
derive_variants = "0.1.3"
|
||||
mongo_indexed = "0.2.2"
|
||||
resolver_api = "0.1.9"
|
||||
@@ -38,7 +38,6 @@ tokio-util = "0.7.11"
|
||||
futures = "0.3.30"
|
||||
futures-util = "0.3.30"
|
||||
async-trait = "0.1.80"
|
||||
async-recursion = "1.1.1"
|
||||
|
||||
# SERVER
|
||||
axum = { version = "0.7.5", features = ["ws", "json"] }
|
||||
@@ -90,5 +89,5 @@ aws-sdk-ec2 = "1.40.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.0"
|
||||
colored = "2.1.0"
|
||||
typeshare = "1.0.3"
|
||||
colored = "2.1.0"
|
||||
|
||||
@@ -18,6 +18,10 @@ monitor_client.workspace = true
|
||||
# mogh
|
||||
partial_derive2.workspace = true
|
||||
# external
|
||||
tracing-subscriber.workspace = true
|
||||
serde_json.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
colored.workspace = true
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -25,6 +29,3 @@ serde.workspace = true
|
||||
strum.workspace = true
|
||||
toml.workspace = true
|
||||
clap.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -1,8 +1,19 @@
|
||||
# monitor CLI
|
||||
# Monitor CLI
|
||||
|
||||
Monitor CLI is a command line tool to sync monitor resources and execute file defined procedures.
|
||||
|
||||
## Examples
|
||||
## Usage
|
||||
|
||||
Configure a file `~/.config/monitor/creds.toml` file with contents:
|
||||
```toml
|
||||
url = "https://your.monitor.address"
|
||||
key = "YOUR-API-KEY"
|
||||
secret = "YOUR-API-SECRET"
|
||||
```
|
||||
|
||||
Note. You can specify a different creds file by using `--creds ./other/path.toml`.
|
||||
|
||||
With your creds in place, you can run syncs:
|
||||
|
||||
```sh
|
||||
## Sync resources in a single file
|
||||
@@ -15,6 +26,8 @@ monitor sync ./resources
|
||||
monitor sync
|
||||
```
|
||||
|
||||
And executions:
|
||||
|
||||
```sh
|
||||
## Execute a TOML defined procedure
|
||||
monitor exec ./execution/execution.toml
|
||||
|
||||
@@ -24,6 +24,40 @@ docker_account = "mbecker2020"
|
||||
build_path = "."
|
||||
dockerfile_path = "bin/core/Dockerfile"
|
||||
|
||||
[[build]]
|
||||
name = "monitor_frontend"
|
||||
description = "standalone hosted frontend for monitor.mogh.tech"
|
||||
tags = ["monitor", "frontend"]
|
||||
|
||||
[build.config]
|
||||
builder_id = "mogh-builder"
|
||||
repo = "mbecker20/monitor"
|
||||
branch = "main"
|
||||
docker_account = "mbecker2020"
|
||||
build_path = "."
|
||||
dockerfile_path = "frontend/Dockerfile"
|
||||
|
||||
[[build.config.build_args]]
|
||||
variable = "VITE_MONITOR_HOST"
|
||||
value = "https://monitor.api.mogh.tech"
|
||||
|
||||
[[build]]
|
||||
name = "monitor_frontend_dev"
|
||||
description = "standalone hosted frontend for monitor-dev.mogh.tech"
|
||||
tags = ["monitor", "frontend"]
|
||||
|
||||
[build.config]
|
||||
builder_id = "mogh-builder"
|
||||
repo = "mbecker20/monitor"
|
||||
branch = "main"
|
||||
docker_account = "mbecker2020"
|
||||
build_path = "."
|
||||
dockerfile_path = "frontend/Dockerfile"
|
||||
|
||||
[[build.config.build_args]]
|
||||
variable = "VITE_MONITOR_HOST"
|
||||
value = "https://monitor-dev.api.mogh.tech"
|
||||
|
||||
## BUILDER
|
||||
|
||||
[[builder]]
|
||||
|
||||
@@ -173,9 +173,41 @@ local = "/home/ubuntu/.config/monitor/dev.core.config.toml"
|
||||
container = "/config/config.toml"
|
||||
|
||||
[[deployment.config.volumes]]
|
||||
local = "/etc/monitor/repos/monitor-dev-frontend/frontend/dist"
|
||||
local = "/data/repos/monitor-dev-frontend/frontend/dist"
|
||||
container = "/frontend"
|
||||
|
||||
[[deployment.config.labels]]
|
||||
variable = "vector"
|
||||
value = "rust"
|
||||
value = "rust"
|
||||
|
||||
## MONITOR FRONTEND
|
||||
[[deployment]]
|
||||
name = "monitor-frontend"
|
||||
description = ""
|
||||
tags = ["monitor", "frontend"]
|
||||
|
||||
[deployment.config]
|
||||
server_id = "monitor-01"
|
||||
redeploy_on_build = true
|
||||
network = "host"
|
||||
restart = "unless-stopped"
|
||||
image.type = "Build"
|
||||
image.params.build = "monitor_frontend"
|
||||
|
||||
## MONITOR DEV FRONTEND
|
||||
[[deployment]]
|
||||
name = "monitor-dev-frontend"
|
||||
description = ""
|
||||
tags = ["monitor", "dev", "frontend"]
|
||||
|
||||
[deployment.config]
|
||||
server_id = "monitor-01"
|
||||
redeploy_on_build = true
|
||||
network = "host"
|
||||
restart = "unless-stopped"
|
||||
image.type = "Build"
|
||||
image.params.build = "monitor_frontend_dev"
|
||||
|
||||
[[deployment.config.environment]]
|
||||
variable = "PORT"
|
||||
value = "4175"
|
||||
@@ -1,7 +1,26 @@
|
||||
# [[repo]]
|
||||
# name = "monitor-dev-frontend"
|
||||
# description = "Used as frontend for monitor-core-dev"
|
||||
# tags = ["monitor", "dev"]
|
||||
|
||||
# [repo.config]
|
||||
# server_id = "monitor-01"
|
||||
# repo = "mbecker20/monitor"
|
||||
# branch = "main"
|
||||
# github_account = ""
|
||||
|
||||
# [repo.config.on_clone]
|
||||
# path = ""
|
||||
# command = ""
|
||||
|
||||
# [repo.config.on_pull]
|
||||
# path = "frontend"
|
||||
# command = "sh on_pull.sh"
|
||||
|
||||
[[repo]]
|
||||
name = "monitor-dev-frontend"
|
||||
description = "Used as frontend for monitor-core-dev"
|
||||
tags = ["monitor", "dev"]
|
||||
name = "monitor-periphery"
|
||||
description = ""
|
||||
tags = ["monitor"]
|
||||
|
||||
[repo.config]
|
||||
server_id = "monitor-01"
|
||||
@@ -14,5 +33,5 @@ path = ""
|
||||
command = ""
|
||||
|
||||
[repo.config.on_pull]
|
||||
path = "frontend"
|
||||
command = "sh on_pull.sh"
|
||||
path = "."
|
||||
command = "/root/.cargo/bin/cargo build -p monitor_periphery --release"
|
||||
|
||||
@@ -25,13 +25,19 @@ struct CliArgs {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
/// The path to a creds file.
|
||||
#[arg(long, default_value_t = String::from("./creds.toml"))]
|
||||
#[arg(long, default_value_t = default_creds())]
|
||||
creds: String,
|
||||
/// Log less (just resource names).
|
||||
#[arg(long, default_value_t = false)]
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
fn default_creds() -> String {
|
||||
let home = std::env::var("HOME")
|
||||
.expect("no HOME env var. cannot get default config path.");
|
||||
format!("{home}/.config/monitor/creds.toml")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
enum Command {
|
||||
/// Runs syncs on resource files
|
||||
@@ -94,7 +100,11 @@ fn parse_toml_file<T: DeserializeOwned>(
|
||||
}
|
||||
|
||||
fn wait_for_enter(press_enter_to: &str) -> anyhow::Result<()> {
|
||||
println!("\nPress {} to {}\n", "ENTER".green(), press_enter_to.bold());
|
||||
println!(
|
||||
"\nPress {} to {}\n",
|
||||
"ENTER".green(),
|
||||
press_enter_to.bold()
|
||||
);
|
||||
let buffer = &mut [0u8];
|
||||
std::io::stdin()
|
||||
.read_exact(buffer)
|
||||
|
||||
@@ -10,6 +10,7 @@ use monitor_client::{
|
||||
},
|
||||
};
|
||||
use partial_derive2::{Diff, FieldDiff, MaybeNone, PartialDiff};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{cli_args, maps::id_to_tag, monitor_client};
|
||||
|
||||
@@ -43,6 +44,7 @@ pub trait ResourceSync {
|
||||
+ Clone
|
||||
+ Send
|
||||
+ From<Self::ConfigDiff>
|
||||
+ Serialize
|
||||
+ 'static;
|
||||
type ConfigDiff: Diff + MaybeNone;
|
||||
type ListItemInfo: 'static;
|
||||
@@ -171,10 +173,16 @@ pub trait ResourceSync {
|
||||
None => {
|
||||
if !quiet {
|
||||
println!(
|
||||
"{}: {}: {}: {resource:#?}",
|
||||
"\n{}: {}: {}\n{}: {}\n{}: {:?}\n{}: {}",
|
||||
"CREATE".green(),
|
||||
Self::display(),
|
||||
resource.name.bold().green(),
|
||||
"description".dimmed(),
|
||||
resource.description,
|
||||
"tags".dimmed(),
|
||||
resource.tags,
|
||||
"config".dimmed(),
|
||||
serde_json::to_string_pretty(&resource.config)?
|
||||
)
|
||||
}
|
||||
to_create.push(resource);
|
||||
|
||||
@@ -28,31 +28,30 @@ mungos.workspace = true
|
||||
serror.workspace = true
|
||||
slack.workspace = true
|
||||
# external
|
||||
tokio.workspace = true
|
||||
urlencoding.workspace = true
|
||||
async-trait.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
typeshare.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
futures.workspace = true
|
||||
anyhow.workspace = true
|
||||
dotenv.workspace = true
|
||||
bcrypt.workspace = true
|
||||
tokio.workspace = true
|
||||
tower.workspace = true
|
||||
serde.workspace = true
|
||||
axum.workspace = true
|
||||
toml.workspace = true
|
||||
uuid.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
dotenv.workspace = true
|
||||
envy.workspace = true
|
||||
reqwest.workspace = true
|
||||
urlencoding.workspace = true
|
||||
rand.workspace = true
|
||||
jwt.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
bcrypt.workspace = true
|
||||
jwt.workspace = true
|
||||
hex.workspace = true
|
||||
async-trait.workspace = true
|
||||
async-recursion.workspace = true
|
||||
futures.workspace = true
|
||||
aws-config.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
typeshare.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Core
|
||||
FROM rust:1.77.2-bullseye as core-builder
|
||||
FROM rust:1.78.0-bullseye as core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p monitor_core --release
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
@@ -11,7 +11,7 @@ use monitor_client::{
|
||||
all_logs_success,
|
||||
build::Build,
|
||||
builder::{AwsBuilderConfig, Builder, BuilderConfig},
|
||||
deployment::DockerContainerState,
|
||||
deployment::DeploymentState,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
@@ -22,8 +22,9 @@ use monitor_client::{
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
mongodb::bson::{doc, to_bson, to_document},
|
||||
};
|
||||
use periphery_client::{
|
||||
api::{self, GetVersionResponse},
|
||||
@@ -48,7 +49,7 @@ use crate::{
|
||||
query::get_deployment_state,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
resource,
|
||||
resource::{self, refresh_build_state_cache},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
@@ -135,9 +136,7 @@ impl Resolve<RunBuild, User> for State {
|
||||
"get builder",
|
||||
serialize_error_pretty(&e),
|
||||
));
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
return handle_early_return(update).await;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,9 +161,7 @@ impl Resolve<RunBuild, User> for State {
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update)
|
||||
return handle_early_return(update).await
|
||||
},
|
||||
};
|
||||
|
||||
@@ -198,9 +195,7 @@ impl Resolve<RunBuild, User> for State {
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update)
|
||||
return handle_early_return(update).await
|
||||
},
|
||||
};
|
||||
|
||||
@@ -218,12 +213,13 @@ impl Resolve<RunBuild, User> for State {
|
||||
|
||||
update.finalize();
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
if update.success {
|
||||
let _ = db_client()
|
||||
.await
|
||||
let _ = db
|
||||
.builds
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&build.id)? },
|
||||
doc! { "name": &build.name },
|
||||
doc! {
|
||||
"$set": {
|
||||
"config.version": to_bson(&build.config.version)
|
||||
@@ -241,19 +237,57 @@ impl Resolve<RunBuild, User> for State {
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
|
||||
info!("builder instance cleaned up");
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db.updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_build_state_cache().await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if update.success {
|
||||
handle_post_build_redeploy(&build.id).await;
|
||||
info!("post build redeploy handled");
|
||||
// don't hold response up for user
|
||||
tokio::spawn(async move {
|
||||
handle_post_build_redeploy(&build.id).await;
|
||||
info!("post build redeploy handled");
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_early_return(
|
||||
mut update: Update,
|
||||
) -> anyhow::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_build_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CancelBuild, User> for State {
|
||||
#[instrument(name = "CancelBuild", skip(self, user))]
|
||||
@@ -352,11 +386,8 @@ async fn get_aws_builder(
|
||||
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
|
||||
let start_create_ts = monitor_timestamp();
|
||||
|
||||
let instance_name = format!(
|
||||
"BUILDER-{}-v{}",
|
||||
build.name,
|
||||
build.config.version
|
||||
);
|
||||
let instance_name =
|
||||
format!("BUILDER-{}-v{}", build.name, build.config.version);
|
||||
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
|
||||
&instance_name,
|
||||
AwsServerTemplateConfig::from_builder_config(&config),
|
||||
@@ -478,7 +509,7 @@ async fn handle_post_build_redeploy(build_id: &str) {
|
||||
.map(|deployment| async move {
|
||||
let state =
|
||||
get_deployment_state(&deployment).await.unwrap_or_default();
|
||||
if state == DockerContainerState::Running {
|
||||
if state == DeploymentState::Running {
|
||||
let res = State
|
||||
.resolve(
|
||||
Deploy {
|
||||
|
||||
@@ -8,7 +8,7 @@ use monitor_client::{
|
||||
deployment::{Deployment, DeploymentImage},
|
||||
get_image_name, monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::ServerStatus,
|
||||
server::ServerState,
|
||||
update::{Log, ResourceTarget, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation, Version,
|
||||
@@ -68,7 +68,7 @@ impl Resolve<Deploy, User> for State {
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
if status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
@@ -168,7 +168,7 @@ impl Resolve<StartContainer, User> for State {
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
if status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
@@ -247,7 +247,7 @@ impl Resolve<StopContainer, User> for State {
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
if status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
@@ -296,7 +296,7 @@ impl Resolve<StopAllContainers, User> for State {
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (server, status) = get_server_with_status(&server).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
if status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
@@ -406,7 +406,7 @@ impl Resolve<RemoveContainer, User> for State {
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
if status != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
@@ -14,7 +12,10 @@ use monitor_client::{
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, to_document},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
@@ -25,7 +26,7 @@ use crate::{
|
||||
periphery_client,
|
||||
update::{add_update, update_update},
|
||||
},
|
||||
resource,
|
||||
resource::{self, refresh_repo_state_cache},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
@@ -98,24 +99,10 @@ impl Resolve<CloneRepo, User> for State {
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
let res = db_client().await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&repo.id)? },
|
||||
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
|
||||
repo.id
|
||||
);
|
||||
}
|
||||
update_last_pulled(&repo.name).await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
handle_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +155,9 @@ impl Resolve<PullRepo, User> for State {
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::PullRepo {
|
||||
name: repo.name,
|
||||
name: repo.name.clone(),
|
||||
branch: optional_string(&repo.config.branch),
|
||||
commit: optional_string(&repo.config.commit),
|
||||
on_pull: repo.config.on_pull.into_option(),
|
||||
})
|
||||
.await
|
||||
@@ -185,23 +173,47 @@ impl Resolve<PullRepo, User> for State {
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
let res = db_client().await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&repo.id)? },
|
||||
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
|
||||
repo.id
|
||||
);
|
||||
}
|
||||
update_last_pulled(&repo.name).await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
handle_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_update_return(
|
||||
update: Update,
|
||||
) -> anyhow::Result<Update> {
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().await.updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_repo_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
async fn update_last_pulled(repo_name: &str) {
|
||||
let res = db_client()
|
||||
.await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "name": repo_name },
|
||||
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo: {repo_name} | {e:#}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use monitor_client::{
|
||||
entities::{
|
||||
deployment::{
|
||||
Deployment, DeploymentActionState, DeploymentConfig,
|
||||
DeploymentListItem, DockerContainerState, DockerContainerStats,
|
||||
DeploymentListItem, DeploymentState, DockerContainerStats,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
@@ -237,13 +237,13 @@ impl Resolve<GetDeploymentsSummary, User> for State {
|
||||
let status =
|
||||
status_cache.get(&deployment.id).await.unwrap_or_default();
|
||||
match status.curr.state {
|
||||
DockerContainerState::Running => {
|
||||
DeploymentState::Running => {
|
||||
res.running += 1;
|
||||
}
|
||||
DockerContainerState::Unknown => {
|
||||
DeploymentState::Unknown => {
|
||||
res.unknown += 1;
|
||||
}
|
||||
DockerContainerState::NotDeployed => {
|
||||
DeploymentState::NotDeployed => {
|
||||
res.not_deployed += 1;
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -72,7 +72,7 @@ enum ReadRequest {
|
||||
GetServersSummary(GetServersSummary),
|
||||
GetServer(GetServer),
|
||||
ListServers(ListServers),
|
||||
GetServerStatus(GetServerStatus),
|
||||
GetServerState(GetServerState),
|
||||
GetPeripheryVersion(GetPeripheryVersion),
|
||||
GetDockerContainers(GetDockerContainers),
|
||||
GetDockerImages(GetDockerImages),
|
||||
|
||||
@@ -15,7 +15,7 @@ use monitor_client::{
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
docker_image::ImageSummary, docker_network::DockerNetwork,
|
||||
Server, ServerActionState, ServerListItem, ServerStatus,
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
},
|
||||
user::User,
|
||||
},
|
||||
@@ -48,14 +48,14 @@ impl Resolve<GetServersSummary, User> for State {
|
||||
let mut res = GetServersSummaryResponse::default();
|
||||
for server in servers {
|
||||
res.total += 1;
|
||||
match server.info.status {
|
||||
ServerStatus::Ok => {
|
||||
match server.info.state {
|
||||
ServerState::Ok => {
|
||||
res.healthy += 1;
|
||||
}
|
||||
ServerStatus::NotOk => {
|
||||
ServerState::NotOk => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
ServerStatus::Disabled => {
|
||||
ServerState::Disabled => {
|
||||
res.disabled += 1;
|
||||
}
|
||||
}
|
||||
@@ -114,12 +114,12 @@ impl Resolve<ListServers, User> for State {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServerStatus, User> for State {
|
||||
impl Resolve<GetServerState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServerStatus { server }: GetServerStatus,
|
||||
GetServerState { server }: GetServerState,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServerStatusResponse> {
|
||||
) -> anyhow::Result<GetServerStateResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
@@ -130,8 +130,8 @@ impl Resolve<GetServerStatus, User> for State {
|
||||
.get(&server.id)
|
||||
.await
|
||||
.ok_or(anyhow!("did not find cached status for server"))?;
|
||||
let response = GetServerStatusResponse {
|
||||
status: status.status,
|
||||
let response = GetServerStateResponse {
|
||||
status: status.state,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
deployment::{Deployment, DockerContainerState},
|
||||
deployment::{Deployment, DeploymentState},
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
@@ -112,7 +112,7 @@ impl Resolve<RenameDeployment, User> for State {
|
||||
|
||||
let container_state = get_deployment_state(&deployment).await?;
|
||||
|
||||
if container_state == DockerContainerState::Unknown {
|
||||
if container_state == DeploymentState::Unknown {
|
||||
return Err(anyhow!(
|
||||
"cannot rename deployment when container status is unknown"
|
||||
));
|
||||
@@ -132,7 +132,7 @@ impl Resolve<RenameDeployment, User> for State {
|
||||
.await
|
||||
.context("failed to update deployment name on db")?;
|
||||
|
||||
if container_state != DockerContainerState::NotDeployed {
|
||||
if container_state != DeploymentState::NotDeployed {
|
||||
let server =
|
||||
resource::get::<Server>(&deployment.config.server_id).await?;
|
||||
let log = periphery_client(&server)?
|
||||
|
||||
@@ -3,7 +3,7 @@ use futures::future::join_all;
|
||||
use monitor_client::entities::{
|
||||
alert::{Alert, AlertData},
|
||||
alerter::*,
|
||||
deployment::DockerContainerState,
|
||||
deployment::DeploymentState,
|
||||
server::stats::SeverityLevel,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
@@ -236,14 +236,12 @@ fn fmt_region(region: &Option<String>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_docker_container_state(
|
||||
state: &DockerContainerState,
|
||||
) -> String {
|
||||
fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
||||
match state {
|
||||
DockerContainerState::Running => String::from("Running ▶️"),
|
||||
DockerContainerState::Exited => String::from("Exited 🛑"),
|
||||
DockerContainerState::Restarting => String::from("Restarting 🔄"),
|
||||
DockerContainerState::NotDeployed => String::from("Not Deployed"),
|
||||
DeploymentState::Running => String::from("Running ▶️"),
|
||||
DeploymentState::Exited => String::from("Exited 🛑"),
|
||||
DeploymentState::Restarting => String::from("Restarting 🔄"),
|
||||
DeploymentState::NotDeployed => String::from("Not Deployed"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::entities::{
|
||||
deployment::{Deployment, DockerContainerState},
|
||||
deployment::{Deployment, DeploymentState},
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerStatus},
|
||||
server::{Server, ServerState},
|
||||
tag::Tag,
|
||||
update::ResourceTargetVariant,
|
||||
user::{admin_service_user, User},
|
||||
@@ -31,17 +31,17 @@ pub async fn get_user(user_id: &str) -> anyhow::Result<User> {
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_server_with_status(
|
||||
server_id_or_name: &str,
|
||||
) -> anyhow::Result<(Server, ServerStatus)> {
|
||||
) -> anyhow::Result<(Server, ServerState)> {
|
||||
let server = resource::get::<Server>(server_id_or_name).await?;
|
||||
if !server.config.enabled {
|
||||
return Ok((server, ServerStatus::Disabled));
|
||||
return Ok((server, ServerState::Disabled));
|
||||
}
|
||||
let status = match super::periphery_client(&server)?
|
||||
.request(periphery_client::api::GetHealth {})
|
||||
.await
|
||||
{
|
||||
Ok(_) => ServerStatus::Ok,
|
||||
Err(_) => ServerStatus::NotOk,
|
||||
Ok(_) => ServerState::Ok,
|
||||
Err(_) => ServerState::NotOk,
|
||||
};
|
||||
Ok((server, status))
|
||||
}
|
||||
@@ -49,14 +49,14 @@ pub async fn get_server_with_status(
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_deployment_state(
|
||||
deployment: &Deployment,
|
||||
) -> anyhow::Result<DockerContainerState> {
|
||||
) -> anyhow::Result<DeploymentState> {
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Ok(DockerContainerState::NotDeployed);
|
||||
return Ok(DeploymentState::NotDeployed);
|
||||
}
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Ok(DockerContainerState::Unknown);
|
||||
if status != ServerState::Ok {
|
||||
return Ok(DeploymentState::Unknown);
|
||||
}
|
||||
let container = super::periphery_client(&server)?
|
||||
.request(periphery_client::api::container::GetContainerList {})
|
||||
@@ -66,7 +66,7 @@ pub async fn get_deployment_state(
|
||||
|
||||
let state = match container {
|
||||
Some(container) => container.state,
|
||||
None => DockerContainerState::NotDeployed,
|
||||
None => DeploymentState::NotDeployed,
|
||||
};
|
||||
|
||||
Ok(state)
|
||||
|
||||
@@ -6,7 +6,9 @@ use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use monitor_client::{
|
||||
api::execute,
|
||||
entities::{build::Build, repo::Repo, user::github_user},
|
||||
entities::{
|
||||
build::Build, procedure::Procedure, repo::Repo, user::github_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::Deserialize;
|
||||
@@ -129,6 +131,9 @@ async fn handle_build_webhook(
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let build = resource::get::<Build>(&build_id).await?;
|
||||
if !build.config.webhook_enabled {
|
||||
return Err(anyhow!("build does not have webhook enabled"));
|
||||
}
|
||||
if request_branch != build.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
@@ -155,6 +160,9 @@ async fn handle_repo_clone_webhook(
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let repo = resource::get::<Repo>(&repo_id).await?;
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
@@ -181,6 +189,9 @@ async fn handle_repo_pull_webhook(
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let repo = resource::get::<Repo>(&repo_id).await?;
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
@@ -211,6 +222,10 @@ async fn handle_procedure_webhook(
|
||||
if request_branch != target_branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
let procedure = resource::get::<Procedure>(&procedure_id).await?;
|
||||
if !procedure.config.webhook_enabled {
|
||||
return Err(anyhow!("procedure does not have webhook enabled"));
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
execute::RunProcedure {
|
||||
|
||||
@@ -33,8 +33,10 @@ async fn app() -> anyhow::Result<()> {
|
||||
info!("config: {:?}", config.sanitized());
|
||||
|
||||
// Spawn monitoring loops
|
||||
monitor::spawn_monitor_loop();
|
||||
monitor::spawn_monitor_loop()?;
|
||||
helpers::prune::spawn_prune_loop();
|
||||
resource::spawn_build_state_refresh_loop();
|
||||
resource::spawn_repo_state_refresh_loop();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = frontend_path();
|
||||
|
||||
@@ -5,7 +5,7 @@ use mongo_indexed::Indexed;
|
||||
use monitor_client::entities::{
|
||||
alert::{Alert, AlertData, AlertDataVariant},
|
||||
monitor_timestamp, optional_string,
|
||||
server::{stats::SeverityLevel, ServerListItem, ServerStatus},
|
||||
server::{stats::SeverityLevel, ServerListItem, ServerState},
|
||||
update::ResourceTarget,
|
||||
};
|
||||
use mungos::{
|
||||
@@ -56,8 +56,8 @@ pub async fn alert_servers(
|
||||
let health_alert = server_alerts.as_ref().and_then(|alerts| {
|
||||
alerts.get(&AlertDataVariant::ServerUnreachable)
|
||||
});
|
||||
match (server_status.status, health_alert) {
|
||||
(ServerStatus::NotOk, None) => {
|
||||
match (server_status.state, health_alert) {
|
||||
(ServerState::NotOk, None) => {
|
||||
// open unreachable alert
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
@@ -77,7 +77,7 @@ pub async fn alert_servers(
|
||||
alerts_to_open
|
||||
.push((alert, server.info.send_unreachable_alerts))
|
||||
}
|
||||
(ServerStatus::NotOk, Some(alert)) => {
|
||||
(ServerState::NotOk, Some(alert)) => {
|
||||
// update alert err
|
||||
let mut alert = alert.clone();
|
||||
let (id, name, region) = match alert.data {
|
||||
@@ -102,7 +102,7 @@ pub async fn alert_servers(
|
||||
|
||||
// Close an open alert
|
||||
(
|
||||
ServerStatus::Ok | ServerStatus::Disabled,
|
||||
ServerState::Ok | ServerState::Disabled,
|
||||
Some(health_alert),
|
||||
) => alert_ids_to_close.push((
|
||||
health_alert.id.clone(),
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
use monitor_client::entities::{
|
||||
deployment::{Deployment, DockerContainerState},
|
||||
deployment::{Deployment, DeploymentState},
|
||||
repo::Repo,
|
||||
server::{
|
||||
stats::{
|
||||
ServerHealth, SeverityLevel, SingleDiskUsage, SystemStats,
|
||||
},
|
||||
Server, ServerConfig, ServerStatus,
|
||||
Server, ServerConfig, ServerState,
|
||||
},
|
||||
};
|
||||
use serror::Serror;
|
||||
|
||||
use crate::state::{deployment_status_cache, server_status_cache};
|
||||
use crate::state::{
|
||||
deployment_status_cache, repo_status_cache, server_status_cache,
|
||||
};
|
||||
|
||||
use super::{CachedDeploymentStatus, CachedServerStatus, History};
|
||||
use super::{
|
||||
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
|
||||
History,
|
||||
};
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn insert_deployments_status_unknown(
|
||||
@@ -27,7 +33,7 @@ pub async fn insert_deployments_status_unknown(
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: DockerContainerState::Unknown,
|
||||
state: DeploymentState::Unknown,
|
||||
container: None,
|
||||
},
|
||||
prev,
|
||||
@@ -38,10 +44,28 @@ pub async fn insert_deployments_status_unknown(
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn insert_repos_status_unknown(repos: Vec<Repo>) {
|
||||
let status_cache = repo_status_cache();
|
||||
for repo in repos {
|
||||
status_cache
|
||||
.insert(
|
||||
repo.id.clone(),
|
||||
CachedRepoStatus {
|
||||
id: repo.id,
|
||||
latest_hash: None,
|
||||
latest_message: None,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn insert_server_status(
|
||||
server: &Server,
|
||||
status: ServerStatus,
|
||||
state: ServerState,
|
||||
version: String,
|
||||
stats: Option<SystemStats>,
|
||||
err: impl Into<Option<Serror>>,
|
||||
@@ -52,7 +76,7 @@ pub async fn insert_server_status(
|
||||
server.id.clone(),
|
||||
CachedServerStatus {
|
||||
id: server.id.clone(),
|
||||
status,
|
||||
state,
|
||||
version,
|
||||
stats,
|
||||
health,
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use futures::future::join_all;
|
||||
use monitor_client::entities::{
|
||||
deployment::{ContainerSummary, DockerContainerState},
|
||||
deployment::{ContainerSummary, DeploymentState},
|
||||
server::{
|
||||
stats::{ServerHealth, SystemStats},
|
||||
Server, ServerStatus,
|
||||
Server, ServerState,
|
||||
},
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use periphery_client::api;
|
||||
use periphery_client::api::{self, git::GetLatestCommit};
|
||||
use serror::Serror;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::periphery_client,
|
||||
monitor::{alert::check_alerts, record::record_server_stats},
|
||||
state::{db_client, deployment_status_cache},
|
||||
state::{db_client, deployment_status_cache, repo_status_cache},
|
||||
};
|
||||
|
||||
use self::helpers::{
|
||||
insert_deployments_status_unknown, insert_server_status,
|
||||
insert_deployments_status_unknown, insert_repos_status_unknown,
|
||||
insert_server_status,
|
||||
};
|
||||
|
||||
mod alert;
|
||||
@@ -34,7 +36,7 @@ pub struct History<Curr: Default, Prev> {
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedServerStatus {
|
||||
pub id: String,
|
||||
pub status: ServerStatus,
|
||||
pub state: ServerState,
|
||||
pub version: String,
|
||||
pub stats: Option<SystemStats>,
|
||||
pub health: Option<ServerHealth>,
|
||||
@@ -44,16 +46,24 @@ pub struct CachedServerStatus {
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedDeploymentStatus {
|
||||
pub id: String,
|
||||
pub state: DockerContainerState,
|
||||
pub state: DeploymentState,
|
||||
pub container: Option<ContainerSummary>,
|
||||
}
|
||||
|
||||
pub fn spawn_monitor_loop() {
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedRepoStatus {
|
||||
pub id: String,
|
||||
pub latest_hash: Option<String>,
|
||||
pub latest_message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn spawn_monitor_loop() -> anyhow::Result<()> {
|
||||
let interval: async_timing_util::Timelength =
|
||||
core_config().monitoring_interval.try_into()?;
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let ts = (wait_until_timelength(Timelength::FiveSeconds, 500)
|
||||
.await
|
||||
- 500) as i64;
|
||||
let ts =
|
||||
(wait_until_timelength(interval, 500).await - 500) as i64;
|
||||
let servers =
|
||||
match find_collect(&db_client().await.servers, None, None)
|
||||
.await
|
||||
@@ -73,6 +83,7 @@ pub fn spawn_monitor_loop() {
|
||||
tokio::join!(check_alerts(ts), record_server_stats(ts));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -87,15 +98,31 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
Ok(deployments) => deployments,
|
||||
Err(e) => {
|
||||
error!("failed to get deployments list from mongo (update status cache) | server id: {} | {e:#}", server.id);
|
||||
return;
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let repos = match find_collect(
|
||||
&db_client().await.repos,
|
||||
doc! { "config.server_id": &server.id },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(repos) => repos,
|
||||
Err(e) => {
|
||||
error!("failed to get repos list from mongo (update status cache) | server id: {} | {e:#}", server.id);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle server disabled
|
||||
if !server.config.enabled {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_repos_status_unknown(repos).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::Disabled,
|
||||
ServerState::Disabled,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
None,
|
||||
@@ -103,16 +130,22 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
// already handle server disabled case above, so using unwrap here
|
||||
let periphery = periphery_client(server).unwrap();
|
||||
|
||||
let Ok(periphery) = periphery_client(server) else {
|
||||
error!(
|
||||
"somehow periphery not ok to create. should not be reached."
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let version = match periphery.request(api::GetVersion {}).await {
|
||||
Ok(version) => version.version,
|
||||
Err(e) => {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_repos_status_unknown(repos).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::NotOk,
|
||||
ServerState::NotOk,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
@@ -127,9 +160,10 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
Ok(stats) => Some(stats),
|
||||
Err(e) => {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_repos_status_unknown(repos).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::NotOk,
|
||||
ServerState::NotOk,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
@@ -142,45 +176,64 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
None
|
||||
};
|
||||
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::Ok,
|
||||
version,
|
||||
stats,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
insert_server_status(server, ServerState::Ok, version, stats, None)
|
||||
.await;
|
||||
|
||||
let containers =
|
||||
periphery.request(api::container::GetContainerList {}).await;
|
||||
if containers.is_err() {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
return;
|
||||
}
|
||||
match periphery.request(api::container::GetContainerList {}).await {
|
||||
Ok(containers) => {
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let container = containers
|
||||
.iter()
|
||||
.find(|c| c.name == deployment.name)
|
||||
.cloned();
|
||||
let prev = status_cache
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| s.curr.state);
|
||||
let state = container
|
||||
.as_ref()
|
||||
.map(|c| c.state)
|
||||
.unwrap_or(DeploymentState::NotDeployed);
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state,
|
||||
container,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("could not get containers list | {e:#}");
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
}
|
||||
};
|
||||
|
||||
let containers = containers.unwrap();
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let container = containers
|
||||
.iter()
|
||||
.find(|c| c.name == deployment.name)
|
||||
.cloned();
|
||||
let prev =
|
||||
status_cache.get(&deployment.id).await.map(|s| s.curr.state);
|
||||
let state = container
|
||||
.as_ref()
|
||||
.map(|c| c.state)
|
||||
.unwrap_or(DockerContainerState::NotDeployed);
|
||||
let status_cache = repo_status_cache();
|
||||
for repo in repos {
|
||||
let (latest_hash, latest_message) = periphery
|
||||
.request(GetLatestCommit {
|
||||
name: repo.name.clone(),
|
||||
})
|
||||
.await
|
||||
.map(|r| (r.hash, r.message))
|
||||
.ok()
|
||||
.unzip();
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state,
|
||||
container,
|
||||
},
|
||||
prev,
|
||||
repo.id.clone(),
|
||||
CachedRepoStatus {
|
||||
id: repo.id,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
build::{
|
||||
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
|
||||
BuildListItemInfo, BuildQuerySpecifics, PartialBuildConfig,
|
||||
BuildListItemInfo, BuildQuerySpecifics, BuildState,
|
||||
PartialBuildConfig,
|
||||
},
|
||||
builder::Builder,
|
||||
permission::PermissionLevel,
|
||||
@@ -11,11 +14,14 @@ use monitor_client::entities::{
|
||||
user::User,
|
||||
Operation,
|
||||
};
|
||||
use mungos::mongodb::Collection;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOneOptions, Collection},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::empty_or_only_spaces,
|
||||
state::{action_states, db_client},
|
||||
state::{action_states, build_state_cache, db_client},
|
||||
};
|
||||
|
||||
impl super::MonitorResource for Build {
|
||||
@@ -38,6 +44,7 @@ impl super::MonitorResource for Build {
|
||||
async fn to_list_item(
|
||||
build: Resource<Self::Config, Self::Info>,
|
||||
) -> anyhow::Result<Self::ListItem> {
|
||||
let state = get_build_state(&build.id).await;
|
||||
Ok(BuildListItem {
|
||||
name: build.name,
|
||||
id: build.id,
|
||||
@@ -48,6 +55,7 @@ impl super::MonitorResource for Build {
|
||||
version: build.config.version,
|
||||
repo: build.config.repo,
|
||||
branch: build.config.branch,
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -127,6 +135,33 @@ impl super::MonitorResource for Build {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_build_state_refresh_loop() {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
refresh_build_state_cache().await;
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn refresh_build_state_cache() {
|
||||
let _ = async {
|
||||
let builds = find_collect(&db_client().await.builds, None, None)
|
||||
.await
|
||||
.context("failed to get builds from db")?;
|
||||
let cache = build_state_cache();
|
||||
for build in builds {
|
||||
let state = get_build_state_from_db(&build.id).await;
|
||||
cache.insert(build.id, state).await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!("failed to refresh build state cache | {e:#}")
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(skip(user))]
|
||||
async fn validate_config(
|
||||
config: &mut PartialBuildConfig,
|
||||
@@ -149,3 +184,52 @@ async fn validate_config(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_build_state(id: &String) -> BuildState {
|
||||
if action_states()
|
||||
.build
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.building))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return BuildState::Building;
|
||||
}
|
||||
build_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn get_build_state_from_db(id: &str) -> BuildState {
|
||||
async {
|
||||
let state = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(
|
||||
doc! {
|
||||
"target.type": "Build",
|
||||
"target.id": id,
|
||||
"operation": "RunBuild"
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build(),
|
||||
)
|
||||
.await?
|
||||
.map(|u| {
|
||||
if u.success {
|
||||
BuildState::Ok
|
||||
} else {
|
||||
BuildState::Failed
|
||||
}
|
||||
})
|
||||
.unwrap_or(BuildState::Ok);
|
||||
anyhow::Ok(state)
|
||||
}
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to get build state for {id} | {e:#}")
|
||||
})
|
||||
.unwrap_or(BuildState::Unknown)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use monitor_client::entities::{
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentConfigDiff,
|
||||
DeploymentImage, DeploymentListItem, DeploymentListItemInfo,
|
||||
DeploymentQuerySpecifics, DockerContainerState,
|
||||
DeploymentQuerySpecifics, DeploymentState,
|
||||
PartialDeploymentConfig,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
@@ -149,8 +149,8 @@ impl super::MonitorResource for Deployment {
|
||||
.context("failed to get container state")?;
|
||||
if !matches!(
|
||||
state,
|
||||
DockerContainerState::NotDeployed
|
||||
| DockerContainerState::Unknown
|
||||
DeploymentState::NotDeployed
|
||||
| DeploymentState::Unknown
|
||||
) {
|
||||
// container needs to be destroyed
|
||||
let server =
|
||||
|
||||
@@ -48,6 +48,13 @@ mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
|
||||
pub use build::{
|
||||
refresh_build_state_cache, spawn_build_state_refresh_loop,
|
||||
};
|
||||
pub use repo::{
|
||||
refresh_repo_state_cache, spawn_repo_state_refresh_loop,
|
||||
};
|
||||
|
||||
/// Implement on each monitor resource for common methods
|
||||
pub trait MonitorResource {
|
||||
type ListItem: Serialize + Send;
|
||||
@@ -377,7 +384,7 @@ pub async fn update<T: MonitorResource>(
|
||||
|
||||
for FieldDiff { field, from, to } in diff.iter_field_diffs() {
|
||||
diff_log.push_str(&format!(
|
||||
"\n\nfield: '{field}'\nfrom: {from}\nto: {to}"
|
||||
"\n\n<span class=\"text-muted-foreground\">field</span>: '{field}'\n<span class=\"text-muted-foreground\">from</span>: <span class=\"text-red-500\">{from}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-500\">{to}</span>",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
permission::PermissionLevel,
|
||||
repo::{
|
||||
PartialRepoConfig, Repo, RepoConfig, RepoConfigDiff, RepoInfo,
|
||||
RepoListItem, RepoListItemInfo, RepoQuerySpecifics,
|
||||
RepoListItem, RepoListItemInfo, RepoQuerySpecifics, RepoState,
|
||||
},
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
@@ -11,13 +13,18 @@ use monitor_client::entities::{
|
||||
user::User,
|
||||
Operation,
|
||||
};
|
||||
use mungos::mongodb::Collection;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOneOptions, Collection},
|
||||
};
|
||||
use periphery_client::api::git::DeleteRepo;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client,
|
||||
state::{action_states, db_client},
|
||||
state::{
|
||||
action_states, db_client, repo_state_cache, repo_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::get_check_permissions;
|
||||
@@ -42,6 +49,9 @@ impl super::MonitorResource for Repo {
|
||||
async fn to_list_item(
|
||||
repo: Resource<Self::Config, Self::Info>,
|
||||
) -> anyhow::Result<Self::ListItem> {
|
||||
let state = get_repo_state(&repo.id).await;
|
||||
let status =
|
||||
repo_status_cache().get(&repo.id).await.unwrap_or_default();
|
||||
Ok(RepoListItem {
|
||||
name: repo.name,
|
||||
id: repo.id,
|
||||
@@ -51,6 +61,9 @@ impl super::MonitorResource for Repo {
|
||||
last_pulled_at: repo.info.last_pulled_at,
|
||||
repo: repo.config.repo,
|
||||
branch: repo.config.branch,
|
||||
state,
|
||||
latest_hash: status.latest_hash.clone(),
|
||||
latest_message: status.latest_message.clone(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -150,6 +163,33 @@ impl super::MonitorResource for Repo {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_repo_state_refresh_loop() {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
refresh_repo_state_cache().await;
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn refresh_repo_state_cache() {
|
||||
let _ = async {
|
||||
let repos = find_collect(&db_client().await.repos, None, None)
|
||||
.await
|
||||
.context("failed to get repos from db")?;
|
||||
let cache = repo_state_cache();
|
||||
for repo in repos {
|
||||
let state = get_repo_state_from_db(&repo.id).await;
|
||||
cache.insert(repo.id, state).await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!("failed to refresh repo state cache | {e:#}")
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(skip(user))]
|
||||
async fn validate_config(
|
||||
config: &mut PartialRepoConfig,
|
||||
@@ -170,3 +210,62 @@ async fn validate_config(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_repo_state(id: &String) -> RepoState {
|
||||
if let Some(state) = action_states()
|
||||
.repo
|
||||
.get(id)
|
||||
.await
|
||||
.and_then(|s| {
|
||||
s.get()
|
||||
.map(|s| {
|
||||
if s.cloning {
|
||||
Some(RepoState::Cloning)
|
||||
} else if s.pulling {
|
||||
Some(RepoState::Pulling)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
return state;
|
||||
}
|
||||
repo_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn get_repo_state_from_db(id: &str) -> RepoState {
|
||||
async {
|
||||
let state = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(
|
||||
doc! {
|
||||
"target.type": "Repo",
|
||||
"target.id": id,
|
||||
"$or": [
|
||||
{ "operation": "CloneRepo" },
|
||||
{ "operation": "PullRepo" },
|
||||
],
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build(),
|
||||
)
|
||||
.await?
|
||||
.map(|u| {
|
||||
if u.success {
|
||||
RepoState::Ok
|
||||
} else {
|
||||
RepoState::Failed
|
||||
}
|
||||
})
|
||||
.unwrap_or(RepoState::Ok);
|
||||
anyhow::Ok(state)
|
||||
}
|
||||
.await
|
||||
.inspect_err(|e| warn!("failed to get repo state for {id} | {e:#}"))
|
||||
.unwrap_or(RepoState::Unknown)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl super::MonitorResource for Server {
|
||||
tags: server.tags,
|
||||
resource_type: ResourceTargetVariant::Server,
|
||||
info: ServerListItemInfo {
|
||||
status: status.map(|s| s.status).unwrap_or_default(),
|
||||
state: status.map(|s| s.state).unwrap_or_default(),
|
||||
region: server.config.region,
|
||||
send_unreachable_alerts: server
|
||||
.config
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use monitor_client::entities::deployment::DockerContainerState;
|
||||
use monitor_client::entities::{
|
||||
build::BuildState, deployment::DeploymentState, repo::RepoState,
|
||||
};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::{
|
||||
@@ -8,7 +10,10 @@ use crate::{
|
||||
config::core_config,
|
||||
db::DbClient,
|
||||
helpers::{action_state::ActionStates, cache::Cache},
|
||||
monitor::{CachedDeploymentStatus, CachedServerStatus, History},
|
||||
monitor::{
|
||||
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
|
||||
History,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct State;
|
||||
@@ -36,7 +41,7 @@ pub fn action_states() -> &'static ActionStates {
|
||||
|
||||
pub type DeploymentStatusCache = Cache<
|
||||
String,
|
||||
Arc<History<CachedDeploymentStatus, DockerContainerState>>,
|
||||
Arc<History<CachedDeploymentStatus, DeploymentState>>,
|
||||
>;
|
||||
|
||||
pub fn deployment_status_cache() -> &'static DeploymentStatusCache {
|
||||
@@ -52,3 +57,26 @@ pub fn server_status_cache() -> &'static ServerStatusCache {
|
||||
OnceLock::new();
|
||||
SERVER_STATUS_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub type RepoStatusCache = Cache<String, Arc<CachedRepoStatus>>;
|
||||
|
||||
pub fn repo_status_cache() -> &'static RepoStatusCache {
|
||||
static REPO_STATUS_CACHE: OnceLock<RepoStatusCache> =
|
||||
OnceLock::new();
|
||||
REPO_STATUS_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub type BuildStateCache = Cache<String, BuildState>;
|
||||
|
||||
pub fn build_state_cache() -> &'static BuildStateCache {
|
||||
static BUILD_STATE_CACHE: OnceLock<BuildStateCache> =
|
||||
OnceLock::new();
|
||||
BUILD_STATE_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub type RepoStateCache = Cache<String, RepoState>;
|
||||
|
||||
pub fn repo_state_cache() -> &'static RepoStateCache {
|
||||
static REPO_STATE_CACHE: OnceLock<RepoStateCache> = OnceLock::new();
|
||||
REPO_STATE_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
@@ -230,6 +230,8 @@ impl TryFrom<Build> for monitor_client::entities::build::Build {
|
||||
extra_args,
|
||||
use_buildx,
|
||||
labels: Default::default(),
|
||||
webhook_enabled: true,
|
||||
commit: Default::default(),
|
||||
},
|
||||
};
|
||||
Ok(build)
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
use anyhow::anyhow;
|
||||
use monitor_client::entities::{to_monitor_name, update::Log};
|
||||
use periphery_client::api::git::{CloneRepo, DeleteRepo, PullRepo};
|
||||
use periphery_client::api::git::{
|
||||
CloneRepo, DeleteRepo, GetLatestCommit, GetLatestCommitResponse,
|
||||
PullRepo,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{config::periphery_config, helpers::git, State};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Resolve<GetLatestCommit, ()> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetLatestCommit { name }: GetLatestCommit,
|
||||
_: (),
|
||||
) -> anyhow::Result<GetLatestCommitResponse> {
|
||||
let repo_path = periphery_config().repo_dir.join(name);
|
||||
if !repo_path.is_dir() {
|
||||
return Err(anyhow!(
|
||||
"repo path is not directory. is it cloned?"
|
||||
));
|
||||
}
|
||||
git::get_commit_hash_info(&repo_path).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Resolve<CloneRepo> for State {
|
||||
#[instrument(name = "CloneRepo", skip(self))]
|
||||
@@ -26,6 +47,7 @@ impl Resolve<PullRepo> for State {
|
||||
PullRepo {
|
||||
name,
|
||||
branch,
|
||||
commit,
|
||||
on_pull,
|
||||
}: PullRepo,
|
||||
_: (),
|
||||
@@ -35,6 +57,7 @@ impl Resolve<PullRepo> for State {
|
||||
git::pull(
|
||||
&periphery_config().repo_dir.join(name),
|
||||
&branch,
|
||||
&commit,
|
||||
&on_pull,
|
||||
)
|
||||
.await,
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::path::Path;
|
||||
use anyhow::Context;
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use monitor_client::entities::{
|
||||
monitor_timestamp, to_monitor_name, update::Log, CloneArgs,
|
||||
SystemCommand,
|
||||
all_logs_success, monitor_timestamp, to_monitor_name, update::Log,
|
||||
CloneArgs, SystemCommand,
|
||||
};
|
||||
use periphery_client::api::git::GetLatestCommitResponse;
|
||||
use run_command::async_run_command;
|
||||
|
||||
use crate::config::periphery_config;
|
||||
@@ -15,6 +16,7 @@ use super::{get_github_token, run_monitor_command};
|
||||
pub async fn pull(
|
||||
path: &Path,
|
||||
branch: &Option<String>,
|
||||
commit: &Option<String>,
|
||||
on_pull: &Option<SystemCommand>,
|
||||
) -> Vec<Log> {
|
||||
let branch = match branch {
|
||||
@@ -27,8 +29,19 @@ pub async fn pull(
|
||||
|
||||
let pull_log = run_monitor_command("git pull", command).await;
|
||||
|
||||
if !pull_log.success {
|
||||
return vec![pull_log];
|
||||
let mut logs = vec![pull_log];
|
||||
|
||||
if !logs[0].success {
|
||||
return logs;
|
||||
}
|
||||
|
||||
if let Some(commit) = commit {
|
||||
let reset_log = run_monitor_command(
|
||||
"set commit",
|
||||
format!("cd {} && git reset --hard {commit}", path.display()),
|
||||
)
|
||||
.await;
|
||||
logs.push(reset_log);
|
||||
}
|
||||
|
||||
let commit_hash_log =
|
||||
@@ -36,8 +49,7 @@ pub async fn pull(
|
||||
"latest commit",
|
||||
String::from("failed to get latest commit"),
|
||||
));
|
||||
|
||||
let mut logs = vec![pull_log, commit_hash_log];
|
||||
logs.push(commit_hash_log);
|
||||
|
||||
if let Some(on_pull) = on_pull {
|
||||
if !on_pull.path.is_empty() && !on_pull.command.is_empty() {
|
||||
@@ -66,6 +78,7 @@ where
|
||||
name,
|
||||
repo,
|
||||
branch,
|
||||
commit,
|
||||
on_clone,
|
||||
on_pull,
|
||||
github_account,
|
||||
@@ -83,19 +96,19 @@ where
|
||||
|
||||
let repo_dir = periphery_config().repo_dir.join(name);
|
||||
|
||||
let clone_log =
|
||||
clone_inner(repo, &repo_dir, &branch, access_token).await;
|
||||
let mut logs =
|
||||
clone_inner(repo, &repo_dir, &branch, &commit, access_token)
|
||||
.await;
|
||||
|
||||
if !clone_log.success {
|
||||
if !all_logs_success(&logs) {
|
||||
warn!("repo at {repo_dir:?} failed to clone");
|
||||
return Ok(vec![clone_log]);
|
||||
return Ok(logs);
|
||||
}
|
||||
|
||||
info!("repo at {repo_dir:?} cloned with clone_inner");
|
||||
|
||||
let commit_hash_log = get_commit_hash_log(&repo_dir).await?;
|
||||
|
||||
let mut logs = vec![clone_log, commit_hash_log];
|
||||
logs.push(commit_hash_log);
|
||||
|
||||
if let Some(command) = on_clone {
|
||||
if !command.path.is_empty() && !command.command.is_empty() {
|
||||
@@ -143,8 +156,9 @@ async fn clone_inner(
|
||||
repo: &str,
|
||||
destination: &Path,
|
||||
branch: &Option<String>,
|
||||
commit: &Option<String>,
|
||||
access_token: Option<String>,
|
||||
) -> Log {
|
||||
) -> Vec<Log> {
|
||||
let _ = std::fs::remove_dir_all(destination);
|
||||
let access_token_at = match &access_token {
|
||||
Some(token) => format!("{token}@"),
|
||||
@@ -162,6 +176,7 @@ async fn clone_inner(
|
||||
let output = async_run_command(&command).await;
|
||||
let success = output.success();
|
||||
let (command, stderr) = if !access_token_at.is_empty() {
|
||||
// know that access token can't be none if access token non-empty
|
||||
let access_token = access_token.unwrap();
|
||||
(
|
||||
command.replace(&access_token, "<TOKEN>"),
|
||||
@@ -170,7 +185,7 @@ async fn clone_inner(
|
||||
} else {
|
||||
(command, output.stderr)
|
||||
};
|
||||
Log {
|
||||
let mut logs = vec![Log {
|
||||
stage: "clone repo".to_string(),
|
||||
command,
|
||||
success,
|
||||
@@ -178,7 +193,45 @@ async fn clone_inner(
|
||||
stderr,
|
||||
start_ts,
|
||||
end_ts: unix_timestamp_ms() as i64,
|
||||
}];
|
||||
|
||||
if !logs[0].success {
|
||||
return logs;
|
||||
}
|
||||
|
||||
if let Some(commit) = commit {
|
||||
let reset_log = run_monitor_command(
|
||||
"set commit",
|
||||
format!(
|
||||
"cd {} && git reset --hard {commit}",
|
||||
destination.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
logs.push(reset_log);
|
||||
}
|
||||
|
||||
logs
|
||||
}
|
||||
|
||||
pub async fn get_commit_hash_info(
|
||||
repo_dir: &Path,
|
||||
) -> anyhow::Result<GetLatestCommitResponse> {
|
||||
let command = format!("cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B", repo_dir.display());
|
||||
let output = async_run_command(&command).await;
|
||||
let mut split = output.stdout.split('\n');
|
||||
let (hash, _, message) = (
|
||||
split
|
||||
.next()
|
||||
.context("failed to get short commit hash")?
|
||||
.to_string(),
|
||||
split.next().context("failed to get long commit hash")?,
|
||||
split
|
||||
.next()
|
||||
.context("failed to get commit message")?
|
||||
.to_string(),
|
||||
);
|
||||
Ok(GetLatestCommitResponse { hash, message })
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
|
||||
@@ -6,7 +6,7 @@ use typeshare::typeshare;
|
||||
use crate::entities::{
|
||||
deployment::{
|
||||
ContainerSummary, Deployment, DeploymentActionState,
|
||||
DeploymentListItem, DeploymentQuery, DockerContainerState,
|
||||
DeploymentListItem, DeploymentQuery, DeploymentState,
|
||||
DockerContainerStats,
|
||||
},
|
||||
update::Log,
|
||||
@@ -76,7 +76,7 @@ pub struct GetDeploymentContainer {
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GetDeploymentContainerResponse {
|
||||
pub state: DockerContainerState,
|
||||
pub state: DeploymentState,
|
||||
pub container: Option<ContainerSummary>,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::entities::{
|
||||
SystemStatsRecord,
|
||||
},
|
||||
Server, ServerActionState, ServerListItem, ServerQuery,
|
||||
ServerStatus,
|
||||
ServerState,
|
||||
},
|
||||
Timelength, I64,
|
||||
};
|
||||
@@ -58,25 +58,25 @@ pub type ListServersResponse = Vec<ServerListItem>;
|
||||
|
||||
//
|
||||
|
||||
/// Get the status of the target server. Response: [GetServerStatusResponse].
|
||||
/// Get the state of the target server. Response: [GetServerStateResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(MonitorReadRequest)]
|
||||
#[response(GetServerStatusResponse)]
|
||||
pub struct GetServerStatus {
|
||||
#[response(GetServerStateResponse)]
|
||||
pub struct GetServerState {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub server: String,
|
||||
}
|
||||
|
||||
/// The status for [GetServerStatus].
|
||||
/// The response for [GetServerState].
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GetServerStatusResponse {
|
||||
pub struct GetServerStateResponse {
|
||||
/// The server status.
|
||||
pub status: ServerStatus,
|
||||
pub status: ServerState,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -11,8 +11,8 @@ use typeshare::typeshare;
|
||||
use crate::entities::{MongoId, I64};
|
||||
|
||||
use super::{
|
||||
_Serror, deployment::DockerContainerState,
|
||||
server::stats::SeverityLevel, update::ResourceTarget,
|
||||
_Serror, deployment::DeploymentState, server::stats::SeverityLevel,
|
||||
update::ResourceTarget,
|
||||
};
|
||||
|
||||
/// Representation of an alert in the system.
|
||||
@@ -140,9 +140,9 @@ pub enum AlertData {
|
||||
/// The server name
|
||||
server_name: String,
|
||||
/// The previous container state
|
||||
from: DockerContainerState,
|
||||
from: DeploymentState,
|
||||
/// The current container state
|
||||
to: DockerContainerState,
|
||||
to: DeploymentState,
|
||||
},
|
||||
|
||||
/// An AWS builder failed to terminate.
|
||||
|
||||
@@ -3,6 +3,7 @@ use derive_default_builder::DefaultBuilder;
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::I64;
|
||||
@@ -19,7 +20,7 @@ pub type Build = Resource<BuildConfig, BuildInfo>;
|
||||
pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuildListItemInfo {
|
||||
/// Unix timestamp in milliseconds of last build
|
||||
pub last_built_at: I64,
|
||||
@@ -29,6 +30,24 @@ pub struct BuildListItemInfo {
|
||||
pub repo: String,
|
||||
/// The branch of the repo
|
||||
pub branch: String,
|
||||
/// State of the build. Reflects whether most recent build successful.
|
||||
pub state: BuildState,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
|
||||
)]
|
||||
pub enum BuildState {
|
||||
/// Last build successful (or never built)
|
||||
Ok,
|
||||
/// Last build failed
|
||||
Failed,
|
||||
/// Currently building
|
||||
Building,
|
||||
/// Other case
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -73,6 +92,11 @@ pub struct BuildConfig {
|
||||
#[partial_default(default_branch())]
|
||||
pub branch: String,
|
||||
|
||||
/// Optionally set a specific commit hash.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub commit: String,
|
||||
|
||||
/// The github account used to clone (used to access private repos).
|
||||
/// Empty string is public clone (only public repos).
|
||||
#[serde(default)]
|
||||
@@ -128,6 +152,12 @@ pub struct BuildConfig {
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub use_buildx: bool,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
#[partial_default(default_webhook_enabled())]
|
||||
pub webhook_enabled: bool,
|
||||
}
|
||||
|
||||
impl BuildConfig {
|
||||
@@ -148,6 +178,10 @@ fn default_dockerfile_path() -> String {
|
||||
String::from("Dockerfile")
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct BuildActionState {
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::entities::{
|
||||
/// configure the periphery agent. A help manual for the periphery binary
|
||||
/// can be printed using `/path/to/periphery --help`.
|
||||
#[derive(Parser)]
|
||||
#[command(author, about, version)]
|
||||
#[command(name = "periphery", author, about, version)]
|
||||
pub struct CliArgs {
|
||||
/// Sets the path of a config file or directory to use.
|
||||
/// Can use multiple times
|
||||
@@ -125,53 +125,53 @@ fn default_config_paths() -> Vec<String> {
|
||||
/// The periphery agent initializes it's configuration by reading the environment,
|
||||
/// parsing the [PeripheryConfig] schema from the files specified by cli args (and falling back to `env.config_paths`),
|
||||
/// and then applying any config field overrides specified in the environment.
|
||||
///
|
||||
///
|
||||
/// ## Example TOML
|
||||
/// ```toml
|
||||
/// ## optional. 8120 is default
|
||||
/// port = 8120
|
||||
///
|
||||
///
|
||||
/// ## optional. /repos is default.
|
||||
/// repo_dir = "/repos"
|
||||
///
|
||||
/// ## optional. 5-sec is default.
|
||||
///
|
||||
/// ## optional. 5-sec is default.
|
||||
/// ## can use 1-sec, 5-sec, 10-sec, 30-sec, 1-min.
|
||||
/// ## controls granularity of system stats recorded
|
||||
/// stats_polling_rate = "5-sec"
|
||||
///
|
||||
///
|
||||
/// ## optional. default is empty, which will not block any request by ip.
|
||||
/// allowed_ips = ["127.0.0.1"]
|
||||
///
|
||||
///
|
||||
/// ## optional. default is empty, which will not require any passkey to be passed by core.
|
||||
/// passkeys = ["abcdefghijk"]
|
||||
///
|
||||
///
|
||||
/// ## specify the log level of the monitor core application
|
||||
/// ## default: info
|
||||
/// ## options: off, error, warn, info, debug, trace
|
||||
/// logging.level = "info"
|
||||
///
|
||||
///
|
||||
/// ## specify the logging format for stdout / stderr.
|
||||
/// ## default: standard
|
||||
/// ## options: standard, json, none
|
||||
/// logging.stdio = "standard"
|
||||
///
|
||||
///
|
||||
/// ## specify an otlp endpoint to send traces to
|
||||
/// ## optional, default unassigned
|
||||
/// # logging.otlp_endpoint = "http://localhost:4317"
|
||||
///
|
||||
///
|
||||
/// ## specify the service name to send with otlp traces.
|
||||
/// ## optional, default 'Monitor'.
|
||||
/// # logging.opentelemetry_service_name = "Monitor"
|
||||
///
|
||||
///
|
||||
/// ## optional. can inject these values into your deployments configuration.
|
||||
/// [secrets]
|
||||
/// secret_variable = "secret_value"
|
||||
///
|
||||
///
|
||||
/// ## optional. can use these accounts with deployments / builds.
|
||||
/// [github_accounts]
|
||||
/// github_username1 = "github_token1"
|
||||
/// github_username2 = "github_token2"
|
||||
///
|
||||
///
|
||||
/// ## optional. can use these accounts with deployments / builds.
|
||||
/// [docker_accounts]
|
||||
/// docker_username1 = "docker_token1"
|
||||
|
||||
@@ -24,8 +24,8 @@ pub type DeploymentListItem =
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeploymentListItemInfo {
|
||||
/// The state of the docker container.
|
||||
pub state: DockerContainerState,
|
||||
/// The state of the deployment / underlying docker container.
|
||||
pub state: DeploymentState,
|
||||
/// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.)
|
||||
pub status: Option<String>,
|
||||
/// The image attached to the deployment.
|
||||
@@ -237,7 +237,7 @@ pub struct ContainerSummary {
|
||||
/// The docker labels on the container.
|
||||
pub labels: HashMap<String, String>,
|
||||
/// The state of the container, like `running` or `not_deployed`
|
||||
pub state: DockerContainerState,
|
||||
pub state: DeploymentState,
|
||||
/// The status string of the docker container.
|
||||
pub status: Option<String>,
|
||||
}
|
||||
@@ -261,6 +261,12 @@ pub struct DockerContainerStats {
|
||||
pub pids: String,
|
||||
}
|
||||
|
||||
/// Variants de/serialized from/to snake_case.
|
||||
///
|
||||
/// Eg.
|
||||
/// - NotDeployed -> not_deployed
|
||||
/// - Restarting -> restarting
|
||||
/// - Running -> running.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
@@ -277,7 +283,7 @@ pub struct DockerContainerStats {
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum DockerContainerState {
|
||||
pub enum DeploymentState {
|
||||
#[default]
|
||||
Unknown,
|
||||
NotDeployed,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -209,6 +211,7 @@ pub struct CloneArgs {
|
||||
pub name: String,
|
||||
pub repo: Option<String>,
|
||||
pub branch: Option<String>,
|
||||
pub commit: Option<String>,
|
||||
pub on_clone: Option<SystemCommand>,
|
||||
pub on_pull: Option<SystemCommand>,
|
||||
pub github_account: Option<String>,
|
||||
@@ -220,6 +223,7 @@ impl From<&self::build::Build> for CloneArgs {
|
||||
name: build.name.clone(),
|
||||
repo: optional_string(&build.config.repo),
|
||||
branch: optional_string(&build.config.branch),
|
||||
commit: optional_string(&build.config.commit),
|
||||
on_clone: build.config.pre_build.clone().into_option(),
|
||||
on_pull: None,
|
||||
github_account: optional_string(&build.config.github_account),
|
||||
@@ -233,6 +237,7 @@ impl From<&self::repo::Repo> for CloneArgs {
|
||||
name: repo.name.clone(),
|
||||
repo: optional_string(&repo.config.repo),
|
||||
branch: optional_string(&repo.config.branch),
|
||||
commit: optional_string(&repo.config.commit),
|
||||
on_clone: repo.config.on_clone.clone().into_option(),
|
||||
on_pull: repo.config.on_pull.clone().into_option(),
|
||||
github_account: optional_string(&repo.config.github_account),
|
||||
@@ -323,6 +328,16 @@ pub enum Timelength {
|
||||
ThirtyDays,
|
||||
}
|
||||
|
||||
impl TryInto<async_timing_util::Timelength> for Timelength {
|
||||
type Error = anyhow::Error;
|
||||
fn try_into(
|
||||
self,
|
||||
) -> Result<async_timing_util::Timelength, Self::Error> {
|
||||
async_timing_util::Timelength::from_str(&self.to_string())
|
||||
.context("failed to parse timelength?")
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use derive_builder::Builder;
|
||||
use derive_default_builder::DefaultBuilder;
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use partial_derive2::Partial;
|
||||
@@ -29,16 +30,28 @@ pub type Procedure = Resource<ProcedureConfig, ()>;
|
||||
pub type _PartialProcedureConfig = PartialProcedureConfig;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, Partial)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, Partial, Builder)]
|
||||
#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct ProcedureConfig {
|
||||
/// Whether executions in the procedure runs sequentially or in parallel.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub procedure_type: ProcedureType,
|
||||
/// The executions to be run by the procedure.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub executions: Vec<EnabledExecution>,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
#[partial_default(default_webhook_enabled())]
|
||||
pub webhook_enabled: bool,
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -3,6 +3,7 @@ use derive_default_builder::DefaultBuilder;
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::I64;
|
||||
@@ -18,9 +19,36 @@ pub type RepoListItem = ResourceListItem<RepoListItemInfo>;
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct RepoListItemInfo {
|
||||
/// Repo last cloned / pulled timestamp in ms.
|
||||
pub last_pulled_at: I64,
|
||||
/// The configured github repo
|
||||
pub repo: String,
|
||||
/// The configured branch
|
||||
pub branch: String,
|
||||
/// The repo state
|
||||
pub state: RepoState,
|
||||
/// If the repo is cloned, will be the latest short commit hash.
|
||||
pub latest_hash: Option<String>,
|
||||
/// If the repo is cloned, will be the latest commit message.
|
||||
pub latest_message: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
|
||||
)]
|
||||
pub enum RepoState {
|
||||
/// Unknown case
|
||||
#[default]
|
||||
Unknown,
|
||||
/// Last clone / pull successful (or never cloned)
|
||||
Ok,
|
||||
/// Last clone / pull failed
|
||||
Failed,
|
||||
/// Currently cloning
|
||||
Cloning,
|
||||
/// Currently pullling
|
||||
Pulling,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -29,6 +57,7 @@ pub type Repo = Resource<RepoConfig, RepoInfo>;
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct RepoInfo {
|
||||
/// When repo was last pulled
|
||||
pub last_pulled_at: I64,
|
||||
}
|
||||
|
||||
@@ -57,6 +86,11 @@ pub struct RepoConfig {
|
||||
#[partial_default(default_branch())]
|
||||
pub branch: String,
|
||||
|
||||
/// Optionally set a specific commit hash.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub commit: String,
|
||||
|
||||
/// The github account to use to clone.
|
||||
/// It must be available in the server's periphery config.
|
||||
#[serde(default)]
|
||||
@@ -74,6 +108,12 @@ pub struct RepoConfig {
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub on_pull: SystemCommand,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
#[partial_default(default_webhook_enabled())]
|
||||
pub webhook_enabled: bool,
|
||||
}
|
||||
|
||||
impl RepoConfig {
|
||||
@@ -86,10 +126,16 @@ fn default_branch() -> String {
|
||||
String::from("main")
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct RepoActionState {
|
||||
/// Whether repo currently cloning
|
||||
pub cloning: bool,
|
||||
/// Whether repo currently pulling
|
||||
pub pulling: bool,
|
||||
}
|
||||
|
||||
@@ -101,6 +147,7 @@ pub type RepoQuery = ResourceQuery<RepoQuerySpecifics>;
|
||||
Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,
|
||||
)]
|
||||
pub struct RepoQuerySpecifics {
|
||||
/// Filter builds by their repo.
|
||||
pub repos: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ pub type ServerListItem = ResourceListItem<ServerListItemInfo>;
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ServerListItemInfo {
|
||||
/// The server's status.
|
||||
pub status: ServerStatus,
|
||||
/// The server's state.
|
||||
pub state: ServerState,
|
||||
/// Region of the server.
|
||||
pub region: String,
|
||||
/// Whether server is configured to send unreachable alerts.
|
||||
@@ -208,7 +208,7 @@ pub struct ServerActionState {
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
pub enum ServerStatus {
|
||||
pub enum ServerState {
|
||||
/// Server is unreachable.
|
||||
#[default]
|
||||
NotOk,
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export type ReadResponses = {
|
||||
GetServersSummary: Types.GetServersSummaryResponse;
|
||||
GetServer: Types.GetServerResponse;
|
||||
ListServers: Types.ListServersResponse;
|
||||
GetServerStatus: Types.GetServerStatusResponse;
|
||||
GetServerState: Types.GetServerStateResponse;
|
||||
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
|
||||
GetSystemInformation: Types.GetSystemInformationResponse;
|
||||
GetDockerContainers: Types.GetDockerContainersResponse;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Generated by typeshare 1.7.0
|
||||
Generated by typeshare 1.9.2
|
||||
*/
|
||||
|
||||
/** JSON containing an authentication token. */
|
||||
@@ -155,9 +155,9 @@ export type AlertData =
|
||||
/** The server name */
|
||||
server_name: string;
|
||||
/** The previous container state */
|
||||
from: DockerContainerState;
|
||||
from: DeploymentState;
|
||||
/** The current container state */
|
||||
to: DockerContainerState;
|
||||
to: DeploymentState;
|
||||
}}
|
||||
/** An AWS builder failed to terminate. */
|
||||
| { type: "AwsBuilderTerminationFailed", data: {
|
||||
@@ -285,6 +285,8 @@ export interface BuildConfig {
|
||||
repo?: string;
|
||||
/** The branch of the repo. */
|
||||
branch: string;
|
||||
/** Optionally set a specific commit hash. */
|
||||
commit?: string;
|
||||
/**
|
||||
* The github account used to clone (used to access private repos).
|
||||
* Empty string is public clone (only public repos).
|
||||
@@ -317,6 +319,8 @@ export interface BuildConfig {
|
||||
extra_args?: string[];
|
||||
/** Whether to use buildx to build (eg `docker buildx build ...`) */
|
||||
use_buildx?: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BuildInfo {
|
||||
@@ -327,6 +331,17 @@ export type Build = Resource<BuildConfig, BuildInfo>;
|
||||
|
||||
export type GetBuildResponse = Build;
|
||||
|
||||
export enum BuildState {
|
||||
/** Last build successful (or never built) */
|
||||
Ok = "Ok",
|
||||
/** Last build failed */
|
||||
Failed = "Failed",
|
||||
/** Currently building */
|
||||
Building = "Building",
|
||||
/** Other case */
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface BuildListItemInfo {
|
||||
/** Unix timestamp in milliseconds of last build */
|
||||
last_built_at: I64;
|
||||
@@ -336,6 +351,8 @@ export interface BuildListItemInfo {
|
||||
repo: string;
|
||||
/** The branch of the repo */
|
||||
branch: string;
|
||||
/** State of the build. Reflects whether most recent build successful. */
|
||||
state: BuildState;
|
||||
}
|
||||
|
||||
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
|
||||
@@ -488,7 +505,15 @@ export type Deployment = Resource<DeploymentConfig, undefined>;
|
||||
|
||||
export type GetDeploymentResponse = Deployment;
|
||||
|
||||
export enum DockerContainerState {
|
||||
/**
|
||||
* Variants de/serialized from/to snake_case.
|
||||
*
|
||||
* Eg.
|
||||
* - NotDeployed -> not_deployed
|
||||
* - Restarting -> restarting
|
||||
* - Running -> running.
|
||||
*/
|
||||
export enum DeploymentState {
|
||||
Unknown = "unknown",
|
||||
NotDeployed = "not_deployed",
|
||||
Created = "created",
|
||||
@@ -501,8 +526,8 @@ export enum DockerContainerState {
|
||||
}
|
||||
|
||||
export interface DeploymentListItemInfo {
|
||||
/** The state of the docker container. */
|
||||
state: DockerContainerState;
|
||||
/** The state of the deployment / underlying docker container. */
|
||||
state: DeploymentState;
|
||||
/** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */
|
||||
status?: string;
|
||||
/** The image attached to the deployment. */
|
||||
@@ -628,6 +653,8 @@ export interface ProcedureConfig {
|
||||
procedure_type?: ProcedureType;
|
||||
/** The executions to be run by the procedure. */
|
||||
executions?: EnabledExecution[];
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
}
|
||||
|
||||
export type Procedure = Resource<ProcedureConfig, undefined>;
|
||||
@@ -655,6 +682,8 @@ export interface RepoConfig {
|
||||
repo?: string;
|
||||
/** The repo branch. */
|
||||
branch: string;
|
||||
/** Optionally set a specific commit hash. */
|
||||
commit?: string;
|
||||
/**
|
||||
* The github account to use to clone.
|
||||
* It must be available in the server's periphery config.
|
||||
@@ -670,9 +699,12 @@ export interface RepoConfig {
|
||||
* The path is relative to the root of the repo.
|
||||
*/
|
||||
on_pull?: SystemCommand;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RepoInfo {
|
||||
/** When repo was last pulled */
|
||||
last_pulled_at: I64;
|
||||
}
|
||||
|
||||
@@ -680,10 +712,32 @@ export type Repo = Resource<RepoConfig, RepoInfo>;
|
||||
|
||||
export type GetRepoResponse = Repo;
|
||||
|
||||
export enum RepoState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
/** Last clone / pull successful (or never cloned) */
|
||||
Ok = "Ok",
|
||||
/** Last clone / pull failed */
|
||||
Failed = "Failed",
|
||||
/** Currently cloning */
|
||||
Cloning = "Cloning",
|
||||
/** Currently pullling */
|
||||
Pulling = "Pulling",
|
||||
}
|
||||
|
||||
export interface RepoListItemInfo {
|
||||
/** Repo last cloned / pulled timestamp in ms. */
|
||||
last_pulled_at: I64;
|
||||
/** The configured github repo */
|
||||
repo: string;
|
||||
/** The configured branch */
|
||||
branch: string;
|
||||
/** The repo state */
|
||||
state: RepoState;
|
||||
/** If the repo is cloned, will be the latest short commit hash. */
|
||||
latest_hash?: string;
|
||||
/** If the repo is cloned, will be the latest commit message. */
|
||||
latest_message?: string;
|
||||
}
|
||||
|
||||
export type RepoListItem = ResourceListItem<RepoListItemInfo>;
|
||||
@@ -691,7 +745,9 @@ export type RepoListItem = ResourceListItem<RepoListItemInfo>;
|
||||
export type ListReposResponse = RepoListItem[];
|
||||
|
||||
export interface RepoActionState {
|
||||
/** Whether repo currently cloning */
|
||||
cloning: boolean;
|
||||
/** Whether repo currently pulling */
|
||||
pulling: boolean;
|
||||
}
|
||||
|
||||
@@ -749,7 +805,7 @@ export type Server = Resource<ServerConfig, undefined>;
|
||||
|
||||
export type GetServerResponse = Server;
|
||||
|
||||
export enum ServerStatus {
|
||||
export enum ServerState {
|
||||
/** Server is unreachable. */
|
||||
NotOk = "NotOk",
|
||||
/** Server health check passing. */
|
||||
@@ -759,8 +815,8 @@ export enum ServerStatus {
|
||||
}
|
||||
|
||||
export interface ServerListItemInfo {
|
||||
/** The server's status. */
|
||||
status: ServerStatus;
|
||||
/** The server's state. */
|
||||
state: ServerState;
|
||||
/** Region of the server. */
|
||||
region: string;
|
||||
/** Whether server is configured to send unreachable alerts. */
|
||||
@@ -877,7 +933,7 @@ export interface ContainerSummary {
|
||||
/** The docker labels on the container. */
|
||||
labels: Record<string, string>;
|
||||
/** The state of the container, like `running` or `not_deployed` */
|
||||
state: DockerContainerState;
|
||||
state: DeploymentState;
|
||||
/** The status string of the docker container. */
|
||||
status?: string;
|
||||
}
|
||||
@@ -1266,6 +1322,7 @@ export type ProcedureQuery = ResourceQuery<ProcedureQuerySpecifics>;
|
||||
export type _PartialRepoConfig = Partial<RepoConfig>;
|
||||
|
||||
export interface RepoQuerySpecifics {
|
||||
/** Filter builds by their repo. */
|
||||
repos: string[];
|
||||
}
|
||||
|
||||
@@ -1750,7 +1807,7 @@ export interface GetDeploymentContainer {
|
||||
|
||||
/** Response for [GetDeploymentContainer]. */
|
||||
export interface GetDeploymentContainerResponse {
|
||||
state: DockerContainerState;
|
||||
state: DeploymentState;
|
||||
container?: ContainerSummary;
|
||||
}
|
||||
|
||||
@@ -1992,16 +2049,16 @@ export interface ListServers {
|
||||
query?: ServerQuery;
|
||||
}
|
||||
|
||||
/** Get the status of the target server. Response: [GetServerStatusResponse]. */
|
||||
export interface GetServerStatus {
|
||||
/** Get the state of the target server. Response: [GetServerStateResponse]. */
|
||||
export interface GetServerState {
|
||||
/** Id or name */
|
||||
server: string;
|
||||
}
|
||||
|
||||
/** The status for [GetServerStatus]. */
|
||||
export interface GetServerStatusResponse {
|
||||
/** The response for [GetServerState]. */
|
||||
export interface GetServerStateResponse {
|
||||
/** The server status. */
|
||||
status: ServerStatus;
|
||||
status: ServerState;
|
||||
}
|
||||
|
||||
/** Get current action state for the servers. Response: [ServerActionState]. */
|
||||
@@ -3016,6 +3073,7 @@ export interface CloneArgs {
|
||||
name: string;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
on_clone?: SystemCommand;
|
||||
on_pull?: SystemCommand;
|
||||
github_account?: string;
|
||||
@@ -3141,7 +3199,7 @@ export type ReadRequest =
|
||||
| { type: "GetServersSummary", params: GetServersSummary }
|
||||
| { type: "GetServer", params: GetServer }
|
||||
| { type: "ListServers", params: ListServers }
|
||||
| { type: "GetServerStatus", params: GetServerStatus }
|
||||
| { type: "GetServerState", params: GetServerState }
|
||||
| { type: "GetPeripheryVersion", params: GetPeripheryVersion }
|
||||
| { type: "GetDockerContainers", params: GetDockerContainers }
|
||||
| { type: "GetDockerImages", params: GetDockerImages }
|
||||
|
||||
@@ -4,6 +4,18 @@ use monitor_client::entities::{
|
||||
use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
|
||||
#[response(GetLatestCommitResponse)]
|
||||
pub struct GetLatestCommit {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetLatestCommitResponse {
|
||||
pub hash: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Request)]
|
||||
#[response(Vec<Log>)]
|
||||
pub struct CloneRepo {
|
||||
@@ -19,6 +31,7 @@ pub struct CloneRepo {
|
||||
pub struct PullRepo {
|
||||
pub name: String,
|
||||
pub branch: Option<String>,
|
||||
pub commit: Option<String>,
|
||||
pub on_pull: Option<SystemCommand>,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<title>Monitor</title>
|
||||
</head>
|
||||
<body class="min-h-screen pb-12">
|
||||
<body class="min-h-screen">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
## Install, build, and setup yarn link on client.
|
||||
yarn build-client
|
||||
|
||||
## Link, install, build frontend
|
||||
yarn link @monitor/client && yarn && yarn build
|
||||
@@ -19,43 +19,46 @@
|
||||
"@radix-ui/react-popover": "1.0.7",
|
||||
"@radix-ui/react-progress": "1.0.3",
|
||||
"@radix-ui/react-select": "2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-switch": "1.0.3",
|
||||
"@radix-ui/react-tabs": "1.0.4",
|
||||
"@radix-ui/react-toast": "1.1.5",
|
||||
"@tanstack/react-query": "5.29.2",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@tanstack/react-query": "5.35.1",
|
||||
"@tanstack/react-table": "8.16.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"class-variance-authority": "0.7.0",
|
||||
"clsx": "2.1.0",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"jotai": "2.8.0",
|
||||
"lightweight-charts": "4.1.3",
|
||||
"lucide-react": "0.371.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"lightweight-charts": "4.1.4",
|
||||
"lucide-react": "0.378.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-minimal-pie-chart": "8.4.0",
|
||||
"react-router-dom": "6.22.3",
|
||||
"reconnecting-websocket": "4.4.0",
|
||||
"react-router-dom": "6.23.0",
|
||||
"sanitize-html": "2.13.0",
|
||||
"tailwind-merge": "2.2.2",
|
||||
"tailwind-merge": "2.3.0",
|
||||
"tailwindcss-animate": "1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.0",
|
||||
"@typescript-eslint/parser": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.8.0",
|
||||
"@typescript-eslint/parser": "7.8.0",
|
||||
"@vitejs/plugin-react": "4.2.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "9.0.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-refresh": "0.4.6",
|
||||
"eslint": "9.2.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"postcss": "8.4.38",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.10",
|
||||
"vite": "5.2.11",
|
||||
"vite-tsconfig-paths": "4.3.2"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export const ConfigLayout = <
|
||||
onConfirm,
|
||||
onReset,
|
||||
selector,
|
||||
titleOther,
|
||||
}: {
|
||||
config: Partial<T>;
|
||||
children: ReactNode;
|
||||
@@ -37,33 +38,40 @@ export const ConfigLayout = <
|
||||
onConfirm: () => void;
|
||||
onReset: () => void;
|
||||
selector?: ReactNode;
|
||||
}) => (
|
||||
<Section
|
||||
title="Config"
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{selector}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onReset}
|
||||
disabled={disabled || (config ? !Object.keys(config).length : true)}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</Button>
|
||||
{Object.keys(config).length ? (
|
||||
<ConfirmUpdate
|
||||
content={JSON.stringify(config, null, 2)}
|
||||
onConfirm={onConfirm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Section>
|
||||
);
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
const titleProps = titleOther
|
||||
? { titleOther }
|
||||
: { title: "Config", icon: <Settings className="w-4 h-4" /> };
|
||||
return (
|
||||
<Section
|
||||
{...titleProps}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{selector}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onReset}
|
||||
disabled={disabled || (config ? !Object.keys(config).length : true)}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</Button>
|
||||
{Object.keys(config).length ? (
|
||||
<ConfirmUpdate
|
||||
content={JSON.stringify(config, null, 2)}
|
||||
onConfirm={onConfirm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
type PrimitiveConfigArgs = { placeholder: string };
|
||||
|
||||
export const Config = <T,>({
|
||||
config,
|
||||
@@ -73,6 +81,7 @@ export const Config = <T,>({
|
||||
onSave,
|
||||
components,
|
||||
selector,
|
||||
titleOther,
|
||||
}: {
|
||||
config: T;
|
||||
update: Partial<T>;
|
||||
@@ -80,6 +89,7 @@ export const Config = <T,>({
|
||||
set: React.Dispatch<SetStateAction<Partial<T>>>;
|
||||
onSave: () => Promise<void>;
|
||||
selector?: ReactNode;
|
||||
titleOther?: ReactNode;
|
||||
components: Record<
|
||||
string,
|
||||
Record<
|
||||
@@ -87,6 +97,7 @@ export const Config = <T,>({
|
||||
{
|
||||
[K in keyof Partial<T>]:
|
||||
| boolean
|
||||
| PrimitiveConfigArgs
|
||||
| ((value: T[K], set: (value: Partial<T>) => void) => ReactNode);
|
||||
}
|
||||
>
|
||||
@@ -96,6 +107,7 @@ export const Config = <T,>({
|
||||
|
||||
return (
|
||||
<ConfigLayout
|
||||
titleOther={titleOther}
|
||||
config={update}
|
||||
disabled={disabled}
|
||||
onConfirm={async () => {
|
||||
@@ -144,7 +156,7 @@ export const Config = <T,>({
|
||||
<CardTitle>{snake_case_to_upper_space_case(k)}</CardTitle>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className="flex flex-col gap-4 mt-4">
|
||||
<CardContent className="flex flex-col gap-2 mt-4">
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
@@ -176,6 +188,7 @@ export const ConfigAgain = <
|
||||
components: Partial<{
|
||||
[K in keyof T extends string ? keyof T : never]:
|
||||
| boolean
|
||||
| PrimitiveConfigArgs
|
||||
| ((value: T[K], set: (value: Partial<T>) => void) => ReactNode);
|
||||
}>;
|
||||
set: (value: Partial<T>) => void;
|
||||
@@ -185,7 +198,12 @@ export const ConfigAgain = <
|
||||
{keys(components).map((key) => {
|
||||
const component = components[key];
|
||||
const value = update[key] ?? config[key];
|
||||
if (component === true) {
|
||||
if (typeof component === "function") {
|
||||
return (
|
||||
<Fragment key={key.toString()}>{component?.(value, set)}</Fragment>
|
||||
);
|
||||
} else if (typeof component === "object" || component === true) {
|
||||
const args = (typeof component === "object") ? component as PrimitiveConfigArgs : undefined;
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return (
|
||||
@@ -195,6 +213,7 @@ export const ConfigAgain = <
|
||||
value={value}
|
||||
onChange={(value) => set({ [key]: value } as Partial<T>)}
|
||||
disabled={disabled}
|
||||
placeholder={args?.placeholder}
|
||||
/>
|
||||
);
|
||||
case "number":
|
||||
@@ -207,6 +226,7 @@ export const ConfigAgain = <
|
||||
set({ [key]: Number(value) } as Partial<T>)
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder={args?.placeholder}
|
||||
/>
|
||||
);
|
||||
case "boolean":
|
||||
@@ -225,9 +245,7 @@ export const ConfigAgain = <
|
||||
} else if (component === false) {
|
||||
return <Fragment key={key.toString()} />;
|
||||
}
|
||||
return (
|
||||
<Fragment key={key.toString()}>{component?.(value, set)}</Fragment>
|
||||
);
|
||||
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@ui/dialog";
|
||||
import { snake_case_to_upper_space_case } from "@lib/formatting";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { ConfirmButton, TextUpdateMenu } from "@components/util";
|
||||
|
||||
export const ConfigItem = ({
|
||||
label,
|
||||
@@ -34,15 +34,18 @@ export const ConfigItem = ({
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between items-center border-b pb-2 min-h-[60px] last:border-none last:pb-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label && <div>{snake_case_to_upper_space_case(label)}</div>}
|
||||
{children}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-between items-center min-h-[60px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label && <div>{snake_case_to_upper_space_case(label)}</div>}
|
||||
{children}
|
||||
</div>
|
||||
<div className="w-full h-0 border-b last:hidden" />
|
||||
</>
|
||||
);
|
||||
|
||||
export const ConfigInput = ({
|
||||
@@ -224,7 +227,10 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
disabled: boolean;
|
||||
set: (update: Partial<T>) => void;
|
||||
}) => (
|
||||
<ConfigItem label={field as string} className="items-start">
|
||||
<ConfigItem
|
||||
label={field as string}
|
||||
className={values.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{values.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
@@ -239,7 +245,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
set({
|
||||
[field]: [...values.filter((_, idx) => idx !== i)],
|
||||
@@ -254,7 +260,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() => set({ [field]: [...values, ""] } as Partial<T>)}
|
||||
>
|
||||
Add {snake_case_to_upper_space_case(field as string).slice(0, -1)}
|
||||
@@ -319,27 +325,23 @@ export const SystemCommand = ({
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label={label} className="items-start">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-4 items-center justify-end">
|
||||
Path:
|
||||
<Input
|
||||
placeholder="command working directory"
|
||||
value={value?.path}
|
||||
className="w-[300px]"
|
||||
onChange={(e) => set({ ...(value || {}), path: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center justify-end">
|
||||
Command:
|
||||
<Input
|
||||
placeholder="shell command"
|
||||
value={value?.command}
|
||||
className="w-[300px]"
|
||||
onChange={(e) => set({ ...(value || {}), command: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-y-4 gap-x-8 grid-cols-[auto_1fr] grid-rows-1 items-center">
|
||||
Path:
|
||||
<Input
|
||||
placeholder="Command working directory"
|
||||
value={value?.path}
|
||||
className="w-full"
|
||||
onChange={(e) => set({ ...(value || {}), path: e.target.value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
Command:
|
||||
<TextUpdateMenu
|
||||
title="Update Command"
|
||||
placeholder="Set shell command"
|
||||
value={value?.command}
|
||||
onUpdate={(command) => set({ ...(value || {}), command })}
|
||||
triggerClassName="w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
@@ -16,19 +16,29 @@ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@ui/card";
|
||||
import { ResourceTags } from "./tags";
|
||||
import { Topbar } from "./topbar";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
|
||||
export const Layout = () => {
|
||||
return (
|
||||
<>
|
||||
<Topbar />
|
||||
<Outlet />
|
||||
<div className="flex gap-2">
|
||||
<Sidebar />
|
||||
<div className="w-full h-[calc(100vh-70px)] overflow-y-auto">
|
||||
<div className="pb-24">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
title?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
titleRight?: ReactNode;
|
||||
titleOther?: ReactNode;
|
||||
children?: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
@@ -36,16 +46,19 @@ interface PageProps {
|
||||
|
||||
export const Page = ({
|
||||
title,
|
||||
icon,
|
||||
titleRight,
|
||||
titleOther,
|
||||
subtitle,
|
||||
actions,
|
||||
children,
|
||||
}: PageProps) => (
|
||||
<div className="flex flex-col gap-12 container py-8">
|
||||
{(title || subtitle || actions) && (
|
||||
<div className="flex flex-col gap-10 container py-8">
|
||||
{(title || icon || subtitle || actions) && (
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:gap-0 lg:items-start justify-between">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-4 items-center">
|
||||
{icon}
|
||||
<h1 className="text-4xl">{title}</h1>
|
||||
{titleRight}
|
||||
</div>
|
||||
@@ -54,26 +67,28 @@ export const Page = ({
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{titleOther}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SectionProps {
|
||||
title?: ReactNode;
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
titleOther?: ReactNode;
|
||||
children?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const Section = ({ title, icon, actions, children }: SectionProps) => (
|
||||
export const Section = ({ title, icon, titleOther, actions, children }: SectionProps) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start justify-between">
|
||||
{(title || icon) && (
|
||||
{(title || icon) ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
{icon}
|
||||
<h2 className="text-xl">{title}</h2>
|
||||
{title && <h2 className="text-xl">{title}</h2>}
|
||||
</div>
|
||||
)}
|
||||
) : titleOther}
|
||||
{actions}
|
||||
</div>
|
||||
{children}
|
||||
|
||||
@@ -94,6 +94,7 @@ export const AlerterComponents: RequiredResourceComponents = {
|
||||
name: (id) => useAlerter(id)?.name,
|
||||
|
||||
Icon: () => <AlarmClock className="w-4 h-4" />,
|
||||
BigIcon: () => <AlarmClock className="w-8 h-8" />,
|
||||
|
||||
Status: {},
|
||||
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
|
||||
export const AlerterTable = ({ search }: { search?: string }) => {
|
||||
const tags = useTagsFilter();
|
||||
const alerters = useRead("ListAlerters", {}).data;
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(alerters, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="alerters"
|
||||
data={
|
||||
alerters?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -98,8 +98,9 @@ export const BuildConfig = ({ id }: { id: string }) => {
|
||||
),
|
||||
},
|
||||
git: {
|
||||
repo: true,
|
||||
branch: true,
|
||||
repo: { placeholder: "Enter repo" },
|
||||
branch: { placeholder: "Enter branch" },
|
||||
commit: { placeholder: "Enter specific commit hash. Optional." },
|
||||
github_account: (account, set) => (
|
||||
<AccountSelector
|
||||
id={update.builder_id ?? config.builder_id ?? undefined}
|
||||
@@ -161,6 +162,7 @@ export const BuildConfig = ({ id }: { id: string }) => {
|
||||
<CopyGithubWebhook path={`/build/${id}`} />
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: true,
|
||||
},
|
||||
},
|
||||
"Build Args": {
|
||||
@@ -213,7 +215,10 @@ const ExtraArgs = ({
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label="Extra Args" className="items-start">
|
||||
<ConfigItem
|
||||
label="Extra Args"
|
||||
className={args.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{args.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { fill_color_class_by_intention } from "@lib/color";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Hammer } from "lucide-react";
|
||||
|
||||
export const IconStrictId = ({ id }: { id: string }) => {
|
||||
const building = useRead(
|
||||
"GetBuildActionState",
|
||||
{ build: id },
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.building;
|
||||
const className = building
|
||||
? "w-4 h-4 " + fill_color_class_by_intention("Good")
|
||||
: "w-4 h-4";
|
||||
return <Hammer className={className} />;
|
||||
};
|
||||
@@ -8,11 +8,23 @@ import { BuildTable } from "./table";
|
||||
import { DeleteResource, NewResource } from "../common";
|
||||
import { DeploymentTable } from "../deployment/table";
|
||||
import { RunBuild } from "./actions";
|
||||
import { IconStrictId } from "./icon";
|
||||
import {
|
||||
bg_color_class_by_intention,
|
||||
build_state_intention,
|
||||
fill_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { Card, CardHeader } from "@ui/card";
|
||||
import { cn } from "@lib/utils";
|
||||
|
||||
const useBuild = (id?: string) =>
|
||||
useRead("ListBuilds", {}).data?.find((d) => d.id === id);
|
||||
|
||||
const BuildIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useBuild(id)?.info.state;
|
||||
const color = fill_color_class_by_intention(build_state_intention(state));
|
||||
return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
export const BuildComponents: RequiredResourceComponents = {
|
||||
Dashboard: BuildChart,
|
||||
|
||||
@@ -23,12 +35,20 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
Name: ({ id }) => <>{useBuild(id)?.name}</>,
|
||||
name: (id) => useBuild(id)?.name,
|
||||
|
||||
Icon: ({ id }) => {
|
||||
if (id) return <IconStrictId id={id} />;
|
||||
else return <Hammer className="w-4 h-4" />;
|
||||
},
|
||||
Icon: ({ id }) => <BuildIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <BuildIcon id={id} size={8} />,
|
||||
|
||||
Status: {},
|
||||
Status: {
|
||||
State: ({ id }) => {
|
||||
let state = useBuild(id)?.info.state;
|
||||
const color = bg_color_class_by_intention(build_state_intention(state));
|
||||
return (
|
||||
<Card className={cn("w-fit", color)}>
|
||||
<CardHeader className="py-0 px-2">{state}</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Info: {
|
||||
Repo: ({ id }) => {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { fmt_date_with_minutes, fmt_version } from "@lib/formatting";
|
||||
import { fmt_version } from "@lib/formatting";
|
||||
import { ResourceLink } from "../common";
|
||||
import { BuildComponents } from ".";
|
||||
|
||||
export const BuildTable = ({ search }: { search?: string }) => {
|
||||
const builds = useRead("ListBuilds", {}).data;
|
||||
const tags = useTagsFilter();
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(builds, search)
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builds"
|
||||
data={
|
||||
builds?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -39,17 +31,13 @@ export const BuildTable = ({ search }: { search?: string }) => {
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "info.last_built_at",
|
||||
accessorKey: "info.state",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Last Built" />
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<BuildComponents.Status.State id={row.original.id} />
|
||||
),
|
||||
accessorFn: ({ info: { last_built_at } }) => {
|
||||
if (last_built_at > 0) {
|
||||
return fmt_date_with_minutes(new Date(last_built_at));
|
||||
} else {
|
||||
return "never";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
|
||||
@@ -106,6 +106,7 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
name: (id) => useBuilder(id)?.name,
|
||||
|
||||
Icon: () => <Factory className="w-4 h-4" />,
|
||||
BigIcon: () => <Factory className="w-8 h-8" />,
|
||||
|
||||
Status: {},
|
||||
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { BuilderInstanceType } from ".";
|
||||
|
||||
export const BuilderTable = ({ search }: { search?: string }) => {
|
||||
const tags = useTagsFilter();
|
||||
const builders = useRead("ListBuilders", {}).data;
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(builders, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="builders"
|
||||
data={
|
||||
builders?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -284,7 +284,10 @@ export const LabelsConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig | Types.BuildConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Labels" className="items-start">
|
||||
<ConfigItem
|
||||
label="Labels"
|
||||
className={labels.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
values={labels}
|
||||
@@ -320,8 +323,27 @@ export const CopyGithubWebhook = ({
|
||||
const url = base_url + "/listener/github" + path;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input className="w-[400px] max-w-[70vw]" value={url} disabled />
|
||||
<Input className="w-[400px] max-w-[70vw]" value={url} readOnly />
|
||||
<CopyButton content={url} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServerSelector = ({
|
||||
selected,
|
||||
set,
|
||||
disabled,
|
||||
}: {
|
||||
selected: string | undefined;
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Server">
|
||||
<ResourceSelector
|
||||
type="Server"
|
||||
selected={selected}
|
||||
onSelect={(server_id) => set({ server_id })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@ui/select";
|
||||
import { DockerContainerState } from "@monitor/client/dist/types";
|
||||
|
||||
interface DeploymentId {
|
||||
id: string;
|
||||
@@ -48,8 +47,8 @@ export const DeployContainer = ({ id }: DeploymentId) => {
|
||||
if (!deployment) return null;
|
||||
|
||||
const deployed =
|
||||
deployment_item?.info.state !== DockerContainerState.NotDeployed &&
|
||||
deployment_item?.info.state !== DockerContainerState.Unknown;
|
||||
deployment_item?.info.state !== Types.DeploymentState.NotDeployed &&
|
||||
deployment_item?.info.state !== Types.DeploymentState.Unknown;
|
||||
|
||||
if (deployed) {
|
||||
return (
|
||||
@@ -161,15 +160,15 @@ export const StartOrStopContainer = ({ id }: DeploymentId) => {
|
||||
const state = deployment?.info.state;
|
||||
|
||||
if (
|
||||
state === Types.DockerContainerState.NotDeployed ||
|
||||
state === Types.DockerContainerState.Unknown
|
||||
state === Types.DeploymentState.NotDeployed ||
|
||||
state === Types.DeploymentState.Unknown
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
state === Types.DockerContainerState.Running ||
|
||||
state === Types.DockerContainerState.Restarting
|
||||
state === Types.DeploymentState.Running ||
|
||||
state === Types.DeploymentState.Restarting
|
||||
) {
|
||||
return <StopContainer id={id} />;
|
||||
}
|
||||
@@ -202,7 +201,7 @@ export const RemoveContainer = ({ id }: DeploymentId) => {
|
||||
const pending = isPending || removing;
|
||||
|
||||
if (!deployment) return null;
|
||||
if (state === Types.DockerContainerState.NotDeployed) return null;
|
||||
if (state === Types.DeploymentState.NotDeployed) return null;
|
||||
|
||||
return (
|
||||
<ActionWithDialog
|
||||
|
||||
@@ -25,7 +25,10 @@ export const ExtraArgs = ({
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label="Extra Args" className="items-start">
|
||||
<ConfigItem
|
||||
label="Extra Args"
|
||||
className={args.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{args.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
|
||||
@@ -10,7 +10,10 @@ export const PortsConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Ports" className="items-start">
|
||||
<ConfigItem
|
||||
label="Ports"
|
||||
className={ports.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
values={ports}
|
||||
|
||||
@@ -10,7 +10,10 @@ export const VolumesConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Volumes" className="items-start">
|
||||
<ConfigItem
|
||||
label="Volumes"
|
||||
className={volumes.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
inputClassName="w-[300px] max-w-full"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRead, useWrite } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { AccountSelector, ConfigItem } from "@components/config/util";
|
||||
import { ImageConfig } from "./components/image";
|
||||
import { RestartModeSelector } from "./components/restart";
|
||||
@@ -15,29 +15,10 @@ import {
|
||||
TermSignalLabels,
|
||||
TerminationTimeout,
|
||||
} from "./components/term-signal";
|
||||
import { LabelsConfig, ResourceSelector } from "@components/resources/common";
|
||||
import { LabelsConfig, ServerSelector } from "@components/resources/common";
|
||||
import { TextUpdateMenu } from "@components/util";
|
||||
|
||||
export const ServerSelector = ({
|
||||
selected,
|
||||
set,
|
||||
disabled,
|
||||
}: {
|
||||
selected: string | undefined;
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Server">
|
||||
<ResourceSelector
|
||||
type="Server"
|
||||
selected={selected}
|
||||
onSelect={(server_id) => set({ server_id })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
export const DeploymentConfig = ({ id }: { id: string }) => {
|
||||
export const DeploymentConfig = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Deployment", id },
|
||||
}).data;
|
||||
@@ -57,6 +38,7 @@ export const DeploymentConfig = ({ id }: { id: string }) => {
|
||||
|
||||
return (
|
||||
<Config
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
config={config}
|
||||
update={update}
|
||||
@@ -97,17 +79,6 @@ export const DeploymentConfig = ({ id }: { id: string }) => {
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
command: (value, set) => (
|
||||
<ConfigItem label="Command">
|
||||
<TextUpdateMenu
|
||||
title="Update Command"
|
||||
placeholder="Set Command"
|
||||
value={value}
|
||||
onUpdate={(command) => set({ command })}
|
||||
triggerClassName="min-w-[300px] max-w-[400px]"
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
network: (value, set) => (
|
||||
<NetworkModeSelector
|
||||
server_id={update.server_id ?? config.server_id}
|
||||
@@ -133,6 +104,17 @@ export const DeploymentConfig = ({ id }: { id: string }) => {
|
||||
extra_args: (value, set) => (
|
||||
<ExtraArgs args={value ?? []} set={set} disabled={disabled} />
|
||||
),
|
||||
command: (value, set) => (
|
||||
<ConfigItem label="Command">
|
||||
<TextUpdateMenu
|
||||
title="Update Command"
|
||||
placeholder="Set custom command"
|
||||
value={value}
|
||||
onUpdate={(command) => set({ command })}
|
||||
triggerClassName="min-w-[300px] max-w-[400px]"
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
},
|
||||
settings: {
|
||||
send_alerts: true,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RequiredResourceComponents } from "@types";
|
||||
import { HardDrive, Rocket } from "lucide-react";
|
||||
import { cn } from "@lib/utils";
|
||||
import { useServer } from "../server";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import {
|
||||
DeployContainer,
|
||||
StartOrStopContainer,
|
||||
@@ -24,12 +23,81 @@ import { DeploymentsChart } from "./dashboard";
|
||||
import { NewResource, ResourceLink } from "../common";
|
||||
import { Card, CardHeader } from "@ui/card";
|
||||
import { RunBuild } from "../build/actions";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
export const useDeployment = (id?: string) =>
|
||||
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(
|
||||
(d) => d.id === id
|
||||
);
|
||||
|
||||
const ConfigOrLog = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useAtom(configOrLog);
|
||||
const state = useDeployment(id)?.info.state;
|
||||
const logsDisabled =
|
||||
state === undefined ||
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed;
|
||||
return (
|
||||
<Tabs
|
||||
value={logsDisabled ? "Config" : view}
|
||||
onValueChange={setView}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<TabsContent value="Config">
|
||||
<DeploymentConfig
|
||||
id={id}
|
||||
titleOther={
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[70px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Log"
|
||||
className="w-[70px]"
|
||||
disabled={logsDisabled}
|
||||
>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="Log">
|
||||
<DeploymentLogs
|
||||
id={id}
|
||||
titleOther={
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger value="Config" className="w-[70px]">
|
||||
Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="Log"
|
||||
className="w-[70px]"
|
||||
disabled={logsDisabled}
|
||||
>
|
||||
Log
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const DeploymentIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useDeployment(id)?.info.state;
|
||||
const color = fill_color_class_by_intention(
|
||||
deployment_state_intention(state)
|
||||
);
|
||||
return <Rocket className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
export const DeploymentComponents: RequiredResourceComponents = {
|
||||
Dashboard: DeploymentsChart,
|
||||
|
||||
@@ -43,18 +111,13 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
Name: ({ id }) => <>{useDeployment(id)?.name}</>,
|
||||
name: (id) => useDeployment(id)?.name,
|
||||
|
||||
Icon: ({ id }) => {
|
||||
const state = useDeployment(id)?.info.state;
|
||||
const color = fill_color_class_by_intention(
|
||||
deployment_state_intention(state)
|
||||
);
|
||||
return <Rocket className={cn("w-4 h-4", state && color)} />;
|
||||
},
|
||||
Icon: ({ id }) => <DeploymentIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />,
|
||||
|
||||
Status: {
|
||||
State: ({ id }) => {
|
||||
const state =
|
||||
useDeployment(id)?.info.state ?? Types.DockerContainerState.Unknown;
|
||||
useDeployment(id)?.info.state ?? Types.DeploymentState.Unknown;
|
||||
const color = bg_color_class_by_intention(
|
||||
deployment_state_intention(state)
|
||||
);
|
||||
@@ -106,9 +169,9 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
RemoveContainer,
|
||||
},
|
||||
|
||||
Page: { DeploymentLogs },
|
||||
Page: {},
|
||||
|
||||
Config: DeploymentConfig,
|
||||
Config: ConfigOrLog,
|
||||
|
||||
DangerZone: ({ id }) => (
|
||||
<>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/tabs";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
TerminalSquare,
|
||||
AlertOctagon,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
X,
|
||||
AlertOctagon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useDeployment } from ".";
|
||||
import {
|
||||
Select,
|
||||
@@ -20,25 +18,26 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import Convert from "ansi-to-html";
|
||||
import { Input } from "@ui/input";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { logToHtml } from "@lib/utils";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@ui/toggle-group";
|
||||
|
||||
export const DeploymentLogs = ({ id }: { id: string }) => {
|
||||
export const DeploymentLogs = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
|
||||
const state = useDeployment(id)?.info.state;
|
||||
if (
|
||||
state === undefined ||
|
||||
state === Types.DockerContainerState.Unknown ||
|
||||
state === Types.DockerContainerState.NotDeployed
|
||||
state === Types.DeploymentState.Unknown ||
|
||||
state === Types.DeploymentState.NotDeployed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return <DeploymentLogsInner id={id} />;
|
||||
return <DeploymentLogsInner id={id} titleOther={titleOther} />;
|
||||
};
|
||||
|
||||
const DeploymentLogsInner = ({ id }: { id: string }) => {
|
||||
const DeploymentLogsInner = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
|
||||
const { toast } = useToast();
|
||||
const [stream, setStream] = useState("stdout");
|
||||
const [tail, set] = useState("100");
|
||||
const [terms, setTerms] = useState<string[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -61,88 +60,77 @@ const DeploymentLogsInner = ({ id }: { id: string }) => {
|
||||
|
||||
const { Log, refetch, stderr } = terms.length
|
||||
? SearchLogs(id, terms)
|
||||
: NoSearchLogs(id, tail);
|
||||
: NoSearchLogs(id, tail, stream);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="stdout">
|
||||
<Section
|
||||
title="Logs"
|
||||
icon={<TerminalSquare className="w-4 h-4" />}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{terms.map((term, index) => (
|
||||
<Button
|
||||
key={term}
|
||||
variant="destructive"
|
||||
onClick={() => setTerms(terms.filter((_, i) => i !== index))}
|
||||
className="flex gap-2 items-center py-0 px-2"
|
||||
>
|
||||
{term}
|
||||
<X className="w-4 h-h" />
|
||||
</Button>
|
||||
))}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Search Logs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onBlur={addTerm}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") addTerm();
|
||||
}}
|
||||
className="w-[300px]"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<TabsList>
|
||||
<TabsTrigger value="stdout">stdout</TabsTrigger>
|
||||
<TabsTrigger value="stderr">
|
||||
stderr
|
||||
{stderr && (
|
||||
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button variant="secondary" size="icon" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{terms.map((term, index) => (
|
||||
<Button
|
||||
key={term}
|
||||
variant="destructive"
|
||||
onClick={() => setTerms(terms.filter((_, i) => i !== index))}
|
||||
className="flex gap-2 items-center py-0 px-2"
|
||||
>
|
||||
{term}
|
||||
<X className="w-4 h-h" />
|
||||
</Button>
|
||||
<TailLengthSelector
|
||||
selected={tail}
|
||||
onSelect={set}
|
||||
disabled={search.length > 0}
|
||||
))}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Search Logs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onBlur={addTerm}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") addTerm();
|
||||
}}
|
||||
className="w-[300px]"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{Log}
|
||||
</Section>
|
||||
</Tabs>
|
||||
<ToggleGroup type="single" value={stream} onValueChange={setStream}>
|
||||
<ToggleGroupItem value="stdout">stdout</ToggleGroupItem>
|
||||
<ToggleGroupItem value="stderr">
|
||||
stderr
|
||||
{stderr && (
|
||||
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
|
||||
)}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Button variant="secondary" size="icon" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<TailLengthSelector
|
||||
selected={tail}
|
||||
onSelect={set}
|
||||
disabled={search.length > 0}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{Log}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const NoSearchLogs = (id: string, tail: string) => {
|
||||
const NoSearchLogs = (id: string, tail: string, stream: string) => {
|
||||
const { data: log, refetch } = useRead(
|
||||
"GetLog",
|
||||
{ deployment: id, tail: Number(tail) },
|
||||
{ refetchInterval: 30000 }
|
||||
);
|
||||
return {
|
||||
Log: (
|
||||
<>
|
||||
{["stdout", "stderr"].map((stream) => (
|
||||
<TabsContent key={stream} className="h-full relative" value={stream}>
|
||||
<Log log={log} stream={stream as "stdout" | "stderr"} />
|
||||
</TabsContent>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
Log: <Log log={log} stream={stream as "stdout" | "stderr"} />,
|
||||
refetch,
|
||||
stderr: !!log?.stderr,
|
||||
};
|
||||
@@ -221,20 +209,3 @@ const TailLengthSelector = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const convert = new Convert();
|
||||
/**
|
||||
* Converts the ansi colors in log to html.
|
||||
* sanitizes incoming log first for any eg. script tags.
|
||||
* @param log incoming log string
|
||||
*/
|
||||
const logToHtml = (log: string) => {
|
||||
if (!log) return "No log.";
|
||||
const sanitized = sanitizeHtml(log, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.filter(
|
||||
(tag) => tag !== "script"
|
||||
),
|
||||
allowedAttributes: sanitizeHtml.defaults.allowedAttributes,
|
||||
});
|
||||
return convert.toHtml(sanitized);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { Types } from "@monitor/client";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { ResourceLink } from "../common";
|
||||
import { DeploymentComponents } from ".";
|
||||
import { HardDrive } from "lucide-react";
|
||||
@@ -14,25 +14,16 @@ export const DeploymentTable = ({
|
||||
deployments: Types.DeploymentListItem[] | undefined;
|
||||
search?: string;
|
||||
}) => {
|
||||
const tags = useTagsFilter();
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const serverName = useCallback(
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
const filtered = useFilterResources(deployments, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="deployments"
|
||||
data={
|
||||
deployments?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -61,14 +52,14 @@ export const DeploymentTable = ({
|
||||
sortingFn: (a, b) => {
|
||||
const sa = serverName(a.original.info.server_id);
|
||||
const sb = serverName(b.original.info.server_id);
|
||||
|
||||
|
||||
if (!sa && !sb) return 0;
|
||||
if (!sa) return -1;
|
||||
if (!sb) return 1;
|
||||
|
||||
if (sa > sb) return 1;
|
||||
else if (sa < sb) return -1;
|
||||
else return 0
|
||||
else return 0;
|
||||
},
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Server" />
|
||||
|
||||
@@ -210,6 +210,57 @@ const ProcedureConfigInner = ({
|
||||
{
|
||||
header: "Modify",
|
||||
cell: ({ row }) => {
|
||||
const on_move_up = () =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: executions.map((item, i) => {
|
||||
// Make sure its not the first row
|
||||
if (i === row.index && row.index !== 0) {
|
||||
return executions[row.index - 1];
|
||||
} else if (i === row.index - 1) {
|
||||
// Reverse the entry, moving this row "Up"
|
||||
return executions[row.index];
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}),
|
||||
});
|
||||
const on_move_down = () =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: executions.map((item, i) => {
|
||||
// The index also cannot be the last index, which cannot be moved down
|
||||
if (
|
||||
i === row.index &&
|
||||
row.index !== executions.length - 1
|
||||
) {
|
||||
return executions[row.index + 1];
|
||||
} else if (i === row.index + 1) {
|
||||
// Move the row "Down"
|
||||
return executions[row.index];
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}),
|
||||
});
|
||||
const on_insert_above = () =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: [
|
||||
...executions.slice(0, row.index),
|
||||
default_enabled_execution(),
|
||||
...executions.slice(row.index),
|
||||
],
|
||||
});
|
||||
const on_insert_below = () =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: [
|
||||
...executions.slice(0, row.index + 1),
|
||||
default_enabled_execution(),
|
||||
...executions.slice(row.index + 1),
|
||||
],
|
||||
});
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
@@ -228,22 +279,7 @@ const ProcedureConfigInner = ({
|
||||
{row.index ? (
|
||||
<DropdownMenuItem
|
||||
className="flex gap-4 justify-between cursor-pointer"
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: executions.map((item, i) => {
|
||||
// Make sure its not the first row
|
||||
if (i === row.index && row.index !== 0) {
|
||||
return executions[row.index - 1];
|
||||
} else if (i === row.index - 1) {
|
||||
// Reverse the entry, moving this row "Up"
|
||||
return executions[row.index];
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
onClick={on_move_up}
|
||||
>
|
||||
Move Up <ArrowUp className="w-4 h-4" />
|
||||
</DropdownMenuItem>
|
||||
@@ -251,25 +287,7 @@ const ProcedureConfigInner = ({
|
||||
{row.index < executions.length - 1 && (
|
||||
<DropdownMenuItem
|
||||
className="flex gap-4 justify-between cursor-pointer"
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: executions.map((item, i) => {
|
||||
// The index also cannot be the last index, which cannot be moved down
|
||||
if (
|
||||
i === row.index &&
|
||||
row.index !== executions.length - 1
|
||||
) {
|
||||
return executions[row.index + 1];
|
||||
} else if (i === row.index + 1) {
|
||||
// Move the row "Down"
|
||||
return executions[row.index];
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
onClick={on_move_down}
|
||||
>
|
||||
Move Down <ArrowDown className="w-4 h-4" />
|
||||
</DropdownMenuItem>
|
||||
@@ -277,16 +295,7 @@ const ProcedureConfigInner = ({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex gap-4 justify-between cursor-pointer"
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: [
|
||||
...executions.slice(0, row.index),
|
||||
default_enabled_execution(),
|
||||
...executions.slice(row.index),
|
||||
],
|
||||
})
|
||||
}
|
||||
onClick={on_insert_above}
|
||||
>
|
||||
Insert Above{" "}
|
||||
<div className="flex">
|
||||
@@ -296,16 +305,7 @@ const ProcedureConfigInner = ({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex gap-4 justify-between cursor-pointer"
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
executions: [
|
||||
...executions.slice(0, row.index + 1),
|
||||
default_enabled_execution(),
|
||||
...executions.slice(row.index + 1),
|
||||
],
|
||||
})
|
||||
}
|
||||
onClick={on_insert_below}
|
||||
>
|
||||
Insert Below{" "}
|
||||
<div className="flex">
|
||||
@@ -356,6 +356,18 @@ const ProcedureConfigInner = ({
|
||||
<CopyGithubWebhook
|
||||
path={`/procedure/${procedure._id?.$oid!}/${branch}`}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-4 w-full">
|
||||
<div className="text-muted-foreground">
|
||||
Enabled:
|
||||
</div>
|
||||
<Switch
|
||||
checked={procedure.config.webhook_enabled}
|
||||
onCheckedChange={(webhook_enabled) =>
|
||||
setConfig({ ...config, webhook_enabled })
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigItem>
|
||||
</CardHeader>
|
||||
|
||||
@@ -39,6 +39,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
name: (id) => useProcedure(id)?.name,
|
||||
|
||||
Icon: () => <Route className="w-4" />,
|
||||
BigIcon: () => <Route className="w-8" />,
|
||||
|
||||
Status: {},
|
||||
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { ResourceLink } from "../common";
|
||||
|
||||
export const ProcedureTable = ({ search }: { search?: string }) => {
|
||||
const tags = useTagsFilter();
|
||||
const procedures = useRead("ListProcedures", {}).data;
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(procedures, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="procedures"
|
||||
data={
|
||||
procedures?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { useState } from "react";
|
||||
import { CopyGithubWebhook, ResourceSelector } from "../common";
|
||||
import { CopyGithubWebhook, ServerSelector } from "../common";
|
||||
|
||||
export const RepoConfig = ({ id }: { id: string }) => {
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
@@ -34,18 +34,15 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
}}
|
||||
components={{
|
||||
general: {
|
||||
general: {
|
||||
server_id: (selected, set) => (
|
||||
<ConfigItem label="Server">
|
||||
<ResourceSelector
|
||||
type="Server"
|
||||
selected={selected}
|
||||
onSelect={(server_id) => set({ server_id })}
|
||||
/>
|
||||
</ConfigItem>
|
||||
"": {
|
||||
server_id: (value, set) => (
|
||||
<ServerSelector selected={value} set={set} disabled={disabled} />
|
||||
),
|
||||
repo: true,
|
||||
branch: true,
|
||||
},
|
||||
general: {
|
||||
repo: { placeholder: "Enter repo" },
|
||||
branch: { placeholder: "Enter branch" },
|
||||
commit: { placeholder: "Enter specific commit hash. Optional." },
|
||||
github_account: (value, set) => {
|
||||
const server_id = update.server_id || config.server_id;
|
||||
if (server_id) {
|
||||
@@ -87,6 +84,7 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
<CopyGithubWebhook path={`/repo/${id}/pull`} />
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -7,10 +7,20 @@ import { RepoConfig } from "./config";
|
||||
import { CloneRepo, PullRepo } from "./actions";
|
||||
import { DeleteResource, NewResource } from "../common";
|
||||
import { RepoTable } from "./table";
|
||||
import { bg_color_class_by_intention, fill_color_class_by_intention, repo_state_intention } from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
|
||||
const useRepo = (id?: string) =>
|
||||
useRead("ListRepos", {}).data?.find((d) => d.id === id);
|
||||
|
||||
const RepoIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useRepo(id)?.info.state;
|
||||
const color = fill_color_class_by_intention(
|
||||
repo_state_intention(state)
|
||||
);
|
||||
return <GitBranch className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
export const RepoComponents: RequiredResourceComponents = {
|
||||
Dashboard: () => {
|
||||
const repo_count = useRead("ListRepos", {}).data?.length;
|
||||
@@ -38,9 +48,20 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
Name: ({ id }) => <>{useRepo(id)?.name}</>,
|
||||
name: (id) => useRepo(id)?.name,
|
||||
|
||||
Icon: () => <GitBranch className="w-4 h-4" />,
|
||||
Icon: ({ id }) => <RepoIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <RepoIcon id={id} size={8} />,
|
||||
|
||||
Status: {},
|
||||
Status: {
|
||||
State: ({ id }) => {
|
||||
let state = useRepo(id)?.info.state;
|
||||
const color = bg_color_class_by_intention(repo_state_intention(state));
|
||||
return (
|
||||
<Card className={cn("w-fit", color)}>
|
||||
<CardHeader className="py-0 px-2">{state}</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Info: {
|
||||
Repo: ({ id }) => {
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { RepoComponents } from ".";
|
||||
|
||||
export const RepoTable = ({ search }: { search?: string }) => {
|
||||
const tags = useTagsFilter();
|
||||
const repos = useRead("ListRepos", {}).data;
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(repos, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="repos"
|
||||
data={
|
||||
repos?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -39,6 +31,15 @@ export const RepoTable = ({ search }: { search?: string }) => {
|
||||
<SortableHeader column={column} title="Branch" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "info.state",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<RepoComponents.Status.State id={row.original.id} />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
cell: ({ row }) => {
|
||||
|
||||
@@ -66,7 +66,10 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
),
|
||||
volumes: (volumes, set) => {
|
||||
return (
|
||||
<ConfigItem label="EBS Volumes" className="items-start">
|
||||
<ConfigItem
|
||||
label="EBS Volumes"
|
||||
className={volumes.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{volumes.map((_, index) => (
|
||||
<div
|
||||
@@ -81,7 +84,7 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
set({
|
||||
@@ -96,7 +99,7 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
))}
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
set({
|
||||
volumes: [...volumes, newVolume(volumes.length)],
|
||||
|
||||
@@ -94,6 +94,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
|
||||
name: (id) => useServerTemplate(id)?.name,
|
||||
|
||||
Icon: () => <ServerCog className="w-4 h-4" />,
|
||||
BigIcon: () => <ServerCog className="w-8 h-8" />,
|
||||
|
||||
Status: {},
|
||||
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
|
||||
export const ServerTemplateTable = ({ search }: { search?: string }) => {
|
||||
const tags = useTagsFilter();
|
||||
const server_templates = useRead("ListServerTemplates", {}).data;
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(server_templates, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="server-templates"
|
||||
data={
|
||||
server_templates?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
Scissors,
|
||||
XOctagon,
|
||||
AreaChart,
|
||||
Milestone,
|
||||
} from "lucide-react";
|
||||
import { Section } from "@components/layouts";
|
||||
import { RenameServer } from "./actions";
|
||||
import {
|
||||
bg_color_class_by_intention,
|
||||
fill_color_class_by_intention,
|
||||
server_status_intention,
|
||||
server_state_intention,
|
||||
} from "@lib/color";
|
||||
import { ServerConfig } from "./config";
|
||||
import { DeploymentTable } from "../deployment/table";
|
||||
@@ -34,6 +35,18 @@ export const useServer = (id?: string) =>
|
||||
(d) => d.id === id
|
||||
);
|
||||
|
||||
const _ServerIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useServer(id)?.info.state;
|
||||
return (
|
||||
<ServerIcon
|
||||
className={cn(
|
||||
`w-${size} h-${size}`,
|
||||
id && fill_color_class_by_intention(server_state_intention(state))
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServerComponents: RequiredResourceComponents = {
|
||||
Dashboard: ServersChart,
|
||||
|
||||
@@ -44,38 +57,38 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>,
|
||||
name: (id) => useServer(id)?.name,
|
||||
|
||||
Icon: ({ id }) => {
|
||||
const status = useServer(id)?.info.status;
|
||||
return (
|
||||
<ServerIcon
|
||||
className={cn(
|
||||
"w-4 h-4",
|
||||
id && fill_color_class_by_intention(server_status_intention(status))
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Icon: ({ id }) => <_ServerIcon id={id} size={4} />,
|
||||
BigIcon: ({ id }) => <_ServerIcon id={id} size={8} />,
|
||||
|
||||
Status: {
|
||||
Status: ({ id }) => {
|
||||
const status = useServer(id)?.info.status;
|
||||
const color = bg_color_class_by_intention(
|
||||
server_status_intention(status)
|
||||
);
|
||||
State: ({ id }) => {
|
||||
const state = useServer(id)?.info.state;
|
||||
const color = bg_color_class_by_intention(server_state_intention(state));
|
||||
return (
|
||||
<Card className={cn("w-fit", color)}>
|
||||
<CardHeader className="py-0 px-2">
|
||||
{status === Types.ServerStatus.NotOk ? "Not Ok" : status}
|
||||
{state === Types.ServerState.NotOk ? "Not Ok" : state}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
Stats: ({id}) => <Link to={`/servers/${id}/stats`}>
|
||||
<Button variant="link" className="flex gap-2 items-center p-0">
|
||||
<AreaChart className="w-4 h-4" />
|
||||
Stats
|
||||
</Button>
|
||||
</Link>
|
||||
Stats: ({ id }) => (
|
||||
<Link to={`/servers/${id}/stats`}>
|
||||
<Button variant="link" className="flex gap-2 items-center p-0">
|
||||
<AreaChart className="w-4 h-4" />
|
||||
Stats
|
||||
</Button>
|
||||
</Link>
|
||||
),
|
||||
Version: ({ id }) => {
|
||||
const version = useRead("GetPeripheryVersion", { server: id }).data
|
||||
?.version;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Milestone className="w-4 h-4" />v{version}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Info: {
|
||||
@@ -85,7 +98,7 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
useRead(
|
||||
"GetSystemInformation",
|
||||
{ server: id },
|
||||
{ enabled: server ? server.info.status !== "Disabled" : false }
|
||||
{ enabled: server ? server.info.state !== "Disabled" : false }
|
||||
).data?.core_count ?? 0;
|
||||
return (
|
||||
<Link to={`/servers/${id}/stats`} className="flex gap-2 items-center">
|
||||
@@ -99,7 +112,7 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{ enabled: server ? server.info.status !== "Disabled" : false }
|
||||
{ enabled: server ? server.info.state !== "Disabled" : false }
|
||||
).data;
|
||||
return (
|
||||
<Link to={`/servers/${id}/stats`} className="flex gap-2 items-center">
|
||||
@@ -113,7 +126,7 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
{ server: id },
|
||||
{ enabled: server ? server.info.status !== "Disabled" : false }
|
||||
{ enabled: server ? server.info.state !== "Disabled" : false }
|
||||
).data;
|
||||
const disk_total_gb = stats?.disks.reduce(
|
||||
(acc, curr) => acc + curr.total_gb,
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { useRead, useTagsFilter } from "@lib/hooks";
|
||||
import { useFilterResources, useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ServerComponents } from ".";
|
||||
import { ResourceLink } from "../common";
|
||||
|
||||
export const ServerTable = ({ search }: { search?: string }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const tags = useTagsFilter();
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
const filtered = useFilterResources(servers, search);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="servers"
|
||||
data={
|
||||
servers?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? []
|
||||
}
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -56,16 +47,16 @@ export const ServerTable = ({ search }: { search?: string }) => {
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "info.status",
|
||||
accessorKey: "info.state",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Status" />
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({
|
||||
row: {
|
||||
original: { id },
|
||||
},
|
||||
}) => {
|
||||
return <ServerComponents.Status.Status id={id} />;
|
||||
return <ServerComponents.Status.State id={id} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
93
frontend/src/components/sidebar.tsx
Normal file
93
frontend/src/components/sidebar.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { RESOURCE_TARGETS, cn, usableResourcePath } from "@lib/utils";
|
||||
import { Button } from "@ui/button";
|
||||
import { Card, CardContent } from "@ui/card";
|
||||
import { AlertTriangle, Bell, Box, Home, Tag, UserCircle2 } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import { Separator } from "@ui/separator";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const Sidebar = () => {
|
||||
return (
|
||||
<Card className="h-fit m-4 hidden lg:flex">
|
||||
<CardContent className="h-fit grid gap-2 px-6 py-4">
|
||||
<SidebarLink label="Home" to="/" icon={<Home className="w-4 h-4" />} />
|
||||
|
||||
<Separator />
|
||||
|
||||
{RESOURCE_TARGETS.map((type) => {
|
||||
const RTIcon = ResourceComponents[type].Icon;
|
||||
const name = type === "ServerTemplate" ? "Template" : type;
|
||||
return (
|
||||
<SidebarLink
|
||||
key={type}
|
||||
label={`${name}s`}
|
||||
to={`/${usableResourcePath(type)}`}
|
||||
icon={<RTIcon />}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarLink
|
||||
label="Alerts"
|
||||
to="/alerts"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Updates"
|
||||
to="/updates"
|
||||
icon={<Bell className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Tags"
|
||||
to="/tags"
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarLink
|
||||
label="Api Keys"
|
||||
to="/keys"
|
||||
icon={<Box className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Users"
|
||||
to="/users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarLink = ({
|
||||
to,
|
||||
icon,
|
||||
label,
|
||||
}: {
|
||||
to: string;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<Link to={to} className="w-full">
|
||||
<Button
|
||||
variant="link"
|
||||
className={cn(
|
||||
"flex justify-start items-center gap-2 w-full hover:bg-accent",
|
||||
"/" + location.pathname.split("/")[1] === to && "bg-accent"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useRead, useResourceParamType } from "@lib/hooks";
|
||||
import { useRead, useResourceParamType, useUser } from "@lib/hooks";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Bell,
|
||||
Box,
|
||||
Boxes,
|
||||
FileQuestion,
|
||||
@@ -33,7 +34,7 @@ import { UsableResource } from "@types";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -46,10 +47,10 @@ import { ResourceLink } from "./resources/common";
|
||||
|
||||
export const Topbar = () => {
|
||||
return (
|
||||
<div className="sticky top-0 border-b z-50 w-full bg-card text-card-foreground shadow">
|
||||
<div className="container flex items-center justify-between py-4 gap-8">
|
||||
<div className="sticky top-0 h-[70px] border-b z-50 w-full bg-card text-card-foreground shadow flex items-center">
|
||||
<div className="w-full flex items-center justify-between p-4 gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to={"/"} className="text-2xl tracking-widest">
|
||||
<Link to={"/"} className="text-2xl tracking-widest mx-8">
|
||||
MONITOR
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
@@ -73,6 +74,8 @@ export const Topbar = () => {
|
||||
};
|
||||
|
||||
const PrimaryDropdown = () => {
|
||||
const user = useUser().data;
|
||||
|
||||
const type = useResourceParamType();
|
||||
const Components = type && ResourceComponents[type];
|
||||
|
||||
@@ -89,6 +92,8 @@ const PrimaryDropdown = () => {
|
||||
? [<Tag className="w-4 h-4" />, "Tags"]
|
||||
: location.pathname === "/alerts"
|
||||
? [<AlertTriangle className="w-4 h-4" />, "Alerts"]
|
||||
: location.pathname === "/updates"
|
||||
? [<Bell className="w-4 h-4" />, "Updates"]
|
||||
: location.pathname.split("/")[1] === "user-groups"
|
||||
? [<Users className="w-4 h-4" />, "User Groups"]
|
||||
: location.pathname === "/users" ||
|
||||
@@ -99,7 +104,7 @@ const PrimaryDropdown = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild className="lg:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex justify-start items-center gap-2 w-36 px-3"
|
||||
@@ -110,12 +115,11 @@ const PrimaryDropdown = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-36" side="bottom">
|
||||
<DropdownMenuGroup>
|
||||
<Link to="/">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Home className="w-4 h-4" />
|
||||
Home
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Home"
|
||||
icon={<Home className="w-4 h-4" />}
|
||||
to="/"
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -123,51 +127,75 @@ const PrimaryDropdown = () => {
|
||||
const RTIcon = ResourceComponents[type].Icon;
|
||||
const name = type === "ServerTemplate" ? "Template" : type;
|
||||
return (
|
||||
<Link key={type} to={`/${usableResourcePath(type)}`}>
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<RTIcon />
|
||||
{name}s
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
key={type}
|
||||
label={`${name}s`}
|
||||
icon={<RTIcon />}
|
||||
to={`/${usableResourcePath(type)}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<Link to="/alerts">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Alerts
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Alerts"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
to="/alerts"
|
||||
/>
|
||||
|
||||
<Link to="/tags">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Tag className="w-4 h-4" />
|
||||
Tags
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Updates"
|
||||
icon={<Bell className="w-4 h-4" />}
|
||||
to="/updates"
|
||||
/>
|
||||
|
||||
<DropdownLinkItem
|
||||
label="Tags"
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
to="/tags"
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<Link to="/keys">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Box className="w-4 h-4" />
|
||||
Api Keys
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/users">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<UserCircle2 className="w-4 h-4" />
|
||||
Users
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Api Keys"
|
||||
icon={<Box className="w-4 h-4" />}
|
||||
to="/keys"
|
||||
/>
|
||||
|
||||
{user?.admin && (
|
||||
<DropdownLinkItem
|
||||
label="Users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
to="/users"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownLinkItem = ({
|
||||
label,
|
||||
icon,
|
||||
to,
|
||||
}: {
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
to: string;
|
||||
}) => {
|
||||
return (
|
||||
<Link to={to}>
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
{icon}
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export type HomeView = "Dashboard" | "Tree" | "Resources";
|
||||
|
||||
export const homeViewAtom = atomWithStorage<HomeView>(
|
||||
@@ -250,7 +278,7 @@ const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
|
||||
{selected ? selected.name : `All ${type}s`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
|
||||
<PopoverContent className="w-[300px] max-h-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={`Search ${type}s`}
|
||||
|
||||
@@ -19,10 +19,17 @@ import { useRead } from "@lib/hooks";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { Link } from "react-router-dom";
|
||||
import { fmt_duration, fmt_version } from "@lib/formatting";
|
||||
import { usableResourcePath, version_is_none } from "@lib/utils";
|
||||
import { sanitizeOnlySpan, usableResourcePath, version_is_none } from "@lib/utils";
|
||||
import { UsableResource } from "@types";
|
||||
|
||||
export const UpdateUser = ({ user_id }: { user_id: string }) => {
|
||||
if (user_id === "Procedure") return <>{user_id}</>;
|
||||
if (user_id === "Github") return <>{user_id}</>;
|
||||
if (user_id === "Auto Redeploy") return <>{user_id}</>;
|
||||
return <RealUpdateUser user_id={user_id} />;
|
||||
};
|
||||
|
||||
const RealUpdateUser = ({ user_id }: { user_id: string }) => {
|
||||
const username = useRead("GetUsername", { user_id }).data?.username;
|
||||
return <>{username || user_id}</>;
|
||||
};
|
||||
@@ -85,9 +92,9 @@ export const UpdateDetailsInner = ({
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
to={`/${usableResourcePath(update.target.type as UsableResource)}/${
|
||||
update.target.id
|
||||
}`}
|
||||
to={`/${usableResourcePath(
|
||||
update.target.type as UsableResource
|
||||
)}/${update.target.id}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
@@ -146,17 +153,23 @@ export const UpdateDetailsInner = ({
|
||||
{log.stdout && (
|
||||
<div>
|
||||
<CardDescription>stdout</CardDescription>
|
||||
<pre className="max-h-[500px] overflow-y-auto">
|
||||
{log.stdout}
|
||||
</pre>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stdout),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{log.stderr && (
|
||||
<div>
|
||||
<CardDescription>stdout</CardDescription>
|
||||
<pre className="max-h-[500px] overflow-y-auto">
|
||||
{log.stderr}
|
||||
</pre>
|
||||
<CardDescription>stderr</CardDescription>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stderr),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -58,6 +58,33 @@ const UpdateCard = ({ update }: { update: Types.UpdateListItem }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AllUpdates = () => {
|
||||
|
||||
const updates = useRead("ListUpdates", {}).data;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Updates"
|
||||
icon={<Bell className="w-4 h-4" />}
|
||||
actions={
|
||||
<Link
|
||||
to="/updates"
|
||||
>
|
||||
<Button variant="secondary" size="icon">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{updates?.updates.slice(0, 3).map((update) => (
|
||||
<UpdateCard update={update} key={update.id} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceUpdates = ({ type, id }: Types.ResourceTarget) => {
|
||||
const deployments = useRead("ListDeployments", {}).data;
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
--secondary: 220.1 40.1% 90.2%;
|
||||
--secondary-foreground: 220.1 40.1% 5.2%;
|
||||
|
||||
--muted: 220.1 40.1% 98.2%;
|
||||
--muted-foreground: 220.1 20.1% 40.2%;
|
||||
--muted: 220.1 40.1% 90.2%;
|
||||
--muted-foreground: 220.1 20.1% 20.2%;
|
||||
|
||||
--accent: 220.1 40.1% 90.2%;
|
||||
--accent-foreground: 220.1 40.1% 11.2%;
|
||||
|
||||
@@ -59,7 +59,7 @@ export const bg_color_class_by_intention = (intention: ColorIntention) => {
|
||||
case "Neutral":
|
||||
return "bg-blue-400 dark:bg-blue-700";
|
||||
case "Warning":
|
||||
return "bg-orange-400 dark:bg-orange-700";
|
||||
return "bg-orange-400 dark:bg-orange-500";
|
||||
case "Critical":
|
||||
return "bg-red-400 dark:bg-red-700";
|
||||
case "Unknown":
|
||||
@@ -86,15 +86,15 @@ export const text_color_class_by_intention = (intention: ColorIntention) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const server_status_intention: (
|
||||
status?: Types.ServerStatus
|
||||
export const server_state_intention: (
|
||||
status?: Types.ServerState
|
||||
) => ColorIntention = (status) => {
|
||||
switch (status) {
|
||||
case Types.ServerStatus.Ok:
|
||||
case Types.ServerState.Ok:
|
||||
return "Good";
|
||||
case Types.ServerStatus.NotOk:
|
||||
case Types.ServerState.NotOk:
|
||||
return "Critical";
|
||||
case Types.ServerStatus.Disabled:
|
||||
case Types.ServerState.Disabled:
|
||||
return "Neutral";
|
||||
case undefined:
|
||||
return "None";
|
||||
@@ -102,28 +102,67 @@ export const server_status_intention: (
|
||||
};
|
||||
|
||||
export const deployment_state_intention: (
|
||||
state?: Types.DockerContainerState
|
||||
state?: Types.DeploymentState
|
||||
) => ColorIntention = (state) => {
|
||||
switch (state) {
|
||||
case undefined:
|
||||
return "None";
|
||||
case Types.DockerContainerState.Running:
|
||||
case Types.DeploymentState.Running:
|
||||
return "Good";
|
||||
case Types.DockerContainerState.NotDeployed:
|
||||
case Types.DeploymentState.NotDeployed:
|
||||
return "Neutral";
|
||||
case Types.DockerContainerState.Unknown:
|
||||
case Types.DeploymentState.Unknown:
|
||||
return "Unknown";
|
||||
default:
|
||||
return "Critical";
|
||||
}
|
||||
};
|
||||
|
||||
export const build_state_intention = (status?: Types.BuildState) => {
|
||||
switch (status) {
|
||||
case undefined:
|
||||
return "None";
|
||||
case Types.BuildState.Unknown:
|
||||
return "Unknown";
|
||||
case Types.BuildState.Ok:
|
||||
return "Good";
|
||||
case Types.BuildState.Building:
|
||||
return "Warning";
|
||||
case Types.BuildState.Failed:
|
||||
return "Critical";
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
};
|
||||
|
||||
export const repo_state_intention = (state?: Types.RepoState) => {
|
||||
switch (state) {
|
||||
case undefined:
|
||||
return "None";
|
||||
case Types.RepoState.Unknown:
|
||||
return "Unknown";
|
||||
case Types.RepoState.Ok:
|
||||
return "Good";
|
||||
case Types.RepoState.Cloning:
|
||||
return "Warning";
|
||||
case Types.RepoState.Pulling:
|
||||
return "Warning";
|
||||
case Types.RepoState.Failed:
|
||||
return "Critical";
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
};
|
||||
|
||||
export const alert_level_intention: (
|
||||
level: Types.SeverityLevel
|
||||
) => ColorIntention = (level) => {
|
||||
switch (level) {
|
||||
case Types.SeverityLevel.Ok: return "Good"
|
||||
case Types.SeverityLevel.Warning: return "Warning"
|
||||
case Types.SeverityLevel.Critical: return "Critical"
|
||||
case Types.SeverityLevel.Ok:
|
||||
return "Good";
|
||||
case Types.SeverityLevel.Warning:
|
||||
return "Warning";
|
||||
case Types.SeverityLevel.Critical:
|
||||
return "Critical";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -216,3 +216,18 @@ export const useCheckResourceExists = () => {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const useFilterResources = <Info>(
|
||||
resources?: Types.ResourceListItem<Info>[],
|
||||
search?: string,
|
||||
) => {
|
||||
const tags = useTagsFilter();
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
return resources?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? [];
|
||||
};
|
||||
@@ -5,11 +5,10 @@ import { toast } from "@ui/use-toast";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { Circle } from "lucide-react";
|
||||
import { ReactNode, useCallback, useEffect } from "react";
|
||||
import Rws from "reconnecting-websocket";
|
||||
import { cn } from "@lib/utils";
|
||||
import { AUTH_TOKEN_STORAGE_KEY } from "@main";
|
||||
|
||||
const rws_atom = atom<Rws | null>(null);
|
||||
const rws_atom = atom<WebSocket | null>(null);
|
||||
const useWebsocket = () => useAtom(rws_atom);
|
||||
|
||||
const ws_connected = atom(false);
|
||||
@@ -46,7 +45,7 @@ const on_message = (
|
||||
["ListServers"],
|
||||
["GetServer"],
|
||||
["GetServerActionState"],
|
||||
["GetServerStatus"],
|
||||
["GetServerState"],
|
||||
["GetHistoricalServerStats"],
|
||||
["GetServersSummary"]
|
||||
);
|
||||
@@ -103,13 +102,6 @@ const on_message = (
|
||||
}
|
||||
};
|
||||
|
||||
const on_open = (ws: Rws | null) => {
|
||||
const jwt = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
|
||||
if (!ws || !jwt) return;
|
||||
const msg: Types.WsLoginMessage = { type: "Jwt", params: { jwt } };
|
||||
if (jwt && ws) ws.send(JSON.stringify(msg));
|
||||
};
|
||||
|
||||
export const WebsocketProvider = ({
|
||||
url,
|
||||
children,
|
||||
@@ -118,38 +110,28 @@ export const WebsocketProvider = ({
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const invalidate = useInvalidate();
|
||||
const [ws, set] = useWebsocket();
|
||||
const setConnected = useWebsocketConnected()[1];
|
||||
const [_, set] = useWebsocket();
|
||||
const [connected, setConnected] = useWebsocketConnected();
|
||||
|
||||
const on_open_fn = useCallback(() => {
|
||||
on_open(ws);
|
||||
setConnected(true);
|
||||
}, [ws, setConnected]);
|
||||
const on_message_fn = useCallback(
|
||||
(e: MessageEvent) => on_message(e, invalidate),
|
||||
[invalidate]
|
||||
);
|
||||
const on_close_fn = useCallback(() => {
|
||||
setConnected(false);
|
||||
}, [setConnected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ws) set(new Rws(url));
|
||||
return () => {
|
||||
ws?.close();
|
||||
};
|
||||
}, [set, url, ws]);
|
||||
|
||||
useEffect(() => {
|
||||
ws?.addEventListener("open", on_open_fn);
|
||||
ws?.addEventListener("message", on_message_fn);
|
||||
ws?.addEventListener("close", on_close_fn);
|
||||
return () => {
|
||||
ws?.removeEventListener("open", on_open_fn);
|
||||
ws?.removeEventListener("message", on_message_fn);
|
||||
ws?.removeEventListener("close", on_close_fn);
|
||||
};
|
||||
}, [on_message_fn, on_open_fn, on_close_fn, ws]);
|
||||
if (!connected) {
|
||||
const ws = make_websocket(
|
||||
url,
|
||||
() => setConnected(true),
|
||||
on_message_fn,
|
||||
() => {
|
||||
console.log("ws closed");
|
||||
setConnected(false);
|
||||
}
|
||||
);
|
||||
set(ws);
|
||||
}
|
||||
}, [set, url, connected]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -177,3 +159,34 @@ export const WsStatusIndicator = () => {
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const make_websocket = (
|
||||
url: string,
|
||||
on_open: () => void,
|
||||
on_message: (e: MessageEvent) => void,
|
||||
on_close: () => void
|
||||
) => {
|
||||
console.log("init websocket");
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.addEventListener("open", on_open);
|
||||
ws.addEventListener("message", on_message);
|
||||
ws.addEventListener("close", on_close);
|
||||
|
||||
const _on_open = () => {
|
||||
const jwt = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
|
||||
if (!ws || !jwt) return;
|
||||
const msg: Types.WsLoginMessage = { type: "Jwt", params: { jwt } };
|
||||
if (jwt && ws) ws.send(JSON.stringify(msg));
|
||||
on_open();
|
||||
};
|
||||
|
||||
ws?.addEventListener("open", _on_open);
|
||||
ws?.addEventListener("message", on_message);
|
||||
ws?.addEventListener("close", on_close);
|
||||
|
||||
// force close every 30s to trigger reconnect and keep fresh
|
||||
setTimeout(() => ws.close(), 30_000);
|
||||
|
||||
return ws
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { Types } from "@monitor/client";
|
||||
import { UsableResource } from "@types";
|
||||
import Convert from "ansi-to-html";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@@ -12,11 +14,11 @@ export const object_keys = <T extends object>(o: T): (keyof T)[] =>
|
||||
Object.keys(o) as (keyof T)[];
|
||||
|
||||
export const RESOURCE_TARGETS: UsableResource[] = [
|
||||
"Deployment",
|
||||
"Server",
|
||||
"Deployment",
|
||||
"Build",
|
||||
"Procedure",
|
||||
"Repo",
|
||||
"Procedure",
|
||||
"Builder",
|
||||
"Alerter",
|
||||
"ServerTemplate"
|
||||
@@ -96,3 +98,29 @@ export const usableResourcePath = (resource: UsableResource) => {
|
||||
if (resource === "ServerTemplate") return "server-templates"
|
||||
return `${resource.toLowerCase()}s`
|
||||
}
|
||||
|
||||
export const sanitizeOnlySpan = (log: string) => {
|
||||
return sanitizeHtml(log, {
|
||||
allowedTags: ["span"],
|
||||
allowedAttributes: {
|
||||
"span": ["class"]
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const convert = new Convert();
|
||||
/**
|
||||
* Converts the ansi colors in log to html.
|
||||
* sanitizes incoming log first for any eg. script tags.
|
||||
* @param log incoming log string
|
||||
*/
|
||||
export const logToHtml = (log: string) => {
|
||||
if (!log) return "No log.";
|
||||
const sanitized = sanitizeHtml(log, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.filter(
|
||||
(tag) => tag !== "script"
|
||||
),
|
||||
allowedAttributes: sanitizeHtml.defaults.allowedAttributes,
|
||||
});
|
||||
return convert.toHtml(sanitized);
|
||||
};
|
||||
@@ -13,12 +13,14 @@ import { OpenAlerts } from "@components/alert";
|
||||
import { useCheckResourceExists, useRead, useUser } from "@lib/hooks";
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { Fragment } from "react";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { cn, usableResourcePath } from "@lib/utils";
|
||||
import { AllUpdates } from "@components/updates/resource";
|
||||
|
||||
export const Dashboard = () => {
|
||||
return (
|
||||
<Page title="">
|
||||
<OpenAlerts />
|
||||
<AllUpdates />
|
||||
<RecentlyViewed />
|
||||
<Resources />
|
||||
</Page>
|
||||
@@ -65,18 +67,21 @@ const RecentlyViewed = () => {
|
||||
icon={<History className="w-4 h-4" />}
|
||||
actions=""
|
||||
>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
{recently_viewed
|
||||
?.filter(checkResourceExists)
|
||||
.slice(0, 6)
|
||||
.map(({ type, id }) => (
|
||||
.slice(0, 8)
|
||||
.map(({ type, id }, i) => (
|
||||
<Fragment key={type + id}>
|
||||
{type !== "System" && (
|
||||
<Card
|
||||
onClick={() => nav(`/${usableResourcePath(type)}/${id}`)}
|
||||
className="px-3 py-2 h-fit hover:bg-accent/50 group-focus:bg-accent/50 transition-colors cursor-pointer"
|
||||
className={cn(
|
||||
"px-3 py-2 h-fit hover:bg-accent/50 group-focus:bg-accent/50 transition-colors cursor-pointer",
|
||||
i > 5 && "hidden 2xl:flex"
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex items-center justify-between gap-4 px-3 py-2 text-sm text-muted-foreground">
|
||||
<CardContent className="flex items-center justify-between gap-4 px-3 py-2 text-sm text-muted-foreground w-full">
|
||||
<ResourceLink type={type} id={id} />
|
||||
{type}
|
||||
</CardContent>
|
||||
|
||||
@@ -40,6 +40,7 @@ export const Resource = () => {
|
||||
return (
|
||||
<Page
|
||||
title={<Components.Name id={id} />}
|
||||
icon={<Components.BigIcon id={id} />}
|
||||
titleRight={
|
||||
<div className="flex gap-4 items-center">
|
||||
{Object.entries(Components.Status).map(([key, Status]) => (
|
||||
@@ -50,10 +51,9 @@ export const Resource = () => {
|
||||
subtitle={
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-4 items-center text-muted-foreground">
|
||||
<Components.Icon id={id} />
|
||||
{Object.entries(Components.Info).map(([key, Info], i) => (
|
||||
<Fragment key={key}>
|
||||
| <Info key={i} id={id} />
|
||||
{i !== 0 && "| "}<Info key={i} id={id} />
|
||||
</Fragment>
|
||||
))}
|
||||
| <ExportButton target={{ type, id }} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { Page } from "@components/layouts";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { TagsFilter } from "@components/tags";
|
||||
import { useResourceParamType, useSetTitle } from "@lib/hooks";
|
||||
@@ -15,24 +15,23 @@ export const Resources = () => {
|
||||
return (
|
||||
<Page
|
||||
title={`${name}s`}
|
||||
icon={<Components.BigIcon />}
|
||||
actions={
|
||||
<div className="grid gap-4 justify-items-end">
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => set(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="w-96"
|
||||
/>
|
||||
<TagsFilter />
|
||||
<Components.New />
|
||||
</div>
|
||||
<TagsFilter />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => set(e.target.value)}
|
||||
placeholder="search..."
|
||||
className="w-96"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Section title="">
|
||||
<Components.Table />
|
||||
</Section>
|
||||
<Components.Table search={search} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,36 @@ import { ResourceComponents } from "@components/resources";
|
||||
import { AddTags, ResourceTags } from "@components/tags";
|
||||
import { UpdatesTable } from "@components/updates/table";
|
||||
import { useRead, useResourceParamType, useSetTitle } from "@lib/hooks";
|
||||
import { UsableResource } from "@types";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const ResourceUpdates = () => {
|
||||
export const Updates = () => {
|
||||
const type = useResourceParamType()!;
|
||||
const id = useParams().id as string;
|
||||
if (type && id) {
|
||||
return <ResourceUpdates type={type} id={id} />;
|
||||
} else {
|
||||
return <AllUpdates />;
|
||||
}
|
||||
};
|
||||
|
||||
const AllUpdates = () => {
|
||||
useSetTitle("Updates");
|
||||
const updates = useRead("ListUpdates", {}).data;
|
||||
return (
|
||||
<Page title="Updates">
|
||||
<UpdatesTable updates={updates?.updates ?? []} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceUpdates = ({
|
||||
type,
|
||||
id,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
id: string;
|
||||
}) => {
|
||||
const name = useRead(`List${type}s`, {}).data?.find((r) => r.id === id)?.name;
|
||||
useSetTitle(name && `${name} | Updates`);
|
||||
const updates = useRead("ListUpdates", {
|
||||
@@ -7,7 +7,7 @@ import { Keys } from "@pages/keys";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import { Tree } from "@pages/home/tree";
|
||||
import { Tags } from "@pages/tags";
|
||||
import { ResourceUpdates } from "@pages/resource_update";
|
||||
import { Updates } from "@pages/updates";
|
||||
import { UsersPage } from "@pages/users";
|
||||
import { AllResources } from "@pages/home/all_resources";
|
||||
import { UserDisabled } from "@pages/user_disabled";
|
||||
@@ -28,6 +28,7 @@ const ROUTER = createBrowserRouter([
|
||||
{ path: "tree", element: <Tree /> },
|
||||
{ path: "resources", element: <AllResources /> },
|
||||
{ path: "alerts", element: <Alerts /> },
|
||||
{ path: "updates", element: <Updates /> },
|
||||
{
|
||||
path: "users",
|
||||
children: [
|
||||
@@ -42,7 +43,7 @@ const ROUTER = createBrowserRouter([
|
||||
{ path: "", element: <Resources /> },
|
||||
{ path: ":id", element: <Resource /> },
|
||||
{ path: ":id/stats", element: <ResourceStats /> },
|
||||
{ path: ":id/updates", element: <ResourceUpdates /> },
|
||||
{ path: ":id/updates", element: <Updates /> },
|
||||
{ path: ":id/alerts", element: <Alerts /> },
|
||||
],
|
||||
},
|
||||
|
||||
1
frontend/src/types.d.ts
vendored
1
frontend/src/types.d.ts
vendored
@@ -21,6 +21,7 @@ export interface RequiredResourceComponents {
|
||||
|
||||
/** Icon for the component */
|
||||
Icon: OptionalIdComponent;
|
||||
BigIcon: OptionalIdComponent;
|
||||
|
||||
/** status metrics, like deployment state / status */
|
||||
Status: { [status: string]: IdComponent };
|
||||
|
||||
29
frontend/src/ui/separator.tsx
Normal file
29
frontend/src/ui/separator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
59
frontend/src/ui/toggle-group.tsx
Normal file
59
frontend/src/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { toggleVariants } from "@//ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
))
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
43
frontend/src/ui/toggle.tsx
Normal file
43
frontend/src/ui/toggle.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3",
|
||||
sm: "h-8 px-2",
|
||||
lg: "h-10 px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user