Compare commits

...

55 Commits

Author SHA1 Message Date
mbecker20
84d45c5df8 0.2.12 fix docker build command interp 2023-03-31 18:06:18 +00:00
mbecker20
c6559814b1 frontend for docker build extra args and use buildx 2023-03-31 17:31:39 +00:00
mbecker20
c8c080183f remove publish for cli 2023-03-31 17:04:15 +00:00
mbecker20
597b67f799 0.2.11 support buildx and arbitrary extra args 2023-03-31 17:03:38 +00:00
mbecker20
ec52d5f422 support docker buildx build and passing arbitrary extra args 2023-03-31 16:57:02 +00:00
mbecker20
34806304d6 add center menu title bottom border and adjust copy menu 2023-03-31 05:41:35 +00:00
beckerinj
87953d5495 menu padding 2rem 2023-03-31 01:27:17 -04:00
beckerinj
b6c7c80c95 full width input for copy menu 2023-03-31 01:26:19 -04:00
beckerinj
77e568d5c3 small 2023-03-27 12:41:59 -04:00
mbecker20
699fc51cf7 link to build if click on image deployment header 2023-03-27 15:30:11 +00:00
mbecker20
21029c90b7 info page on stats page 2023-03-27 05:13:12 +00:00
mbecker20
6b0530eb7f brush up server stats page 2023-03-26 23:15:58 +00:00
beckerinj
f7061c7225 toggle to show absolutes for mem and disk stat graphs 2023-03-26 18:47:21 -04:00
mbecker20
750f698369 updates page 2023-03-26 02:20:39 +00:00
mbecker20
ec5ef42298 add max height / scrolling to copy menu target selector 2023-03-24 00:45:47 +00:00
beckerinj
46820b0044 increase the tab title padding 2023-03-23 20:36:31 -04:00
beckerinj
425a6648f7 improve summary styling 2023-03-23 03:13:19 -04:00
mbecker20
349fc297ce 0.2.10 add renaming functionality 2023-03-22 20:33:26 +00:00
mbecker20
5ad87c03ed show none when none 2023-03-22 07:16:04 +00:00
mbecker20
d16006f28f improve design 2023-03-22 07:03:28 +00:00
beckerinj
7f0452a5f5 improve pie chart home page 2023-03-22 02:59:29 -04:00
mbecker20
c605b2f6fc implement pie chart summary 2023-03-22 06:41:57 +00:00
beckerinj
6c2d8a8494 unnecessary import 2023-03-21 23:10:01 -07:00
mbecker20
874691f729 add a pie chart component 2023-03-21 09:44:00 +00:00
beckerinj
cdf702e17d orange 2023-03-21 00:52:25 -07:00
mbecker20
25fdb32627 rename deployments 2023-03-19 08:14:54 +00:00
mbecker20
e976ea0a3a improve the behavior 2023-03-17 20:55:37 +00:00
mbecker20
34e6b4fc69 rename server working 2023-03-17 20:40:19 +00:00
mbecker20
a2d77567b3 dont need to 'to_monitor_name' servers 2023-03-15 07:35:54 +00:00
mbecker20
ecb460f9b5 add rename deployment to monitor client 2023-03-14 20:15:27 +00:00
mbecker20
63444b089c rename deployment func 2023-03-12 23:36:20 +00:00
mbecker20
c787984b77 initialize mongo with builder 2023-03-12 22:03:31 +00:00
mbecker20
bf3d03e801 fix problem of repeated query for docker accounts, secrets, etc 2023-03-12 05:07:55 +00:00
mbecker20
bc2e69b975 use resource to load stuff 2023-03-12 03:45:49 +00:00
mbecker20
7b94fcf3da 0.2.9 finish implement secret helpers on frontend 2023-03-12 00:48:18 +00:00
mbecker20
9cf03b8b88 add route to get available secret keys 2023-03-12 00:16:03 +00:00
mbecker20
a288edcf61 0.2.8 implement secret interpolation on builds and deployments 2023-03-11 23:34:17 +00:00
mbecker20
89cc18ad37 update tokio version 2023-03-10 19:27:40 +00:00
mbecker20
ffa3b671e1 change default alerting thresholds 2023-03-09 07:08:38 +00:00
beckerinj
f32eeb413b add label to home sort by 2023-03-08 16:10:23 -05:00
mbecker20
b5a5103cfc move core dockerfile 2023-03-08 18:26:42 +00:00
mbecker20
c5697e59f3 delete sample file 2023-03-08 18:24:15 +00:00
mbecker20
f030667ff4 update image in deployment header as well 2023-03-07 17:41:00 +00:00
mbecker20
e9fef5d97c change get_deployment_deployed_version to 'unknown' if not known 2023-03-07 17:39:59 +00:00
beckerinj
f5818ac7ea actually return image 2023-03-07 12:37:45 -05:00
mbecker20
c85ab4110d show derived image is container.image is sha256: 2023-03-07 16:30:59 +00:00
mbecker20
9690ea35b8 make description text area larger 2023-03-07 08:44:31 +00:00
mbecker20
6300c8011b fix modify global user permissions operator - make operator the admin, instead of the target 2023-03-06 17:09:12 +00:00
mbecker20
97f582b381 customizable page title 2023-03-06 02:07:08 +00:00
mbecker20
5135a9c228 show server name under deployment on admin user manage page 2023-03-06 01:46:40 +00:00
mbecker20
b7d1212a82 make resources links in account page 2023-03-05 21:50:15 +00:00
mbecker20
7d9d0a9fc4 add view of resources you can access on account page 2023-03-05 21:42:11 +00:00
beckerinj
ed9aef4321 add resources to account page 2023-03-05 16:33:40 -05:00
mbecker20
0aa638bdf4 only do daily update if servers not empty 2023-03-05 20:19:06 +00:00
mbecker20
0ec39d793d one page to view all permissions for user 2023-03-05 09:24:59 +00:00
113 changed files with 2861 additions and 938 deletions

8
.vscode/tasks.json vendored
View File

@@ -100,14 +100,6 @@
"cwd": "${workspaceFolder}/lib/monitor_client"
}
},
{
"type": "cargo",
"command": "publish",
"label": "publish monitor cli",
"options": {
"cwd": "${workspaceFolder}/cli"
}
},
{
"type": "shell",
"command": "docker compose up -d",

122
Cargo.lock generated
View File

@@ -602,7 +602,7 @@ dependencies = [
"serde_bytes",
"serde_json",
"time 0.3.20",
"uuid 1.3.0",
"uuid",
]
[[package]]
@@ -732,9 +732,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "core"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"async_timing_util",
@@ -753,7 +759,7 @@ dependencies = [
"hmac",
"jwt",
"monitor_helpers",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"mungos",
"periphery_client",
"serde",
@@ -987,10 +993,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "db_client"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"mungos",
]
@@ -1036,6 +1042,19 @@ dependencies = [
"syn",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version 0.4.0",
"syn",
]
[[package]]
name = "diff-struct"
version = "0.5.1"
@@ -1788,9 +1807,9 @@ dependencies = [
[[package]]
name = "mongodb"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a1df476ac9541b0e4fdc8e2cc48884e66c92c933cd17a1fd75e68caf75752e"
checksum = "a37fe10c1485a0cd603468e284a1a8535b4ecf46808f5f7de3639a1e1252dbf8"
dependencies = [
"async-trait",
"base64 0.13.1",
@@ -1798,21 +1817,22 @@ dependencies = [
"bson",
"chrono",
"derivative",
"derive_more",
"flate2",
"futures-core",
"futures-executor",
"futures-io",
"futures-util",
"hex",
"hmac",
"lazy_static",
"md-5",
"os_info",
"pbkdf2",
"percent-encoding",
"rand",
"rustc_version_runtime",
"rustls",
"rustls-pemfile 0.3.0",
"rustls-pemfile",
"serde",
"serde_bytes",
"serde_with 1.14.0",
@@ -1830,19 +1850,19 @@ dependencies = [
"trust-dns-proto",
"trust-dns-resolver",
"typed-builder",
"uuid 0.8.2",
"uuid",
"webpki-roots",
"zstd",
]
[[package]]
name = "monitor_cli"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"async_timing_util",
"clap",
"colored",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"rand",
"run_command",
"serde",
@@ -1854,12 +1874,12 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"envy",
"futures-util",
"monitor_types 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
@@ -1871,11 +1891,11 @@ dependencies = [
[[package]]
name = "monitor_helpers"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"axum",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"rand",
"serde",
"serde_json",
@@ -1884,7 +1904,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"async_timing_util",
@@ -1896,11 +1916,12 @@ dependencies = [
"envy",
"futures",
"monitor_helpers",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"run_command",
"serde",
"serde_derive",
"serde_json",
"svi",
"sysinfo",
"tokio",
"toml",
@@ -1909,7 +1930,7 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"bollard",
@@ -1926,9 +1947,9 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.7"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1c280239929526ffd057372240260b6a78e7f62bbbc061218a46f607f176f3e"
checksum = "2c1217ae77c37da0d97762577f4f2606745897de45b8950cc987b965a155dad4"
dependencies = [
"anyhow",
"bollard",
@@ -1945,11 +1966,12 @@ dependencies = [
[[package]]
name = "mungos"
version = "0.3.7"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc5132f76fafd19d773c68520ab427659a7b17484f2b4705f323b60f84e9d6c"
checksum = "2b8fabef8c6e29f25a64c58736ab58b191e28aa3bafc3e84a3b0e78a1ba00665"
dependencies = [
"anyhow",
"envy",
"futures",
"futures-util",
"mongodb",
@@ -2122,16 +2144,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_info"
version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0"
dependencies = [
"log",
"winapi",
]
[[package]]
name = "os_str_bytes"
version = "6.4.1"
@@ -2169,9 +2181,9 @@ dependencies = [
[[package]]
name = "pbkdf2"
version = "0.10.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
]
@@ -2184,11 +2196,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "periphery_client"
version = "0.2.7"
version = "0.2.12"
dependencies = [
"anyhow",
"futures-util",
"monitor_types 0.2.7",
"monitor_types 0.2.12",
"reqwest",
"serde",
"serde_json",
@@ -2498,20 +2510,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile 1.0.2",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
dependencies = [
"base64 0.13.1",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.2"
@@ -2862,6 +2865,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "svi"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1ee5e6cf961310f3b4ba037f6a3680fc264f9077e0b9f16a0d7cc8d0ade140"
dependencies = [
"thiserror",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -3012,9 +3024,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [
"autocfg",
"bytes",
@@ -3027,7 +3039,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -3095,6 +3107,7 @@ checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@@ -3419,15 +3432,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]]
name = "uuid"
version = "1.3.0"

View File

@@ -1,22 +0,0 @@
FROM rust:latest as builder
WORKDIR /builder
COPY ./periphery ./periphery
COPY ./lib/types ./lib/types
COPY ./lib/helpers ./lib/helpers
RUN cd periphery && cargo build --release
FROM debian:stable-slim
ARG DEPS_INSTALLER
COPY ./${DEPS_INSTALLER}.sh ./
RUN sh ./${DEPS_INSTALLER}.sh
COPY --from=builder /builder/periphery/target/release/periphery /usr/local/bin/periphery
EXPOSE 8000
CMD "periphery"

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_cli"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"

View File

@@ -62,6 +62,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
.map(|p| p.to_owned());
let config = CoreConfig {
title: String::from("monitor"),
host,
port,
jwt_valid_for,

View File

@@ -1,3 +1,6 @@
# optional. this will be the document title on the web page (shows up as text in the browser tab). default is 'monitor'
title = "monitor"
# this should be the url used to access monitor in browser, potentially behind DNS, eg https://monitor.mogh.tech or http://12.34.56.78:9000
host = "https://monitor.mogh.tech"

View File

@@ -1,6 +1,6 @@
[package]
name = "core"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,7 +11,7 @@ types = { package = "monitor_types", path = "../lib/types" }
db = { package = "db_client", path = "../lib/db_client" }
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
axum_oauth2 = { path = "../lib/axum_oauth2" }
tokio = { version = "1.25", features = ["full"] }
tokio = { version = "1.26", features = ["full"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio-util = "0.7"
axum = { version = "0.6", features = ["ws", "json"] }
@@ -19,7 +19,7 @@ axum-extra = { version = "0.5.0", features = ["spa"] }
tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.4.0", features = ["cors"] }
slack = { package = "slack_client_rs", version = "0.0.8" }
mungos = "0.3.3"
mungos = "0.3.14"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"

View File

@@ -1,10 +1,12 @@
use anyhow::{anyhow, Context};
use diff::Diff;
use helpers::{all_logs_success, to_monitor_name};
use mungos::doc;
use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
Deployment, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget,
Deployment, DeploymentWithContainerState, DockerContainerState, Log, Operation,
PermissionLevel, ServerStatus, ServerWithStatus, Update, UpdateStatus, UpdateTarget,
};
use crate::{
@@ -274,6 +276,157 @@ impl State {
Ok(new_deployment)
}
pub async fn rename_deployment(
&self,
deployment_id: &str,
new_name: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
if self.deployment_busy(&deployment_id).await {
return Err(anyhow!("deployment busy"));
}
{
let mut lock = self.deployment_action_states.lock().await;
let entry = lock.entry(deployment_id.to_string()).or_default();
entry.renaming = true;
}
let res = self
.rename_deployment_inner(deployment_id, new_name, user)
.await;
{
let mut lock = self.deployment_action_states.lock().await;
let entry = lock.entry(deployment_id.to_string()).or_default();
entry.renaming = false;
}
res
}
async fn rename_deployment_inner(
&self,
deployment_id: &str,
new_name: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
let start_ts = monitor_timestamp();
let deployment = self
.get_deployment_check_permissions(deployment_id, user, PermissionLevel::Update)
.await?;
let mut update = Update {
target: UpdateTarget::Deployment(deployment_id.to_string()),
operation: Operation::RenameDeployment,
start_ts,
status: UpdateStatus::InProgress,
operator: user.id.to_string(),
success: true,
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
let server_with_status = self.get_server(&deployment.server_id, user).await;
if server_with_status.is_err() {
update.logs.push(Log::error(
"get server",
format!(
"failed to get server info: {:?}",
server_with_status.as_ref().err().unwrap()
),
));
update.status = UpdateStatus::Complete;
update.end_ts = monitor_timestamp().into();
update.success = false;
self.update_update(update).await?;
return Err(server_with_status.err().unwrap());
}
let ServerWithStatus { server, status } = server_with_status.unwrap();
if status != ServerStatus::Ok {
update.logs.push(Log::error(
"check server status",
String::from("cannot rename deployment when periphery is disabled or unreachable"),
));
update.status = UpdateStatus::Complete;
update.end_ts = monitor_timestamp().into();
update.success = false;
self.update_update(update).await?;
return Err(anyhow!(
"cannot rename deployment when periphery is disabled or unreachable"
));
}
let deployment_state = self
.get_deployment_with_container_state(user, deployment_id)
.await;
if deployment_state.is_err() {
update.logs.push(Log::error(
"check deployment status",
format!(
"could not get current state of deployment: {:?}",
deployment_state.as_ref().err().unwrap()
),
));
update.status = UpdateStatus::Complete;
update.end_ts = monitor_timestamp().into();
update.success = false;
self.update_update(update).await?;
return Err(deployment_state.err().unwrap());
}
let DeploymentWithContainerState { state, .. } = deployment_state.unwrap();
if state != DockerContainerState::NotDeployed {
let log = self
.periphery
.container_rename(&server, &deployment.name, new_name)
.await;
if log.is_err() {
update.logs.push(Log::error(
"rename container",
format!("{:?}", log.as_ref().err().unwrap()),
));
update.status = UpdateStatus::Complete;
update.end_ts = monitor_timestamp().into();
update.success = false;
self.update_update(update).await?;
return Err(log.err().unwrap());
}
let log = log.unwrap();
if !log.success {
update.logs.push(log);
update.status = UpdateStatus::Complete;
update.end_ts = monitor_timestamp().into();
update.success = false;
self.update_update(update).await?;
return Err(anyhow!("rename container on periphery not successful"));
}
update.logs.push(log);
}
let res = self
.db
.deployments
.update_one(
deployment_id,
mungos::Update::<()>::Set(
doc! { "name": to_monitor_name(new_name), "updated_at": monitor_timestamp() },
),
)
.await
.context("failed to update deployment name on mongo");
if let Err(e) = res {
update
.logs
.push(Log::error("mongo update", format!("{e:?}")));
} else {
update.logs.push(Log::simple(
"mongo update",
String::from("updated name on mongo"),
))
}
update.end_ts = monitor_timestamp().into();
update.status = UpdateStatus::Complete;
update.success = all_logs_success(&update.logs);
self.update_update(update.clone()).await?;
Ok(update)
}
pub async fn reclone_deployment(
&self,
deployment_id: &str,

View File

@@ -1,7 +1,6 @@
use anyhow::{anyhow, Context};
use diff::Diff;
use futures_util::future::join_all;
use helpers::to_monitor_name;
use mungos::doc;
use types::{
monitor_timestamp,
@@ -49,7 +48,7 @@ impl State {
}
let start_ts = monitor_timestamp();
let server = Server {
name: to_monitor_name(name),
name: name.to_string(),
address,
permissions: [(user.id.clone(), PermissionLevel::Update)]
.into_iter()

View File

@@ -43,6 +43,12 @@ pub struct CopyDeploymentBody {
server_id: String,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
pub struct RenameDeploymentBody {
new_name: String,
}
#[typeshare]
#[derive(Deserialize)]
pub struct GetContainerLogQuery {
@@ -162,6 +168,24 @@ pub fn router() -> Router {
},
),
)
.route(
"/:id/rename",
patch(
|state: StateExtension,
user: RequestUserExtension,
deployment: Path<DeploymentId>,
body: Json<RenameDeploymentBody>| async move {
let update = spawn_request_action(async move {
state
.rename_deployment(&deployment.id, &body.new_name, &user)
.await
.map_err(handle_anyhow_error)
})
.await??;
response!(Json(update))
},
),
)
.route(
"/:id/reclone",
post(
@@ -324,7 +348,7 @@ pub fn router() -> Router {
}
impl State {
async fn get_deployment_with_container_state(
pub async fn get_deployment_with_container_state(
&self,
user: &RequestUser,
id: &str,
@@ -489,10 +513,10 @@ impl State {
if let Some(version) = update.version {
Ok(version.to_string())
} else {
Ok("latest".to_string())
Ok("unknown".to_string())
}
} else {
Ok("latest".to_string())
Ok("unknown".to_string())
}
} else {
let split = deployment
@@ -503,7 +527,7 @@ impl State {
if let Some(version) = split.get(1) {
Ok(version.to_string())
} else {
Ok("latest".to_string())
Ok("unknown".to_string())
}
}
}

View File

@@ -15,7 +15,9 @@ use typeshare::typeshare;
use crate::{
auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension},
response,
state::{State, StateExtension},
ResponseResult,
};
pub mod build;
@@ -35,22 +37,33 @@ struct UpdateDescriptionBody {
description: String,
}
#[derive(Deserialize)]
struct UserId {
id: String,
}
pub fn router() -> Router {
Router::new()
.route(
"/user",
get(|jwt, req| async { get_user(jwt, req).await.map_err(handle_anyhow_error) }),
"/title",
get(|state: StateExtension| async move { state.config.title.clone() }),
)
.route("/user", get(get_request_user))
.nest("/listener", github_listener::router())
.nest(
"/",
Router::new()
.route("/user/:id", get(get_user_at_id))
.route(
"/username/:id",
get(|state, user_id| async {
get_username(state, user_id)
get(|state: StateExtension, Path(UserId { id })| async move {
let user = state
.db
.get_user(&id)
.await
.map_err(handle_anyhow_error)
.context("failed to find user at id")
.map_err(handle_anyhow_error)?;
response!(Json(user.username))
}),
)
.route(
@@ -90,8 +103,11 @@ pub fn router() -> Router {
)
}
async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::Result<Json<User>> {
let mut user = jwt.authenticate(&req).await?;
async fn get_request_user(
Extension(jwt): JwtExtension,
req: Request<Body>,
) -> ResponseResult<Json<User>> {
let mut user = jwt.authenticate(&req).await.map_err(handle_anyhow_error)?;
user.password = None;
for secret in &mut user.secrets {
secret.hash = String::new();
@@ -99,23 +115,10 @@ async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::R
Ok(Json(user))
}
#[derive(Deserialize)]
struct UserId {
id: String,
}
async fn get_username(
state: StateExtension,
Path(UserId { id }): Path<UserId>,
) -> anyhow::Result<String> {
let user = state.db.get_user(&id).await?;
Ok(user.username)
}
async fn get_users(
state: StateExtension,
user: RequestUserExtension,
) -> Result<Json<Vec<User>>, (StatusCode, String)> {
) -> ResponseResult<Json<Vec<User>>> {
if user.is_admin {
let users = state
.db
@@ -137,8 +140,33 @@ async fn get_users(
}
}
async fn get_user_at_id(
state: StateExtension,
Path(UserId { id }): Path<UserId>,
user: RequestUserExtension,
) -> ResponseResult<Json<User>> {
if user.is_admin {
let mut user = state
.db
.users
.find_one_by_id(&id)
.await
.context("failed at query to get user from mongo")
.map_err(handle_anyhow_error)?
.ok_or(anyhow!(""))
.map_err(handle_anyhow_error)?;
user.password = None;
for secret in &mut user.secrets {
secret.hash = String::new();
}
Ok(Json(user))
} else {
Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string()))
}
}
// need to run requested actions in here to prevent them being dropped mid action when user disconnects prematurely
pub async fn spawn_request_action<A>(action: A) -> Result<A::Output, (StatusCode, String)>
pub async fn spawn_request_action<A>(action: A) -> ResponseResult<A::Output>
where
A: Future + Send + 'static,
A::Output: Send + 'static,

View File

@@ -284,7 +284,7 @@ async fn modify_user_create_server_permissions(
"user does not have permissions for this action (not admin)"
));
}
let user = state
let target_user = state
.db
.users
.find_one_by_id(&user_id)
@@ -312,7 +312,7 @@ async fn modify_user_create_server_permissions(
"modify user create server permissions",
format!(
"{update_type} create server permissions for {} (id: {})",
user.username, user.id
target_user.username, target_user.id
),
)],
start_ts: ts.clone(),
@@ -339,7 +339,7 @@ async fn modify_user_create_build_permissions(
"user does not have permissions for this action (not admin)"
));
}
let user = state
let target_user = state
.db
.users
.find_one_by_id(&user_id)
@@ -367,7 +367,7 @@ async fn modify_user_create_build_permissions(
"modify user create build permissions",
format!(
"{update_type} create build permissions for {} (id: {})",
user.username, user.id
target_user.username, target_user.id
),
)],
start_ts: ts.clone(),

View File

@@ -339,6 +339,20 @@ pub fn router() -> Router {
},
),
)
.route(
"/:id/secrets",
get(
|state: StateExtension,
user: RequestUserExtension,
Path(ServerId { id })| async move {
let vars = state
.get_available_secrets(&id, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(vars))
},
),
)
.route(
"/:id/action_state",
get(
@@ -356,7 +370,11 @@ pub fn router() -> Router {
}
impl State {
async fn get_server(&self, id: &str, user: &RequestUser) -> anyhow::Result<ServerWithStatus> {
pub async fn get_server(
&self,
id: &str,
user: &RequestUser,
) -> anyhow::Result<ServerWithStatus> {
let server = self
.get_server_check_permissions(id, user, PermissionLevel::Read)
.await?;
@@ -628,6 +646,18 @@ impl State {
Ok(docker_accounts)
}
async fn get_available_secrets(
&self,
id: &str,
user: &RequestUser,
) -> anyhow::Result<Vec<String>> {
let server = self
.get_server_check_permissions(id, user, PermissionLevel::Read)
.await?;
let vars = self.periphery.get_available_secrets(&server).await?;
Ok(vars)
}
async fn get_server_action_states(
&self,
id: String,

View File

@@ -12,7 +12,7 @@ use crate::{
state::{State, StateExtension},
};
const NUM_UPDATES_PER_PAGE: usize = 10;
const NUM_UPDATES_PER_PAGE: usize = 20;
pub fn router() -> Router {
Router::new().route(

View File

@@ -2,7 +2,7 @@
use ::helpers::get_socket_addr;
use auth::JwtClient;
use axum::Router;
use axum::{http::StatusCode, Router};
use state::State;
use tower_http::cors::{Any, CorsLayer};
@@ -16,8 +16,10 @@ mod monitoring;
mod state;
mod ws;
type ResponseResult<T> = Result<T, (StatusCode, String)>;
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
let (config, spa_router) = config::load();
println!("starting monitor core on port {}...", config.port);
@@ -40,6 +42,7 @@ async fn main() {
axum::Server::bind(&get_socket_addr(config.port))
.serve(app.into_make_service())
.await
.expect("monitor core axum server crashed");
.await?;
Ok(())
}

View File

@@ -412,8 +412,12 @@ impl State {
);
continue;
}
let servers = servers.unwrap();
if servers.is_empty() {
continue;
}
let mut blocks = vec![Block::header("INFO | daily update"), Block::divider()];
for (server, stats) in servers.unwrap() {
for (server, stats) in servers {
let region = if let Some(region) = &server.region {
format!(" | {region}")
} else {

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use axum::{
extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade},
http::StatusCode,
response::IntoResponse,
};
use futures_util::{SinkExt, StreamExt};
@@ -13,7 +12,7 @@ use tokio_tungstenite::tungstenite::Message;
use tokio_util::sync::CancellationToken;
use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery};
use crate::{auth::JwtExtension, state::StateExtension};
use crate::{auth::JwtExtension, state::StateExtension, ResponseResult};
#[derive(Deserialize)]
pub struct ServerId {
@@ -26,7 +25,7 @@ pub async fn ws_handler(
path: Path<ServerId>,
query: Query<SystemStatsQuery>,
ws: WebSocketUpgrade,
) -> Result<impl IntoResponse, (StatusCode, String)> {
) -> ResponseResult<impl IntoResponse> {
let server = state
.db
.get_server(&path.id)

View File

@@ -18,6 +18,7 @@
},
"dependencies": {
"@solidjs/router": "^0.6.0",
"@tanstack/solid-query": "^4.26.0",
"axios": "^1.2.1",
"js-file-download": "^0.4.12",
"lightweight-charts": "^3.8.0",

View File

@@ -8,8 +8,10 @@ const Deployment = lazy(() => import("./components/deployment/Deployment"));
const Server = lazy(() => import("./components/server/Server"));
const Build = lazy(() => import("./components/build/Build"));
const Users = lazy(() => import("./components/users/Users"));
const User = lazy(() => import("./components/users/User"));
const Stats = lazy(() => import("./components/stats/Stats"));
const Account = lazy(() => import("./components/Account"));
const Account = lazy(() => import("./components/account/Account"));
const Updates = lazy(() => import("./components/Updates"));
const App: Component = () => {
const { user } = useUser();
@@ -18,6 +20,7 @@ const App: Component = () => {
<Topbar />
<Routes>
<Route path="/" component={Home} />
<Route path="/updates" component={Updates} />
<Route path="/build/:id" component={Build} />
<Route path="/deployment/:id" component={Deployment} />
<Route path="/server/:id" component={Server} />
@@ -25,6 +28,7 @@ const App: Component = () => {
<Route path="/account" component={Account} />
<Show when={user().admin}>
<Route path="/users" component={Users} />
<Route path="/user/:id" component={User} />
</Show>
</Routes>
</div>

View File

@@ -69,10 +69,10 @@ const CopyMenu: Component<{
targetClass="blue"
content={() => (
<Grid placeItems="center">
<Flex alignItems="center">
<Flex class="full-width" alignItems="center">
<Input
placeholder="copy name"
class="card dark"
class="card dark full-width"
style={{ padding: "0.5rem" }}
value={newName()}
onEdit={setNewName}
@@ -87,6 +87,8 @@ const CopyMenu: Component<{
targetClass="blue"
targetStyle={{ display: "flex", gap: "0.5rem" }}
searchStyle={{ width: "100%" }}
menuClass="scroller"
menuStyle={{ "max-height": "40vh" }}
position="bottom right"
useSearch
/>

View File

@@ -3,8 +3,6 @@ import { client, pushNotification } from "..";
import { useAppState } from "../state/StateProvider";
import { UpdateTarget } from "../types";
import { useToggle } from "../util/hooks";
import ConfirmButton from "./shared/ConfirmButton";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import Loading from "./shared/loading/Loading";
import CenterMenu from "./shared/menu/CenterMenu";
@@ -19,7 +17,8 @@ const Description: Component<{
const [show, toggleShow] = useToggle();
const description = () => {
if (p.description) {
return p.description;
let [description] = p.description.split("\n");
return description;
} else {
return "add a description";
}
@@ -110,8 +109,7 @@ const DescriptionMenu: Component<{
placeholder="add a description"
value={desc()}
onEdit={setDesc}
onEnter={update_description}
style={{ width: "700px", "max-width": "90vw", padding: "1rem" }}
style={{ width: "900px", "max-width": "90vw", height: "70vh", padding: "1rem" }}
disabled={!p.userCanUpdate}
/>
<Show when={p.userCanUpdate}>

View File

@@ -0,0 +1,157 @@
import { A } from "@solidjs/router";
import {
Component,
createEffect,
createMemo,
createSignal,
For,
Show,
} from "solid-js";
import { OPERATIONS } from "..";
import { useAppDimensions } from "../state/DimensionProvider";
import { useAppState } from "../state/StateProvider";
import { Operation, Update as UpdateType, UpdateStatus } from "../types";
import { readableMonitorTimestamp, readableVersion } from "../util/helpers";
import Icon from "./shared/Icon";
import Input from "./shared/Input";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import Loading from "./shared/loading/Loading";
import Selector from "./shared/menu/Selector";
import UpdateMenu from "./update/UpdateMenu";
const Updates: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const { updates, usernames, name_from_update_target } = useAppState();
const [operation, setOperation] = createSignal<Operation>();
createEffect(() => {
if (operation()) {
updates.load([operation()!]);
} else {
updates.load();
}
});
const [search, setSearch] = createSignal("");
const filtered_updates = createMemo(() => {
return updates.collection()?.filter((u) => {
const name = name_from_update_target(u.target);
if (name.includes(search())) return true;
const username = usernames.get(u.operator);
if (username?.includes(search())) return true;
});
});
return (
<Grid class="full-width card shadow">
<Flex alignItems="center" justifyContent="space-between">
<h1>updates</h1>
<Flex alignItems="center">
<Input class="lightgrey" placeholder="search" onEdit={setSearch} />
<Selector
label={isMobile() ? undefined : "operation: "}
selected={operation() ? operation()! : "all"}
items={["all", ...OPERATIONS]}
onSelect={(o) =>
o === "all"
? setOperation(undefined)
: setOperation(o.replaceAll(" ", "_") as Operation)
}
targetClass="blue"
position="bottom right"
searchStyle={{ width: "15rem" }}
menuClass="scroller"
menuStyle={{ "max-height": "50vh" }}
useSearch
/>
</Flex>
</Flex>
<Show
when={updates.loaded()}
fallback={
<Flex justifyContent="center">
<Loading type="three-dot" />
</Flex>
}
>
<For each={filtered_updates()}>
{(update) => <Update update={update} />}
</For>
<Show when={!updates.noMore()}>
<button
class="grey full-width"
onClick={() =>
operation()
? updates.loadMore([operation()!])
: updates.loadMore()
}
>
load more
</button>
</Show>
</Show>
</Grid>
);
};
export default Updates;
const Update: Component<{ update: UpdateType }> = (p) => {
const { isMobile } = useAppDimensions();
const { usernames, name_from_update_target } = useAppState();
const name = () => name_from_update_target(p.update.target);
const operation = () => {
if (p.update.operation === Operation.BuildBuild) {
return `build ${readableVersion(p.update.version!)}`;
}
return `${p.update.operation.replaceAll("_", " ")}${
p.update.version ? " " + readableVersion(p.update.version) : ""
}`;
};
const link_to = () => {
return p.update.target.type === "System"
? "/"
: `/${p.update.target.type.toLowerCase()}/${p.update.target.id}`;
};
return (
<Flex
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<Flex
alignItems="center"
justifyContent="space-between"
style={{ width: isMobile() ? "100%" : undefined }}
>
<A style={{ padding: 0 }} href={link_to()}>
<h2 class="text-hover">{name()}</h2>
</A>
<div
style={{
color: !p.update.success ? "rgb(182, 47, 52)" : "inherit",
}}
>
{operation()}
</div>
<Show when={p.update.status === UpdateStatus.InProgress}>
<div style={{ opacity: 0.7 }}>(in progress)</div>
</Show>
</Flex>
<Flex
alignItems="center"
justifyContent="space-between"
style={{ width: isMobile() ? "100%" : undefined }}
>
<Flex gap="0.5rem">
<Icon type="user" />
<div>{usernames.get(p.update.operator)}</div>
</Flex>
<Flex alignItems="center">
<div style={{ "place-self": "center end" }}>
{readableMonitorTimestamp(p.update.start_ts)}
</div>
<UpdateMenu update={p.update} />
</Flex>
</Flex>
</Flex>
);
};

View File

@@ -0,0 +1,41 @@
import { Component, Show } from "solid-js";
import { useUser } from "../../state/UserProvider";
import { readableMonitorTimestamp, readableUserType } from "../../util/helpers";
import Flex from "../shared/layout/Flex";
import Resources from "./Resources";
import Secrets from "./Secrets";
const Account: Component<{}> = (p) => {
const { user, username } = useUser();
return (
<>
<Flex
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
alignItems="center"
justifyContent="space-between"
>
<h1>{username()}</h1>
<Flex>
<Show when={user().admin}>
<div class="dimmed">admin</div>
</Show>
<Flex gap="0.5rem">
<div class="dimmed">type:</div>
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
</Flex>
<Flex gap="0.5rem">
<div class="dimmed">created:</div>
<div>{readableMonitorTimestamp(user().created_at!)}</div>
</Flex>
</Flex>
</Flex>
<Secrets />
<Show when={!user().admin}>
<Resources />
</Show>
</>
);
};
export default Account;

View File

@@ -0,0 +1,124 @@
import { A } from "@solidjs/router";
import { Component, createMemo, createSignal, For } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { getId } from "../../util/helpers";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
const Resources: Component<{}> = (p) => {
const { user, user_id } = useUser();
const { isMobile } = useAppDimensions();
const { builds, deployments, servers } = useAppState();
const [search, setSearch] = createSignal("");
const _servers = createMemo(() => {
return servers.filterArray((s) => {
if (!s.server.name.includes(search())) return false;
const p = s.server.permissions?.[user_id()];
return p ? p !== PermissionLevel.None : false;
});
});
const _deployments = createMemo(() => {
return deployments.filterArray((d) => {
if (!d.deployment.name.includes(search())) return false;
const p = d.deployment.permissions?.[user_id()];
return p ? p !== PermissionLevel.None : false;
});
});
const _builds = createMemo(() => {
return builds.filterArray((b) => {
if (!b.name.includes(search())) return false;
const p = b.permissions?.[user_id()];
return p ? p !== PermissionLevel.None : false;
});
});
return (
<>
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<h1>servers</h1>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_servers()}>
{(item) => (
<A
class="card light shadow"
href={`/server/${getId(item.server)}`}
style={{
"justify-content": "space-between",
width: "100%",
"box-sizing": "border-box",
}}
>
<Grid gap="0.25rem">
<h2>{item.server.name}</h2>
<div class="dimmed">{item.server.region || "unknown region"}</div>
</Grid>
<div>{item.server.permissions?.[user_id()] || "none"}</div>
</A>
)}
</For>
</Grid>
</Grid>
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<h1>deployments</h1>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_deployments()}>
{(item) => (
<A
href={`/deployment/${getId(item.deployment)}`}
class="card light shadow"
style={{
"justify-content": "space-between",
width: "100%",
"box-sizing": "border-box",
}}
>
<Grid gap="0.25rem">
<h2>{item.deployment.name}</h2>
<div class="dimmed">
{servers.get(item.deployment.server_id)?.server.name ||
"unknown"}
</div>
</Grid>
<div>{item.deployment.permissions?.[user_id()] || "none"}</div>
</A>
)}
</For>
</Grid>
</Grid>
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<h1>builds</h1>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_builds()}>
{(item) => (
<A
href={`/build/${getId(item)}`}
class="card light shadow"
style={{
"justify-content": "space-between",
width: "100%",
"box-sizing": "border-box",
}}
>
<h2>{item.name}</h2>
<div>{item.permissions?.[user_id()] || "none"}</div>
</A>
)}
</For>
</Grid>
</Grid>
</>
);
};
export default Resources;

View File

@@ -1,79 +1,81 @@
import { Component, For, Match, Show, Switch } from "solid-js";
import { Component, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import { client, pushNotification } from "..";
import { useUser } from "../state/UserProvider";
import { copyToClipboard, readableMonitorTimestamp } from "../util/helpers";
import { useToggle } from "../util/hooks";
import ConfirmButton from "./shared/ConfirmButton";
import Icon from "./shared/Icon";
import Input from "./shared/Input";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import Loading from "./shared/loading/Loading";
import CenterMenu from "./shared/menu/CenterMenu";
import Selector from "./shared/menu/Selector";
import { client, pushNotification } from "../..";
import { useUser } from "../../state/UserProvider";
import { copyToClipboard, readableMonitorTimestamp } from "../../util/helpers";
import { useToggle } from "../../util/hooks";
import ConfirmButton from "../shared/ConfirmButton";
import Icon from "../shared/Icon";
import Input from "../shared/Input";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Loading from "../shared/loading/Loading";
import CenterMenu from "../shared/menu/CenterMenu";
import Selector from "../shared/menu/Selector";
const Account: Component<{}> = (p) => {
const { user, reloadUser } = useUser();
const Secrets: Component<{}> = (p) => {
const { user, reloadUser } = useUser();
const [showCreate, toggleShowCreate] = useToggle();
return (
<>
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Flex justifyContent="space-between">
<h1>api secrets</h1>
<CenterMenu
show={showCreate}
toggleShow={toggleShowCreate}
targetClass="green"
title="create secret"
target={<Icon type="plus" />}
content={() => <CreateNewMenu />}
position="center"
/>
</Flex>
<For each={user().secrets}>
{(secret) => (
<Flex
class="card dark shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<h2>{secret.name}</h2>
<Flex alignItems="center">
<Flex gap="0.25rem">
<div style={{ opacity: 0.7 }}>created:</div>
<div>{readableMonitorTimestamp(secret.created_at)}</div>
</Flex>
<Flex gap="0.25rem">
<div style={{ opacity: 0.7 }}>expires:</div>
<div>{secret.expires ? readableMonitorTimestamp(secret.expires) : "never"}</div>
</Flex>
<ConfirmButton
class="red"
onConfirm={() =>
client.delete_api_secret(secret.name).then(reloadUser)
}
>
<Icon type="trash" />
</ConfirmButton>
return (
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Flex justifyContent="space-between">
<h1>api secrets</h1>
<CenterMenu
show={showCreate}
toggleShow={toggleShowCreate}
targetClass="green"
title="create secret"
target={<Icon type="plus" />}
content={() => <CreateNewSecretMenu />}
position="center"
/>
</Flex>
<For each={user().secrets}>
{(secret) => (
<Flex
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<h2>{secret.name}</h2>
<Flex alignItems="center">
<Flex gap="0.25rem">
<div style={{ opacity: 0.7 }}>created:</div>
<div>{readableMonitorTimestamp(secret.created_at)}</div>
</Flex>
<Flex gap="0.25rem">
<div style={{ opacity: 0.7 }}>expires:</div>
<div>
{secret.expires
? readableMonitorTimestamp(secret.expires)
: "never"}
</div>
</Flex>
<ConfirmButton
class="red"
onConfirm={() =>
client.delete_api_secret(secret.name).then(reloadUser)
}
>
<Icon type="trash" />
</ConfirmButton>
</Flex>
)}
</For>
</Grid>
</>
</Flex>
)}
</For>
</Grid>
);
};
}
export default Account;
export default Secrets;
const EXPIRE_LENGTHS = ["30 days", "90 days", "1 year", "never"] as const;
type ExpireLength = typeof EXPIRE_LENGTHS[number];
const CreateNewMenu = () => {
const CreateNewSecretMenu = () => {
const { reloadUser } = useUser();
const [info, setInfo] = createStore<{
name: string;
@@ -167,4 +169,4 @@ function createExpires(length: ExpireLength) {
const add_days = length === "30 days" ? 30 : length === "90 days" ? 90 : 365;
const add_ms = add_days * 24 * 60 * 60 * 1000;
return new Date(Date.now() + add_ms).toISOString();
}
}

View File

@@ -45,7 +45,10 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
set(...args);
set("updated", true);
};
const server = () => build.server_id ? servers.get(build.server_id) : undefined;
const server = () =>
builds.get(params.id)?.server_id
? servers.get(builds.get(params.id)!.server_id!)
: undefined;
const load = () => {
// console.log("load build");

View File

@@ -1,10 +1,22 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { useParams } from "@solidjs/router";
import {
Component,
createEffect,
createResource,
createSignal,
For,
Show,
} from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { ServerStatus } from "../../../../types";
import {
parseDotEnvToEnvVars,
parseEnvVarseToDotEnv,
} from "../../../../util/helpers";
import { useToggle } from "../../../../util/hooks";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import CenterMenu from "../../../shared/menu/CenterMenu";
import TextArea from "../../../shared/TextArea";
import { useConfig } from "../Provider";
@@ -36,9 +48,11 @@ const BuildArgs: Component<{}> = (p) => {
};
const EditBuildArgs: Component<{}> = (p) => {
const { aws_builder_config, builds, serverSecrets } = useAppState();
const [show, toggle] = useToggle();
const [buildArgs, setBuildArgs] = createSignal("");
const { build, setBuild } = useConfig();
const params = useParams();
const { build, setBuild, server } = useConfig();
createEffect(() => {
setBuildArgs(
parseEnvVarseToDotEnv(
@@ -56,6 +70,23 @@ const EditBuildArgs: Component<{}> = (p) => {
}
toggle();
};
const secrets = () => {
if (builds.get(params.id)?.server_id) {
return (
serverSecrets.get(
builds.get(params.id)!.server_id!,
server()?.status || ServerStatus.NotOk
) || []
);
} else if (build.aws_config) {
const ami_name =
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
return ami_name
? aws_builder_config()?.available_ami_accounts![ami_name].secrets || []
: [];
} else return [];
};
let ref: HTMLTextAreaElement;
return (
<CenterMenu
show={show}
@@ -69,19 +100,44 @@ const EditBuildArgs: Component<{}> = (p) => {
</button>
)}
content={() => (
<TextArea
class="scroller"
placeholder="VARIABLE=value #example"
value={buildArgs()}
onEdit={setBuildArgs}
style={{
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",
}}
spellcheck={false}
/>
<Grid>
<Show when={secrets()?.length || 0 > 0}>
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
<h2 class="dimmed">secrets:</h2>
<For each={secrets()}>
{(secret) => (
<button
class="blue"
onClick={() =>
setBuildArgs(
(args) =>
args.slice(0, ref.selectionStart) +
`[[${secret}]]` +
args.slice(ref.selectionStart, undefined)
)
}
>
{secret}
</button>
)}
</For>
</Flex>
</Show>
<TextArea
ref={ref! as any}
class="scroller"
placeholder="VARIABLE=value #example"
value={buildArgs()}
onEdit={setBuildArgs}
style={{
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",
}}
spellcheck={false}
/>
</Grid>
)}
/>
);

View File

@@ -11,6 +11,8 @@ import BuildArgs from "./BuildArgs";
import Version from "./Version";
import Repo from "./Repo";
import WebhookUrl from "./WebhookUrl";
import ExtraArgs from "./ExtraArgs";
import UseBuildx from "./UseBuildx";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
@@ -23,6 +25,8 @@ const BuildConfig: Component<{}> = (p) => {
<Docker />
<CliBuild />
<BuildArgs />
<ExtraArgs />
<UseBuildx />
<Show when={userCanUpdate()}>
<WebhookUrl />
</Show>

View File

@@ -1,4 +1,8 @@
import { Component, createEffect, createResource, createSignal, Show } from "solid-js";
import {
Component,
createResource,
Show,
} from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { ServerStatus } from "../../../../types";
@@ -10,21 +14,11 @@ import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Docker: Component<{}> = (p) => {
const { aws_builder_config } = useAppState();
const { aws_builder_config, serverDockerAccounts, docker_organizations } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [dockerOrgs] = createResource(() => client.get_docker_organizations());
const [peripheryDockerAccounts, setPeripheryDockerAccounts] =
createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_docker_accounts(build.server_id!)
.then(setPeripheryDockerAccounts);
}
});
const dockerAccounts = () => {
if (build.server_id) {
return peripheryDockerAccounts() || [];
return serverDockerAccounts.get(build.server_id, server()?.status || ServerStatus.NotOk) || [];
} else if (build.aws_config) {
const ami_name =
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
@@ -87,7 +81,7 @@ const Docker: Component<{}> = (p) => {
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={build.docker_organization || (dockerOrgs() || []).length > 0}>
<Show when={build.docker_organization || (docker_organizations() || []).length > 0}>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
@@ -97,7 +91,7 @@ const Docker: Component<{}> = (p) => {
<Selector
targetClass="blue"
selected={build.docker_organization || "none"}
items={["none", ...(dockerOrgs() || [])]}
items={["none", ...(docker_organizations() || [])]}
onSelect={(account) => {
setBuild(
"docker_organization",

View File

@@ -0,0 +1,59 @@
import { Component, For, Show } from "solid-js";
import Icon from "../../../shared/Icon";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
const ExtraArgs: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const onAdd = () => {
setBuild("docker_build_args", "extra_args", (extra_args: any) => [
...extra_args,
"",
]);
};
const onRemove = (index: number) => {
setBuild("docker_build_args", "extra_args", (extra_args) =>
extra_args!.filter((_, i) => i !== index)
);
};
return (
<Grid class="config-item shadow">
<Flex justifyContent="space-between" alignItems="center">
<h1>extra args</h1>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={[...build.docker_build_args!.extra_args!.keys()]}>
{(_, index) => (
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<Input
placeholder="--extra-arg=value"
value={build.docker_build_args!.extra_args![index()]}
style={{ width: "80%" }}
onEdit={(value) =>
setBuild("docker_build_args", "extra_args", index(), value)
}
disabled={!userCanUpdate()}
/>
<Show when={userCanUpdate()}>
<button class="red" onClick={() => onRemove(index())}>
<Icon type="minus" />
</button>
</Show>
</Flex>
)}
</For>
</Grid>
);
};
export default ExtraArgs;

View File

@@ -1,29 +1,24 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { Component, Show } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import { combineClasses } from "../../../../util/helpers";
import { useAppState } from "../../../../state/StateProvider";
import { client } from "../../../..";
import { ServerStatus } from "../../../../types";
import Selector from "../../../shared/menu/Selector";
const Repo: Component<{}> = (p) => {
const { aws_builder_config } = useAppState();
const { aws_builder_config, serverGithubAccounts } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [peripheryGithubAccounts, setPeripheryGithubAccounts] =
createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_github_accounts(build.server_id!)
.then(setPeripheryGithubAccounts);
}
});
const githubAccounts = () => {
if (build.server_id) {
return peripheryGithubAccounts() || [];
return (
serverGithubAccounts.get(
build.server_id,
server()?.status || ServerStatus.NotOk
) || []
);
} else if (build.aws_config) {
const ami_name =
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;

View File

@@ -0,0 +1,30 @@
import { Component, Show } from "solid-js";
import Flex from "../../../shared/layout/Flex";
import { useConfig } from "../Provider";
const UseBuildx: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const use_buildx = () => build.docker_build_args?.use_buildx || false;
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>use buildx</h1>
<Show
when={userCanUpdate()}
fallback={<div>{use_buildx() ? "enabled" : "disabled"}</div>}
>
<button
class={use_buildx() ? "green" : "red"}
onClick={() => setBuild("docker_build_args", "use_buildx", (c) => !c)}
>
{use_buildx() ? "enabled" : "disabled"}
</button>
</Show>
</Flex>
);
};
export default UseBuildx;

View File

@@ -1,6 +1,5 @@
import { Component, createSignal, Show } from "solid-js";
import { version_to_string } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import { useConfig } from "../Provider";

View File

@@ -1,5 +1,6 @@
import { Component, createResource, Show } from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { getId } from "../../../../util/helpers";
import CopyClipboard from "../../../shared/CopyClipboard";
import Flex from "../../../shared/layout/Flex";
@@ -8,13 +9,11 @@ import Loading from "../../../shared/loading/Loading";
import { useConfig } from "../Provider";
const ListenerUrl: Component<{}> = (p) => {
const { github_webhook_base_url } = useAppState();
const { build } = useConfig();
const [github_base_url] = createResource(() =>
client.get_github_webhook_base_url()
);
const listenerUrl = () => {
if (github_base_url()) {
return `${github_base_url()}/api/listener/build/${getId(build)}`;
if (github_webhook_base_url()) {
return `${github_webhook_base_url()}/api/listener/build/${getId(build)}`;
}
};
return (

View File

@@ -1,4 +1,4 @@
import { Component, createResource, Show } from "solid-js";
import { Component, createResource, createSignal, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import {
@@ -19,6 +19,8 @@ import { A, useParams } from "@solidjs/router";
import { client } from "../..";
import CopyMenu from "../CopyMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
import Loading from "../shared/loading/Loading";
import { AutofocusInput } from "../shared/Input";
const Header: Component<{}> = (p) => {
const { deployments, servers, builds } = useAppState();
@@ -41,24 +43,38 @@ const Header: Component<{}> = (p) => {
const [deployed_version] = createResource(() =>
client.get_deployment_deployed_version(params.id)
);
const image = () => {
const derived_image = () => {
if (deployment().deployment.build_id) {
const build = builds.get(deployment().deployment.build_id!)!;
if (deployment().state === DockerContainerState.NotDeployed) {
const version = deployment().deployment.build_version
? readableVersion(deployment().deployment.build_version!).replaceAll(
"v",
""
)
: "latest";
return `${build.name}:${version}`;
} else {
return deployed_version() && `${build.name}:${deployed_version()}`;
}
const build = builds.get(deployment().deployment.build_id!);
if (build === undefined) return "unknown";
const version =
deployment().state === DockerContainerState.NotDeployed
? deployment().deployment.build_version
? readableVersion(
deployment().deployment.build_version!
).replaceAll("v", "")
: "latest"
: deployed_version() || "unknown";
return `${build.name}:${version}`;
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
};
const image = () => {
if (deployment().state === DockerContainerState.NotDeployed) {
return derived_image();
} else if (deployment().container?.image) {
if (deployment().container!.image.includes("sha256:")) {
return derived_image();
}
let [account, image] = deployment().container!.image.split("/");
return image ? image : account;
} else {
return "unknown";
}
};
const [editingName, setEditingName] = createSignal(false);
const [updatingName, setUpdatingName] = createSignal(false);
return (
<>
<Grid
@@ -75,8 +91,46 @@ const Header: Component<{}> = (p) => {
>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center">
<h1>{deployment()!.deployment.name}</h1>
<div style={{ opacity: 0.7 }}>{image()}</div>
<Show
when={editingName()}
fallback={
<button
onClick={() => setEditingName(true)}
style={{ padding: 0 }}
>
<h1>{deployment()!.deployment.name}</h1>
</button>
}
>
<Show
when={!updatingName()}
fallback={<Loading type="three-dot" />}
>
<AutofocusInput
value={deployment().deployment.name}
placeholder={deployment().deployment.name}
onEnter={async (new_name) => {
setUpdatingName(true);
await client.rename_deployment(params.id, new_name);
setEditingName(false);
setUpdatingName(false);
}}
onBlur={() => setEditingName(false)}
/>
</Show>
</Show>
<Show
when={deployment().deployment.build_id}
fallback={<div style={{ opacity: 0.7 }}>{image()}</div>}
>
<A
href={`/build/${deployment().deployment.build_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{image()}
</A>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<Flex alignItems="center">

View File

@@ -3,16 +3,24 @@ import {
Accessor,
createContext,
createEffect,
createResource,
createSignal,
onCleanup,
ParentComponent,
Resource,
useContext,
} from "solid-js";
import { createStore, SetStoreFunction } from "solid-js/store";
import { client, pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { Deployment, Operation, PermissionLevel, ServerStatus, ServerWithStatus } from "../../../../types";
import {
Deployment,
Operation,
PermissionLevel,
ServerStatus,
ServerWithStatus,
} from "../../../../types";
import { getId } from "../../../../util/helpers";
type ConfigDeployment = Deployment & {
@@ -28,7 +36,7 @@ type State = {
server: () => ServerWithStatus | undefined;
reset: () => void;
save: () => void;
networks: Accessor<any[]>;
networks: Resource<any[]>;
userCanUpdate: () => boolean;
};
@@ -87,19 +95,20 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
};
createEffect(load);
const [networks, setNetworks] = createSignal<any[]>([]);
const server = () => servers.get(deployments.get(params.id)!.deployment.server_id);
createEffect(() => {
const server = () =>
servers.get(deployments.get(params.id)!.deployment.server_id);
const [networks] = createResource(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_docker_networks(deployments.get(params.id)!.deployment.server_id)
.then(setNetworks);
}
return client.get_docker_networks(
deployments.get(params.id)!.deployment.server_id
);
} else return [];
});
const save = () => {
setDeployment("updating", true);
client.update_deployment(deployment).catch(e => {
client.update_deployment(deployment).catch((e) => {
console.error(e);
pushNotification("bad", "update deployment failed");
setDeployment("updating", false);

View File

@@ -1,5 +1,5 @@
import { Component, createEffect, createSignal } from "solid-js";
import { client } from "../../../../..";
import { Component } from "solid-js";
import { useAppState } from "../../../../../state/StateProvider";
import { ServerStatus } from "../../../../../types";
import { combineClasses } from "../../../../../util/helpers";
import Flex from "../../../../shared/layout/Flex";
@@ -7,25 +7,21 @@ import Selector from "../../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const DockerAccount: Component<{}> = (p) => {
const { serverDockerAccounts } = useAppState();
const { deployment, setDeployment, server, userCanUpdate } = useConfig();
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_docker_accounts(deployment.server_id)
.then(setDockerAccounts);
}
});
const dockerAccounts = () =>
serverDockerAccounts.get(
deployment.server_id,
server()?.status || ServerStatus.NotOk
) || [];
const when_none_selected = () => {
if (deployment.build_id) {
return "same as build"
return "same as build";
} else {
return "none"
return "none";
}
}
const accounts = () => {
return [when_none_selected(), ...(dockerAccounts() || [])];
}
};
const accounts = () => [when_none_selected(), ...dockerAccounts()];
return (
<Flex
class={combineClasses("config-item shadow")}
@@ -37,10 +33,13 @@ const DockerAccount: Component<{}> = (p) => {
<Selector
targetClass="blue"
items={accounts()}
selected={deployment.docker_run_args.docker_account || when_none_selected()}
selected={
deployment.docker_run_args.docker_account || when_none_selected()
}
onSelect={(account) =>
setDeployment("docker_run_args", {
docker_account: account === when_none_selected() ? undefined : account,
docker_account:
account === when_none_selected() ? undefined : account,
})
}
position="bottom right"

View File

@@ -1,4 +1,14 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import {
Component,
createEffect,
createResource,
createSignal,
For,
Show,
} from "solid-js";
import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import { ServerStatus } from "../../../../../types";
import {
combineClasses,
parseDotEnvToEnvVars,
@@ -36,9 +46,10 @@ const Env: Component<{}> = (p) => {
};
const EditDotEnv: Component<{}> = (p) => {
const { serverSecrets } = useAppState();
const [show, toggle] = useToggle();
const [dotenv, setDotEnv] = createSignal("");
const { deployment, setDeployment } = useConfig();
const { deployment, setDeployment, server } = useConfig();
createEffect(() => {
setDotEnv(
parseEnvVarseToDotEnv(
@@ -56,6 +67,12 @@ const EditDotEnv: Component<{}> = (p) => {
}
toggle();
};
const secrets = () =>
serverSecrets.get(
deployment.server_id,
server()?.status || ServerStatus.NotOk
) || [];
let ref: HTMLTextAreaElement;
return (
<CenterMenu
show={show}
@@ -69,19 +86,44 @@ const EditDotEnv: Component<{}> = (p) => {
</button>
)}
content={() => (
<TextArea
class="scroller"
placeholder="VARIABLE=value #example"
value={dotenv()}
onEdit={setDotEnv}
style={{
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",
}}
spellcheck={false}
/>
<Grid>
<Show when={secrets()?.length || 0 > 0}>
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
<h2 class="dimmed">secrets:</h2>
<For each={secrets()}>
{(secret) => (
<button
class="blue"
onClick={() =>
setDotEnv(
(env) =>
env.slice(0, ref.selectionStart) +
`[[${secret}]]` +
env.slice(ref.selectionStart, undefined)
)
}
>
{secret}
</button>
)}
</For>
</Flex>
</Show>
<TextArea
ref={ref! as any}
class="scroller"
placeholder="VARIABLE=value #example"
value={dotenv()}
onEdit={setDotEnv}
style={{
width: "1000px",
"max-width": "90vw",
height: "80vh",
padding: "1rem",
}}
spellcheck={false}
/>
</Grid>
)}
/>
);

View File

@@ -1,8 +1,11 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { Component, createResource, Show } from "solid-js";
import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import { BuildVersionsReponse } from "../../../../../types";
import { combineClasses, string_to_version, version_to_string } from "../../../../../util/helpers";
import {
combineClasses,
string_to_version,
version_to_string,
} from "../../../../../util/helpers";
import Input from "../../../../shared/Input";
import Flex from "../../../../shared/layout/Flex";
import Selector from "../../../../shared/menu/Selector";
@@ -11,10 +14,9 @@ import { useConfig } from "../Provider";
const Image: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const { builds } = useAppState();
const [versions, setVersions] = createSignal<BuildVersionsReponse[]>([]);
createEffect(() => {
const [versions] = createResource(() => {
if (deployment.build_id) {
client.get_build_versions(deployment.build_id).then(setVersions);
return client.get_build_versions(deployment.build_id);
}
});
return (
@@ -72,7 +74,9 @@ const Image: Component<{}> = (p) => {
}
items={[
"latest",
...versions().map((v) => `v${version_to_string(v.version)}`),
...(versions()?.map(
(v) => `v${version_to_string(v.version)}`
) || []),
]}
onSelect={(version) => {
if (version === "latest") {

View File

@@ -1,5 +1,5 @@
import { Component, createResource, Show } from "solid-js";
import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import { getId } from "../../../../../util/helpers";
import CopyClipboard from "../../../../shared/CopyClipboard";
import Flex from "../../../../shared/layout/Flex";
@@ -8,13 +8,11 @@ import Loading from "../../../../shared/loading/Loading";
import { useConfig } from "../Provider";
const WebhookUrl: Component<{}> = (p) => {
const { github_webhook_base_url } = useAppState();
const { deployment } = useConfig();
const [github_base_url] = createResource(() =>
client.get_github_webhook_base_url()
);
const listenerUrl = () => {
if (github_base_url()) {
return `${github_base_url()}/api/listener/deployment/${getId(
if (github_webhook_base_url()) {
return `${github_webhook_base_url()}/api/listener/deployment/${getId(
deployment
)}`;
}

View File

@@ -1,5 +1,5 @@
import { Component, createEffect, createSignal } from "solid-js";
import { client } from "../../../../..";
import { Component, createResource } from "solid-js";
import { useAppState } from "../../../../../state/StateProvider";
import { ServerStatus } from "../../../../../types";
import { combineClasses } from "../../../../../util/helpers";
import Input from "../../../../shared/Input";
@@ -9,15 +9,13 @@ import Selector from "../../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Git: Component<{}> = (p) => {
const { serverGithubAccounts } = useAppState();
const { deployment, server, setDeployment, userCanUpdate } = useConfig();
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_github_accounts(deployment.server_id)
.then(setGithubAccounts);
}
});
const githubAccounts = () =>
serverGithubAccounts.get(
deployment.server_id,
server()?.status || ServerStatus.NotOk
) || [];
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>github config</h1>
@@ -56,7 +54,7 @@ const Git: Component<{}> = (p) => {
<Selector
targetClass="blue"
selected={deployment.github_account || "none"}
items={["none", ...githubAccounts()!]}
items={["none", ...githubAccounts()]}
onSelect={(account) => {
setDeployment(
"github_account",

View File

@@ -1,126 +1,59 @@
import { Component, createMemo, For, Show } from "solid-js";
import { Accessor, Component, createMemo } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { DockerContainerState, ServerStatus } from "../../types";
import Grid from "../shared/layout/Grid";
import Flex from "../shared/layout/Flex";
import PieChart, { PieChartSection } from "../shared/PieChart";
import { COLORS } from "../../style/colors";
import { useAppDimensions } from "../../state/DimensionProvider";
const PIE_CHART_SIZE = 250;
const Summary: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const deployentCount = useDeploymentCount();
const serverCount = useServerCount();
return (
<Grid class="card shadow" gridTemplateRows="auto 1fr 1fr 1fr">
<h1>summary</h1>
<DeploymentsSummary />
<ServersSummary />
<BuildsSummary />
<Grid
class="full-size"
gridTemplateColumns={isMobile() ? "1fr" : "1fr 1fr"}
>
<Grid class="card shadow full-size" placeItems="center">
<div
style={{
width: `${PIE_CHART_SIZE}px`,
height: `${PIE_CHART_SIZE}px`,
}}
>
<PieChart title="deployments" sections={deployentCount()} />
</div>
</Grid>
<Grid class="card shadow full-size" placeItems="center">
<div
style={{
width: `${PIE_CHART_SIZE}px`,
height: `${PIE_CHART_SIZE}px`,
}}
>
<PieChart title="servers" sections={serverCount()} />
</div>
</Grid>
</Grid>
);
};
export default Summary;
const SummaryItem: Component<{
title: string;
metrics: Array<{ title: string; class: string; count?: number }>;
}> = (p) => {
return (
<Flex
class="card light shadow wrap"
justifyContent="space-between"
alignItems="center"
>
<h2>{p.title}</h2>
<Flex class="wrap">
<For each={p.metrics}>
{(metric) => (
<Show when={metric?.count && metric.count > 0}>
<Flex gap="0.4rem" alignItems="center">
<div>{metric.title}</div>
<h2 class={metric.class}>{metric.count}</h2>
</Flex>
</Show>
)}
</For>
</Flex>
</Flex>
);
};
const BuildsSummary = () => {
const { builds } = useAppState();
return (
<SummaryItem
title="builds"
metrics={[
{ title: "total", class: "text-green", count: builds.ids()?.length },
]}
/>
);
};
const DeploymentsSummary = () => {
const deployentCount = useDeploymentCount();
return (
<SummaryItem
title="deployments"
metrics={[
{
title: "total",
class: "text-green",
count: deployentCount().total,
},
{
title: "running",
class: "text-green",
count: deployentCount().running,
},
{
title: "stopped",
class: "text-red",
count: deployentCount().stopped,
},
{
title: "not deployed",
class: "text-blue",
count: deployentCount().notDeployed,
},
{
title: "unknown",
class: "text-blue",
count: deployentCount().unknown,
},
]}
/>
);
};
const ServersSummary = () => {
const serverCount = useServerCount();
return (
<SummaryItem
title="servers"
metrics={[
{ title: "total", class: "text-green", count: serverCount().total },
{ title: "healthy", class: "text-green", count: serverCount().healthy },
{
title: "unhealthy",
class: "text-red",
count: serverCount().unhealthy,
},
{
title: "disabled",
class: "text-blue",
count: serverCount().disabled,
},
]}
/>
);
};
function useDeploymentCount() {
function useDeploymentCount(): Accessor<PieChartSection[]> {
const { deployments } = useAppState();
const count = createMemo(() => {
const ids = deployments.ids();
if (!ids)
return { total: 0, running: 0, stopped: 0, notDeployed: 0, unknown: 0 };
return [
{ title: "running", amount: 0, color: COLORS.textgreen },
{ title: "stopped", amount: 0, color: COLORS.textred },
{ title: "not deployed", amount: 0, color: COLORS.textblue },
{ title: "unknown", amount: 0, color: COLORS.textorange },
];
let running = 0;
let stopped = 0;
let notDeployed = 0;
@@ -137,16 +70,26 @@ function useDeploymentCount() {
unknown++;
}
}
return { total: ids.length, running, stopped, notDeployed, unknown };
return [
{ title: "running", amount: running, color: COLORS.textgreen },
{ title: "stopped", amount: stopped, color: COLORS.textred },
{ title: "not deployed", amount: notDeployed, color: COLORS.textblue },
{ title: "unknown", amount: unknown, color: COLORS.textorange },
];
});
return count;
}
function useServerCount() {
function useServerCount(): Accessor<PieChartSection[]> {
const { servers } = useAppState();
const count = createMemo(() => {
const ids = servers.ids();
if (!ids) return { total: 0, healthy: 0, unhealthy: 0, disabled: 0 };
if (!ids)
return [
{ title: "healthy", amount: 0, color: COLORS.textgreen },
{ title: "unhealthy", amount: 0, color: COLORS.textred },
{ title: "disabled", amount: 0, color: COLORS.textblue },
];
let healthy = 0;
let unhealthy = 0;
let disabled = 0;
@@ -160,7 +103,50 @@ function useServerCount() {
unhealthy++;
}
}
return { total: ids.length, healthy, unhealthy, disabled };
return [
{ title: "healthy", amount: healthy, color: COLORS.textgreen },
{ title: "unhealthy", amount: unhealthy, color: COLORS.textred },
{ title: "disabled", amount: disabled, color: COLORS.textblue },
];
});
return count;
}
// const SummaryItem: Component<{
// title: string;
// metrics: Array<{ title: string; class: string; count?: number }>;
// }> = (p) => {
// return (
// <Flex
// class="card light shadow wrap"
// justifyContent="space-between"
// alignItems="center"
// >
// <h2>{p.title}</h2>
// <Flex class="wrap">
// <For each={p.metrics}>
// {(metric) => (
// <Show when={metric?.count && metric.count > 0}>
// <Flex gap="0.4rem" alignItems="center">
// <div>{metric.title}</div>
// <h2 class={metric.class}>{metric.count}</h2>
// </Flex>
// </Show>
// )}
// </For>
// </Flex>
// </Flex>
// );
// };
// const BuildsSummary = () => {
// const { builds } = useAppState();
// return (
// <SummaryItem
// title="builds"
// metrics={[
// { title: "total", class: "text-green", count: builds.ids()?.length },
// ]}
// />
// );
// };

View File

@@ -58,6 +58,7 @@ const Builds: Component<{}> = (p) => {
style={{ width: "100%", padding: "0.5rem" }}
/>
<Selector
label={<div class="dimmed">sort by:</div>}
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}

View File

@@ -51,6 +51,7 @@ const Groups: Component<{}> = (p) => {
style={{ width: "100%", padding: "0.5rem" }}
/>
<Selector
label={<div class="dimmed">sort by:</div>}
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}

View File

@@ -2,14 +2,14 @@ import { ParentComponent, createContext, useContext } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useLocalStorage } from "../../../util/hooks";
export const TREE_SORTS = ["name", "created"] as const;
export const TREE_SORTS = ["name", "created at"] as const;
export type TreeSortType = typeof TREE_SORTS[number];
const value = () => {
const { servers, groups, builds } = useAppState();
const [sort, setSort] = useLocalStorage<TreeSortType>(
TREE_SORTS[0],
"home-sort-v1"
"home-sort-v2"
);
const server_sorter = () => {
if (!servers.loaded()) return () => 0;

View File

@@ -42,7 +42,8 @@ const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
onEdit={setServerFilter}
style={{ width: "100%", padding: "0.5rem" }}
/>
<Selector
<Selector
label={<div class="dimmed">sort by:</div>}
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}

View File

@@ -14,18 +14,9 @@ import UpdateMenu from "../../update/UpdateMenu";
import s from "./update.module.scss";
const Update: Component<{ update: UpdateType }> = (p) => {
const { deployments, servers, builds, usernames } = useAppState();
const name = () => {
if (p.update.target.type === "Deployment" && deployments.loaded()) {
return deployments.get(p.update.target.id!)?.deployment.name || "deleted";
} else if (p.update.target.type === "Server" && servers.loaded()) {
return servers.get(p.update.target.id)?.server.name || "deleted";
} else if (p.update.target.type === "Build" && builds.loaded()) {
return builds.get(p.update.target.id)?.name || "deleted";
} else {
return "monitor";
}
};
const { usernames, name_from_update_target } =
useAppState();
const name = () => name_from_update_target(p.update.target);
const operation = () => {
if (p.update.operation === Operation.BuildBuild) {
return `build ${readableVersion(p.update.version!)}`;

View File

@@ -1,4 +1,6 @@
import { A } from "@solidjs/router";
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { OPERATIONS } from "../../..";
import { useAppState } from "../../../state/StateProvider";
import { Operation } from "../../../types";
import Flex from "../../shared/layout/Flex";
@@ -7,10 +9,6 @@ import Loading from "../../shared/loading/Loading";
import Selector from "../../shared/menu/Selector";
import Update from "./Update";
const OPERATIONS = Object.values(Operation)
.filter((e) => e !== "none" && !e.includes("user"))
.map((e) => e.replaceAll("_", " "));
const Updates: Component<{}> = () => {
const { updates } = useAppState();
const [operation, setOperation] = createSignal<Operation>();
@@ -24,8 +22,11 @@ const Updates: Component<{}> = () => {
return (
<Grid class="card shadow" style={{ "flex-grow": 1 }}>
<Flex alignItems="center" justifyContent="space-between">
<h1>updates</h1>
<A href="/updates" style={{ padding: 0 }}>
<h1>updates</h1>
</A>
<Selector
label="operation: "
selected={operation() ? operation()! : "all"}
items={["all", ...OPERATIONS]}
onSelect={(o) =>
@@ -50,7 +51,7 @@ const Updates: Component<{}> = () => {
}
>
<Grid class="updates-container-small scroller">
<For each={updates.collection()!}>
<For each={updates.collection()}>
{(update) => <Update update={update} />}
</For>
<Show when={!updates.noMore()}>

View File

@@ -34,9 +34,9 @@
width: 100%;
}
.ServerButton:hover {
background-color: rgba(c.$lightblue, 0.5);
}
// .ServerButton:hover {
// background-color: rgba(c.$lightblue, 0.5);
// }
.Deployments {
background-color: c.$lightgrey;

View File

@@ -1,8 +1,7 @@
import { Component, createResource, Show } from "solid-js";
import { Component, createResource, createSignal, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { combineClasses, getId, serverStatusClass } from "../../util/helpers";
import ConfirmButton from "../shared/ConfirmButton";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
@@ -15,6 +14,7 @@ import { client } from "../..";
import Loading from "../shared/loading/Loading";
import HoverMenu from "../shared/menu/HoverMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
import Input, { AutofocusInput } from "../shared/Input";
const Header: Component<{}> = (p) => {
const { servers } = useAppState();
@@ -25,6 +25,8 @@ const Header: Component<{}> = (p) => {
const { isMobile, isSemiMobile } = useAppDimensions();
const [showUpdates, toggleShowUpdates] =
useLocalStorageToggle("show-updates");
const [editingName, setEditingName] = createSignal(false);
const [updatingName, setUpdatingName] = createSignal(false);
const userCanUpdate = () =>
user().admin ||
server().server.permissions![getId(user())] === PermissionLevel.Update;
@@ -50,7 +52,38 @@ const Header: Component<{}> = (p) => {
}}
>
<Flex alignItems="center" justifyContent="space-between">
<h1>{server().server.name}</h1>
<Show
when={editingName()}
fallback={
<button
onClick={() => setEditingName(true)}
style={{ padding: 0 }}
>
<h1>{server().server.name}</h1>
</button>
}
>
<Show
when={!updatingName()}
fallback={<Loading type="three-dot" />}
>
<AutofocusInput
value={server().server.name}
placeholder={server().server.name}
onEnter={async (new_name) => {
setUpdatingName(true);
await client.update_server({
...server().server,
name: new_name,
});
setEditingName(false);
setUpdatingName(false);
}}
onBlur={() => setEditingName(false)}
/>
</Show>
</Show>
<Show when={userCanUpdate()}>
<Flex alignItems="center">
<div class={serverStatusClass(server().status)}>{status()}</div>

View File

@@ -7,7 +7,6 @@ import { readableStorageAmount } from "../../../util/helpers";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Loading from "../../shared/loading/Loading";
import HoverMenu from "../../shared/menu/HoverMenu";
const Info: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();

View File

@@ -1,36 +1,46 @@
import { A } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { Component, createResource, Show } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { DockerContainerState } from "../../types";
import {
combineClasses,
deploymentStateClass,
readableVersion,
} from "../../util/helpers";
import Circle from "../shared/Circle";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import s from "./serverchildren.module.scss";
const Deployment: Component<{ id: string }> = (p) => {
const { deployments, builds } = useAppState();
const deployment = () => deployments.get(p.id)!;
const [deployed_version] = createResource(() =>
client.get_deployment_deployed_version(p.id)
);
const derived_image = () => {
if (deployment().deployment.build_id) {
const build = builds.get(deployment().deployment.build_id!);
if (build === undefined) return "unknown";
const version =
deployment().state === DockerContainerState.NotDeployed
? deployment().deployment.build_version
? readableVersion(
deployment().deployment.build_version!
).replaceAll("v", "")
: "latest"
: deployed_version() || "unknown";
return `${build.name}:${version}`;
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
};
const image = () => {
if (deployment().state === DockerContainerState.NotDeployed) {
if (deployment().deployment.build_id) {
const build = builds.get(deployment().deployment.build_id!);
if (build === undefined) return "unknown"
const version = deployment().deployment.build_version
? readableVersion(deployment().deployment.build_version!).replaceAll(
"v",
""
)
: "latest";
return `${build.name}:${version}`;
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
return derived_image();
} else if (deployment().container?.image) {
if (deployment().container!.image.includes("sha256:")) {
return derived_image();
}
let [account, image] = deployment().container!.image.split("/");
return image ? image : account;
} else {
@@ -39,7 +49,15 @@ const Deployment: Component<{ id: string }> = (p) => {
};
return (
<Show when={deployment()}>
<A href={`/deployment/${p.id}`} class="card hoverable" style={{ width: "100%", "justify-content": "space-between", padding: "0.5rem" }}>
<A
href={`/deployment/${p.id}`}
class="card hoverable"
style={{
width: "100%",
"justify-content": "space-between",
padding: "0.5rem",
}}
>
<Grid gap="0">
<h2>{deployment().deployment.name}</h2>
<div style={{ opacity: 0.7 }}>{image()}</div>

View File

@@ -0,0 +1,32 @@
import { Component, JSX } from "solid-js";
const CheckBox: Component<{
label: JSX.Element;
checked: boolean;
toggle: () => void;
}> = (p) => {
return (
<button
class="blue"
style={{ gap: "0.5rem" }}
onClick={(e) => {
e.stopPropagation();
p.toggle();
}}
>
{p.label}
<input
type="checkbox"
checked={p.checked}
style={{
width: "fit-content",
margin: 0,
appearance: "auto",
"-webkit-appearance": "checkbox",
}}
/>
</button>
);
};
export default CheckBox;

View File

@@ -1,10 +1,11 @@
import { Component, JSX, Show } from "solid-js";
import { Component, JSX, onMount, Show } from "solid-js";
const Input: Component<
{
onEdit?: (value: string) => void;
onConfirm?: (value: string) => void;
onEnter?: (value: string) => void;
onEsc?: (value: string) => void;
disabled?: boolean;
} & JSX.InputHTMLAttributes<HTMLInputElement> &
JSX.HTMLAttributes<HTMLDivElement>
@@ -14,17 +15,37 @@ const Input: Component<
<input
{...p}
onInput={(e) => p.onEdit && p.onEdit(e.currentTarget.value)}
onBlur={(e) => p.onConfirm && p.onConfirm(e.currentTarget.value)}
onKeyDown={p.onKeyDown || ((e) => {
if (e.key === "Enter") {
p.onEnter
? p.onEnter(e.currentTarget.value)
: e.currentTarget.blur();
}
})}
onBlur={
p.onBlur || ((e) => p.onConfirm && p.onConfirm(e.currentTarget.value))
}
onKeyDown={
p.onKeyDown ||
((e) => {
if (e.key === "Enter") {
p.onEnter && p.onEnter(e.currentTarget.value);
} else if (e.key === "Escape") {
p.onEsc ? p.onEsc(e.currentTarget.value) : e.currentTarget.blur();
}
})
}
/>
</Show>
);
};
export default Input;
export const AutofocusInput: Component<
{
onEdit?: (value: string) => void;
onConfirm?: (value: string) => void;
onEnter?: (value: string) => void;
onEsc?: (value: string) => void;
disabled?: boolean;
} & JSX.InputHTMLAttributes<HTMLInputElement> &
JSX.HTMLAttributes<HTMLDivElement>
> = (p) => {
let ref: HTMLInputElement;
onMount(() => setTimeout(() => ref?.focus(), 100));
return <Input ref={ref! as any} {...p} />;
};

View File

@@ -0,0 +1,241 @@
import {
Component,
createEffect,
createMemo,
createSignal,
For,
onCleanup,
onMount,
Show,
} from "solid-js";
import Grid from "./layout/Grid";
export type PieChartSection = {
title: string;
amount: number;
color: string;
};
const PieChart: Component<{
title: string;
sections: (PieChartSection | undefined)[];
donutProportion?: number;
seperation?: number;
}> = (p) => {
let ref: HTMLDivElement;
let canvas: HTMLCanvasElement;
const [chart, setChart] = createSignal<PieChartCanvas>();
const [selected, setSelected] = createSignal<number>();
const sections = createMemo(
() =>
p.sections
.filter((s) => s && s.amount > 0)
.sort((a, b) => {
if (a!.amount > b!.amount) {
return -1;
} else {
return 1;
}
}) as PieChartSection[]
);
const onResize = () =>
chart()?.updateCanvasDim(ref.clientWidth, ref.clientHeight);
onMount(() => {
const chart = new PieChartCanvas(
canvas,
sections(),
setSelected,
p.donutProportion,
p.seperation
);
setChart(chart);
onResize();
window.addEventListener("resize", onResize);
});
onCleanup(() => {
window.removeEventListener("resize", onResize);
});
createEffect(() => {
chart()?.updateSections(sections());
chart()?.draw();
});
return (
<Grid
ref={ref!}
style={{
width: "100%",
height: "100%",
"box-sizing": "border-box",
position: "relative",
}}
>
<Grid
placeItems="center"
style={{
position: "absolute",
width: "100%",
height: "100%",
}}
>
<div style={{ display: "grid", gap: "0.2rem" }}>
<h2 style={{ "margin-bottom": "0.5rem" }}>{p.title}</h2>
<For each={sections()}>
{(section, index) => (
<div
style={{
display: "flex",
gap: "0.5rem",
"justify-content": "space-between",
}}
>
<div
style={{
opacity: selected() === index() ? 1 : 0.7,
}}
>
{section.title}:
</div>
<div style={{ color: section.color }}>{section.amount}</div>
</div>
)}
</For>
<Show when={sections().length === 0}>
<div style={{ opacity: 0.7 }}>none</div>
</Show>
</div>
</Grid>
<canvas ref={canvas!} style={{ "z-index": 1 }} />
</Grid>
);
};
export default PieChart;
type InnerPieChartSection = PieChartSection & {
startAngle: number;
endAngle: number;
};
class PieChartCanvas {
sections: InnerPieChartSection[];
selected?: number;
cx = 0;
cy = 0;
r = 0;
constructor(
private canvas: HTMLCanvasElement,
sections: PieChartSection[],
private onSelectedUpdate: (selected: number | undefined) => void,
private donutProportion = 0.8,
private seperation = 0.02 // private initAngle = -Math.PI / 8
) {
this.sections = [];
this.updateSections(sections);
this.canvas.addEventListener("mousemove", (e) => this.onMouseOver(e));
this.canvas.addEventListener("mouseout", () => {
this.selected = undefined;
this.onSelectedUpdate(this.selected);
this.draw();
});
}
draw() {
const ctx = this.canvas.getContext("2d");
if (!ctx) {
return;
}
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (const segIndex in this.sections) {
const seg = this.sections[segIndex];
const outerStartAngle = seg.startAngle + this.seperation;
const outerEndAngle = seg.endAngle - this.seperation;
const innerStartAngle =
seg.startAngle + this.seperation / this.donutProportion;
const innerEndAngle =
seg.endAngle - this.seperation / this.donutProportion;
ctx.fillStyle =
Number(segIndex) === this.selected ? seg.color : `${seg.color}B3`;
ctx.beginPath();
ctx.moveTo(
this.cx + this.donutProportion * this.r * Math.cos(innerStartAngle),
this.cy + this.donutProportion * this.r * Math.sin(innerStartAngle)
);
ctx.lineTo(
this.cx + this.r * Math.cos(outerStartAngle),
this.cy + this.r * Math.sin(outerStartAngle)
);
ctx.arc(this.cx, this.cy, this.r, outerStartAngle, outerEndAngle);
ctx.lineTo(
this.cx + this.donutProportion * this.r * Math.cos(innerEndAngle),
this.cy + this.donutProportion * this.r * Math.sin(innerEndAngle)
);
ctx.arc(
this.cx,
this.cy,
this.donutProportion * this.r,
innerEndAngle,
innerStartAngle,
true
);
ctx.fill();
}
}
updateSections(sections: PieChartSection[]) {
let startAngle = 0;
const total = sections.reduce((prev, curr) => prev + curr.amount, 0);
this.sections = sections.map((s) => {
const proportion = s.amount / total;
const rads = Math.PI * 2 * proportion;
startAngle += rads;
return {
...s,
startAngle: startAngle - rads,
endAngle: startAngle,
};
});
this.draw();
}
onMouseOver(e: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
const x = e.x - rect.x - this.cx;
const y = e.y - rect.y - this.cy;
if (x * x + y * y > this.r * this.r) {
this.selected = undefined;
this.onSelectedUpdate(this.selected);
this.draw();
return;
}
const atan = Math.atan(y / x);
const angle =
x >= 0 ? (y >= 0 ? atan : 2 * Math.PI + atan) : Math.PI + atan;
for (const secIndex in this.sections) {
if (angle < this.sections[secIndex].endAngle) {
this.selected = Number(secIndex);
this.onSelectedUpdate(this.selected);
this.draw();
break;
}
}
}
updateCanvasDim(width: number, height: number) {
if (width <= 0 || height <= 0) return;
this.canvas.width = width;
this.canvas.height = height;
this.cx = this.canvas.width / 2;
this.cy = this.canvas.height / 2;
this.r =
this.canvas.width < this.canvas.height
? this.canvas.width / 2 - 2
: this.canvas.height / 2 - 2;
this.draw();
}
}

View File

@@ -5,6 +5,7 @@ const TextArea: Component<
onEdit?: (value: string) => void;
onConfirm?: (value: string) => void;
onEnter?: (value: string) => void;
shiftDisablesOnEnter?: boolean;
disabled?: boolean;
} & JSX.InputHTMLAttributes<HTMLTextAreaElement> &
JSX.HTMLAttributes<HTMLDivElement>
@@ -16,7 +17,11 @@ const TextArea: Component<
onInput={(e) => p.onEdit && p.onEdit(e.currentTarget.value)}
onBlur={(e) => p.onConfirm && p.onConfirm(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && p.onEnter) {
if (
e.key === "Enter" &&
p.onEnter &&
(!p.shiftDisablesOnEnter || !e.shiftKey)
) {
e.preventDefault();
p.onEnter(e.currentTarget.value);
}

View File

@@ -76,7 +76,7 @@ const Child: Component<{
>
<Grid
class={combineClasses(s.Menu, "shadow")}
style={{ padding: (p.padding as any) || "1rem", ...p.style }}
style={{ padding: (p.padding as any) || "2rem", ...p.style }}
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>

View File

@@ -37,10 +37,10 @@ const Selector: Component<{
}> = (p) => {
const [show, toggle] = useToggle();
const [search, setSearch] = createSignal("");
let ref: HTMLInputElement | undefined;
let search_ref: HTMLInputElement | undefined;
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected);
createEffect(() => {
if (show()) setTimeout(() => ref?.focus(), 200);
if (show()) setTimeout(() => search_ref?.focus(), 200);
});
return (
<Show
@@ -70,7 +70,7 @@ const Selector: Component<{
<>
<Show when={p.useSearch}>
<Input
ref={ref}
ref={search_ref}
placeholder="search"
value={search()}
onEdit={setSearch}

View File

@@ -23,6 +23,7 @@
width: fit-content;
/* border: solid 1px rgba(2, 107, 121, 0.25); */
background-color: c.$grey;
border: solid c.$darkgrey 2px;
z-index: 21;
border-radius: 0.25rem;
box-sizing: border-box;
@@ -142,6 +143,11 @@ $anim-time: 350ms;
background-color: rgba(0, 0, 0, 0.4);
}
.CenterMenuHeader {
border-bottom: solid rgba(c.$lightgrey, 0.9) 2px;
padding-bottom: 1rem;
}
.SelectorItem:hover {
background-color: c.$lightgrey;
}

View File

@@ -9,7 +9,7 @@
.TabTitle {
display: grid;
place-items: center;
padding: 0.25rem 0.5rem;
padding: 0.75rem;
border-radius: 0rem;
cursor: pointer;
font-size: 1rem;

View File

@@ -1,35 +1,41 @@
import { LineData, SingleValueData } from "lightweight-charts";
import { Accessor, Component, For, ParentComponent, Show } from "solid-js";
import {
Accessor,
Component,
For,
JSXElement,
ParentComponent,
Show,
} from "solid-js";
import { COLORS } from "../../style/colors";
import { SystemStats, SystemStatsRecord } from "../../types";
import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../util/helpers";
import { useLocalStorage, useLocalStorageToggle } from "../../util/hooks";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";
import s from "./stats.module.scss";
export const COLORS = {
blue: "#184e9f",
orange: "#ac5c36",
purple: "#5A0B4D",
green: "#41764c",
red: "#952E23",
};
const CHART_HEIGHT = "250px";
const SMALL_CHART_HEIGHT = "150px";
const SingleStatChart: Component<{
line?: LightweightValue[];
header: string;
headerRight?: JSXElement;
label: string;
color: string;
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
return (
<StatChartContainer header={p.header} small={p.small}>
<StatChartContainer
header={p.header}
headerRight={p.headerRight}
small={p.small}
>
<Show when={p.line}>
<LightweightChart
class={s.LightweightChart}
@@ -52,23 +58,25 @@ const SingleStatChart: Component<{
const StatChartContainer: ParentComponent<{
header: string;
headerRight?: JSXElement;
small?: boolean;
}> = (p) => {
return (
<Grid
gap="0.5rem"
class="card shadow"
class="card shadow full-width"
style={{
height: "fit-content",
width: "100%",
"box-sizing": "border-box",
"padding-top": "0.5rem",
"padding-bottom": "0.2rem",
}}
>
<Show when={!p.small} fallback={<div>{p.header}</div>}>
<h2>{p.header}</h2>
</Show>
<Flex justifyContent="space-between">
<Show when={!p.small} fallback={<div>{p.header}</div>}>
<h2>{p.header}</h2>
</Show>
{p.headerRight}
</Flex>
{p.children}
</Grid>
);
@@ -160,20 +168,42 @@ export const MemChart: Component<{
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
const [absolute, toggleAbsolute] = useLocalStorageToggle("stats-mem-mode-v2");
const symbol = () => (absolute() ? "GiB" : "%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.mem_used_gb) / s.mem_total_gb,
};
});
if (absolute()) {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: s.mem_used_gb,
};
});
} else {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.mem_used_gb) / s.mem_total_gb,
};
});
}
};
return (
<SingleStatChart
header="memory"
label="mem %"
headerRight={
<button
class="green"
style={{ padding: "0.2rem" }}
onClick={toggleAbsolute}
>
{symbol()}
</button>
}
label={`mem ${symbol()}`}
color={COLORS.green}
line={line()}
small={p.small}
@@ -187,20 +217,43 @@ export const DiskChart: Component<{
small?: boolean;
disableScroll?: boolean;
}> = (p) => {
const [absolute, toggleAbsolute] =
useLocalStorageToggle("stats-disk-mode-v2");
const symbol = () => (absolute() ? "GiB" : "%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.disk.used_gb) / s.disk.total_gb,
};
});
if (absolute()) {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: s.disk.used_gb,
};
});
} else {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(
(s as SystemStatsRecord).ts || (s as SystemStats).refresh_ts
),
value: (100 * s.disk.used_gb) / s.disk.total_gb,
};
});
}
};
return (
<SingleStatChart
header="disk"
label="disk %"
headerRight={
<button
class="orange"
style={{ padding: "0.2rem" }}
onClick={toggleAbsolute}
>
{symbol()}
</button>
}
label={`disk ${symbol()}`}
color={COLORS.orange}
line={line()}
small={p.small}

View File

@@ -10,7 +10,7 @@ import {
Show,
Switch,
} from "solid-js";
import { client, MAX_PAGE_WIDTH } from "../..";
import { client } from "../..";
import { SystemProcess, SystemStats } from "../../types";
import { convert_timelength_to_ms } from "../../util/helpers";
import { useLocalStorage } from "../../util/hooks";

View File

@@ -29,20 +29,23 @@ const HistoricalStats: Component<{
const params = useParams();
const { timelength, page } = useStatsState();
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
createEffect(() => {
client
const [loading, setLoading] = createSignal(false);
createEffect(async () => {
setLoading(true);
const stats = await client
.get_server_stats_history(params.id, {
interval: timelength(),
page: page(),
limit: 500,
networks: true,
components: true,
})
.then(setStats);
});
setStats(stats);
setLoading(false);
});
return (
<Grid class={s.Content} placeItems="start center">
<Show when={stats()} fallback={<Loading type="three-dot" />}>
<Show when={stats() && !loading()} fallback={<Loading type="three-dot" />}>
<SimpleTabs
localStorageKey="historical-stats-view-v3"
defaultSelected="basic"

View File

@@ -1,12 +1,19 @@
import { useParams } from "@solidjs/router";
import { ParentComponent, createContext, useContext, createSignal, createResource } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { SystemInformation, Timelength } from "../../types";
import { useLocalStorage } from "../../util/hooks";
export enum StatsView {
Current = "current",
Historical = "historical",
Info = "info"
}
const value = () => {
const params = useParams();
const [view, setView] = useLocalStorage("current", "stats-view-v1");
const [view, setView] = useLocalStorage(StatsView.Current, "stats-view-v2");
const [timelength, setTimelength] = useLocalStorage(
Timelength.OneMinute,
"stats-timelength-v3"
@@ -16,12 +23,7 @@ const value = () => {
`${params.id}-stats-poll-v3`
);
const [page, setPage] = createSignal(0);
// const [wsOpen, setWsOpen] = createSignal(false);
const [sysInfo] = createResource<SystemInformation>(() =>
client.get_server_system_info(params.id)
);
return {
sysInfo,
view,
setView,
timelength,

View File

@@ -1,20 +1,16 @@
import { A, useParams } from "@solidjs/router";
import {
Component,
Match,
Show,
Switch,
} from "solid-js";
import { MAX_PAGE_WIDTH } from "../..";
import { Component, createResource, For, Match, Show, Switch } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { ServerStatus, Timelength } from "../../types";
import { readableStorageAmount } from "../../util/helpers";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Selector from "../shared/menu/Selector";
import CurrentStats from "./CurrentStats";
import HistoricalStats from "./HistoricalStats";
import { StatsProvider, useStatsState } from "./Provider";
import { StatsProvider, useStatsState, StatsView } from "./Provider";
const TIMELENGTHS = [
Timelength.FifteenSeconds,
@@ -38,115 +34,182 @@ const Stats = () => {
const StatsComp: Component<{}> = () => {
const { view } = useStatsState();
return (
<Grid
style={{
width: "100%",
"box-sizing": "border-box",
}}
>
<Flex justifyContent="space-between" style={{ width: "100%" }}>
<Header />
<SysInfo />
</Flex>
<Show when={view() === "historical"}>
<Grid class="full-width">
<Header />
<Show when={view() === StatsView.Historical}>
<Flex alignItems="center" style={{ "place-self": "center" }}>
<PageManager />
</Flex>
</Show>
<Switch>
<Match when={view() === "current"}>
<Match when={view() === StatsView.Current}>
<CurrentStats />
</Match>
<Match when={view() === "historical"}>
<Match when={view() === StatsView.Historical}>
<HistoricalStats />
</Match>
<Match when={view() === StatsView.Info}>
<SysInfo />
</Match>
</Switch>
</Grid>
);
};
export const Header: Component<{}> = (p) => {
const { servers } = useAppState();
const { servers, serverInfo } = useAppState();
const params = useParams();
const server = () => servers.get(params.id);
const { view, setView, timelength, setTimelength, setPage, pollRate, setPollRate } = useStatsState();
const {
view,
setView,
timelength,
setTimelength,
setPage,
pollRate,
setPollRate,
} = useStatsState();
const sysInfo = () => serverInfo.get(params.id);
return (
<Flex alignItems="center" style={{ height: "fit-content" }}>
<h1>{server()?.server.name}</h1>
<A
href={`/server/${params.id}`}
class={
server()?.server.enabled
? server()?.status === ServerStatus.Ok
? "green"
: "red"
: "blue"
}
style={{
"border-radius": ".35rem",
transition: "background-color 125ms ease-in-out",
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{server()?.status.replaceAll("_", " ").toUpperCase()}
</A>
<Grid gap="0" gridTemplateColumns="repeat(2, 1fr)">
<button
class={view() === "current" ? "selected" : "grey"}
style={{ width: "100%" }}
onClick={() => setView("current")}
>
current
</button>
<button
class={view() === "historical" ? "selected" : "grey"}
style={{ width: "100%" }}
onClick={() => setView("historical")}
>
historical
</button>
</Grid>
<Show when={view() === "historical"}>
<Selector
targetClass="grey"
selected={timelength()}
items={TIMELENGTHS}
onSelect={(selected) => {
setPage(0);
setTimelength(selected as Timelength);
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" style={{ height: "fit-content" }}>
<h1>{server()?.server.name}</h1>
<A
href={`/server/${params.id}`}
class={
server()?.server.enabled
? server()?.status === ServerStatus.Ok
? "green"
: "red"
: "blue"
}
style={{
"border-radius": ".35rem",
transition: "background-color 125ms ease-in-out",
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{server()?.status.replaceAll("_", " ").toUpperCase()}
</A>
<Selector
targetClass="blue"
selected={view()}
items={Object.values(StatsView)}
onSelect={(v) => setView(v as StatsView)}
position="bottom right"
/>
</Show>
<Show when={view() === "current"}>
<Flex gap="0.5rem" alignItems="center">
<div>poll:</div>
<Show when={view() === "historical"}>
<Selector
targetClass="grey"
selected={timelength()}
items={TIMELENGTHS}
itemMap={(t) => t.replaceAll("-", " ")}
itemClass="full-width"
onSelect={(selected) => {
setPage(0);
setTimelength(selected as Timelength);
}}
position="bottom right"
/>
</Show>
<Show when={view() === "current"}>
<Selector
targetClass="grey"
label="poll: "
selected={pollRate()}
items={[Timelength.OneSecond, Timelength.FiveSeconds]}
onSelect={(selected) => {
setPollRate(selected as Timelength);
}}
position="bottom right"
/>
</Flex>
</Show>
</Show>
</Flex>
<Flex>
<div>{sysInfo()?.cpu_brand}</div>
<div>
{sysInfo()?.core_count} core
{sysInfo()?.core_count && sysInfo()?.core_count! > 1 ? "s" : ""}
</div>
</Flex>
</Flex>
);
};
const SysInfo = () => {
const { sysInfo } = useStatsState();
const { serverInfo } = useAppState();
const params = useParams();
const sysInfo = () => serverInfo.get(params.id);
const [stats] = createResource(() =>
client.get_server_stats(params.id, { disks: true })
);
const os_cards = () => {
return [
{
label: "os",
info: sysInfo()?.os,
},
{
label: "kernel",
info: sysInfo()?.kernel,
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
const cpu_cards = () => {
return [
{
label: "cpu",
info: sysInfo()?.cpu_brand,
},
{
label: "core count",
info: `${sysInfo()?.core_count} cores`,
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
const stats_cards = () => {
return [
{
label: "mem",
info:
stats()?.mem_total_gb &&
readableStorageAmount(stats()?.mem_total_gb!),
},
{
label: "disk",
info:
stats()?.disk.total_gb &&
readableStorageAmount(stats()?.disk.total_gb!),
},
].filter((i) => i.info) as Array<{ label: string; info: string }>;
};
return (
<Flex
alignItems="center"
style={{ "place-self": "center end", width: "fit-content" }}
>
<div>{sysInfo()?.os}</div>
{/* <div>{sysInfo()?.kernel}</div> */}
<div>{sysInfo()?.cpu_brand}</div>
<div>{sysInfo()?.core_count} cores</div>
<Grid class="full-width" placeItems="center">
<Show when={sysInfo()?.host_name}>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<InfoCard info={{ label: "hostname", info: sysInfo()?.host_name! }} />
</Grid>
</Show>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={os_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={cpu_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
<Grid class="card full-width" style={{ "max-width": "700px" }}>
<For each={stats_cards()}>{(i) => <InfoCard info={i} />}</For>
</Grid>
</Grid>
);
};
const InfoCard: Component<{ info: { label: string; info: string } }> = (p) => {
return (
<Flex class="full-width" justifyContent="space-between">
<h2>{p.info.label}</h2>
<div>{p.info.info}</div>
</Flex>
);
};

View File

@@ -16,8 +16,8 @@ import Circle from "../../shared/Circle";
import { ControlledTabs } from "../../shared/tabs/Tabs";
import { useAppDimensions } from "../../../state/DimensionProvider";
import Grid from "../../shared/layout/Grid";
import { A, useNavigate } from "@solidjs/router";
import { Build, ServerStatus } from "../../../types";
import { A } from "@solidjs/router";
import { ServerStatus } from "../../../types";
const mobileStyle: JSX.CSSProperties = {
// position: "fixed",
@@ -58,7 +58,8 @@ export const Search: Component<{}> = (p) => {
>
<Input
ref={inputRef}
class={s.SearchInput}
class="lightgrey"
style={{ width: "30rem" }}
placeholder="search"
value={search.value()}
onEdit={input.onEdit}

View File

@@ -0,0 +1,330 @@
import { A, useParams } from "@solidjs/router";
import {
Component,
createMemo,
createResource,
createSignal,
For,
onCleanup,
Show,
} from "solid-js";
import { client } from "../..";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import {
Operation,
PermissionLevel,
PermissionsTarget,
User as UserType,
} from "../../types";
import {
getId,
readableMonitorTimestamp,
readableUserType,
} from "../../util/helpers";
import { useToggle } from "../../util/hooks";
import CheckBox from "../shared/CheckBox";
import Icon from "../shared/Icon";
import Input from "../shared/Input";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Loading from "../shared/loading/Loading";
import Selector from "../shared/menu/Selector";
const User: Component = () => {
const { isMobile } = useAppDimensions();
const { builds, deployments, servers, ws } = useAppState();
const params = useParams<{ id: string }>();
const [user, { refetch }] = createResource(() =>
client.get_user_by_id(params.id)
);
onCleanup(
ws.subscribe(
[
Operation.ModifyUserEnabled,
Operation.ModifyUserCreateServerPermissions,
Operation.ModifyUserCreateBuildPermissions,
Operation.ModifyUserPermissions,
],
refetch
)
);
const [showAll, toggleShowAll] = useToggle(false);
const [search, setSearch] = createSignal("");
const _servers = createMemo(() => {
if (showAll()) {
return servers.filterArray((s) => s.server.name.includes(search()));
} else {
return servers.filterArray((s) => {
if (!s.server.name.includes(search())) return false;
const p = s.server.permissions?.[params.id];
return p ? p !== PermissionLevel.None : false;
});
}
});
const _deployments = createMemo(() => {
if (showAll()) {
return deployments.filterArray((d) =>
d.deployment.name.includes(search())
);
} else {
return deployments.filterArray((d) => {
if (!d.deployment.name.includes(search())) return false;
const p = d.deployment.permissions?.[params.id];
return p ? p !== PermissionLevel.None : false;
});
}
});
const _builds = createMemo(() => {
if (showAll()) {
return builds.filterArray((b) => b.name.includes(search()));
} else {
return builds.filterArray((b) => {
if (!b.name.includes(search())) return false;
const p = b.permissions?.[params.id];
return p ? p !== PermissionLevel.None : false;
});
}
});
return (
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Show when={user()} fallback={<Loading type="three-dot" />}>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center">
<A href="/users" class="grey">
<Icon type="arrow-left" />
</A>
<h1>{user()?.username}</h1>
<Show when={user()?.admin}>
<h2 style={{ opacity: 0.7 }}>admin</h2>
</Show>
</Flex>
<Flex alignItems="center">
<CheckBox
label="show all resources"
checked={showAll()}
toggle={toggleShowAll}
/>
<UserPermissionButtons user={user()!} />
</Flex>
</Flex>
<Flex justifyContent="space-between" alignItems="center">
<Input
placeholder="search resources"
class="lightgrey"
style={{ padding: "0.5rem" }}
value={search()}
onEdit={setSearch}
/>
<Flex>
<Flex gap="0.5rem">
<div class="dimmed">type:</div>
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
</Flex>
<Flex gap="0.5rem">
<div class="dimmed">created:</div>
<div>
{user()?.created_at
? readableMonitorTimestamp(user()?.created_at!)
: "unknown"}
</div>
</Flex>
</Flex>
</Flex>
<Grid class="card light shadow">
<Flex alignItems="center">
<h1>servers</h1>
<Show when={_servers()?.length === 0}>
<div>empty</div>
</Show>
</Flex>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_servers()}>
{(item) => (
<Flex
class="card shadow"
alignItems="center"
justifyContent="space-between"
>
<Grid gap="0.25rem">
<h2>{item.server.name}</h2>
<div class="dimmed">
{item.server.region || "unknown region"}
</div>
</Grid>
<Selector
targetClass={
(item.server.permissions?.[params.id] || "none") !==
"none"
? "blue"
: "red"
}
selected={item.server.permissions?.[params.id] || "none"}
items={["none", "read", "execute", "update"]}
onSelect={(permission) => {
client.update_user_permissions_on_target({
user_id: params.id,
target_type: PermissionsTarget.Server,
target_id: getId(item.server),
permission: permission as PermissionLevel,
});
}}
/>
</Flex>
)}
</For>
</Grid>
</Grid>
<Grid class="card light shadow">
<Flex alignItems="center">
<h1>deployments</h1>
<Show when={_deployments()?.length === 0}>
<div>empty</div>
</Show>
</Flex>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_deployments()}>
{(item) => (
<Flex
class="card shadow"
alignItems="center"
justifyContent="space-between"
>
<Grid gap="0.25rem">
<h2>{item.deployment.name}</h2>
<div class="dimmed">
{servers.get(item.deployment.server_id)?.server.name ||
"unknown"}
</div>
</Grid>
<Selector
targetClass={
(item.deployment.permissions?.[params.id] || "none") !==
"none"
? "blue"
: "red"
}
selected={
item.deployment.permissions?.[params.id] || "none"
}
items={["none", "read", "execute", "update"]}
onSelect={(permission) => {
client.update_user_permissions_on_target({
user_id: params.id,
target_type: PermissionsTarget.Deployment,
target_id: getId(item.deployment),
permission: permission as PermissionLevel,
});
}}
/>
</Flex>
)}
</For>
</Grid>
</Grid>
<Grid class="card light shadow">
<Flex alignItems="center">
<h1>builds</h1>
<Show when={_builds()?.length === 0}>
<div>empty</div>
</Show>
</Flex>
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
<For each={_builds()}>
{(item) => (
<Flex
class="card shadow"
alignItems="center"
justifyContent="space-between"
>
<h2>{item.name}</h2>
<Selector
targetClass={
(item.permissions?.[params.id] || "none") !== "none"
? "blue"
: "red"
}
selected={item.permissions?.[params.id] || "none"}
items={["none", "read", "execute", "update"]}
onSelect={(permission) => {
client.update_user_permissions_on_target({
user_id: params.id,
target_type: PermissionsTarget.Build,
target_id: getId(item),
permission: permission as PermissionLevel,
});
}}
/>
</Flex>
)}
</For>
</Grid>
</Grid>
</Show>
</Grid>
);
};
export default User;
export const UserPermissionButtons: Component<{ user: UserType }> = (p) => {
const { isMobile } = useAppDimensions();
return (
<Show when={!p.user.admin}>
<Grid
placeItems="center end"
gridTemplateColumns={!isMobile() ? "auto 1fr 1fr" : undefined}
>
<button
class={p.user.enabled ? "green" : "red"}
style={{ width: isMobile() ? "11rem" : "6rem" }}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
client.modify_user_enabled({
user_id: getId(p.user),
enabled: !p.user.enabled,
});
}}
>
{p.user.enabled ? "enabled" : "disabled"}
</button>
<button
class={p.user.create_server_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
client.modify_user_create_server_permissions({
user_id: getId(p.user),
create_server_permissions: !p.user.create_server_permissions,
});
}}
>
{p.user.create_server_permissions
? "can create servers"
: "cannot create servers"}
</button>
<button
class={p.user.create_build_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
client.modify_user_create_build_permissions({
user_id: getId(p.user),
create_build_permissions: !p.user.create_build_permissions,
});
}}
>
{p.user.create_build_permissions
? "can create builds"
: "cannot create builds"}
</button>
</Grid>
</Show>
);
};

View File

@@ -1,3 +1,4 @@
import { A } from "@solidjs/router";
import {
Component,
createMemo,
@@ -8,18 +9,16 @@ import {
Show,
} from "solid-js";
import { client } from "../..";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { Operation } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
import { getId } from "../../util/helpers";
import Input from "../shared/Input";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Loading from "../shared/loading/Loading";
import s from "./users.module.scss";
import { UserPermissionButtons } from "./User";
const Users: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const { ws } = useAppState();
const [users, { refetch }] = createResource(() => client.list_users());
onCleanup(
@@ -60,61 +59,28 @@ const Users: Component<{}> = (p) => {
</Flex>
<For each={filteredUsers()}>
{(user) => (
<Flex class={combineClasses(s.User, "shadow")}>
<div class={s.Username}>{user.username}</div>
<Grid
placeItems="center end"
gridTemplateColumns={!isMobile() ? "1fr 1fr 1fr" : undefined}
<Show
when={!user.admin}
fallback={
<Flex class="card light shadow">
<h2>{user.username}</h2>
<h2 style={{ opacity: 0.7 }}>admin</h2>
</Flex>
}
>
<A
href={`/user/${getId(user)}`}
class="card light shadow"
style={{
width: "100%",
"justify-content": "space-between",
"align-items": "center",
}}
>
<button
class={user.enabled ? "green" : "red"}
style={{ width: isMobile() ? "11rem" : "6rem" }}
onClick={() => {
client.modify_user_enabled({
user_id: getId(user),
enabled: !user.enabled,
});
}}
>
{user.enabled ? "enabled" : "disabled"}
</button>
<button
class={user.create_server_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={() => {
client.modify_user_create_server_permissions({
user_id: getId(user),
create_server_permissions:
!user.create_server_permissions,
});
}}
>
{user.create_server_permissions
? "can create servers"
: "cannot create servers"}
</button>
<button
class={user.create_build_permissions ? "green" : "red"}
style={{ width: "11rem" }}
onClick={() => {
client.modify_user_create_build_permissions({
user_id: getId(user),
create_build_permissions: !user.create_build_permissions,
});
}}
>
{user.create_build_permissions
? "can create builds"
: "cannot create builds"}
</button>
{/* <ConfirmButton
class="red"
onConfirm={() => deleteUser(user._id!)}
>
<Icon type="trash" />
</ConfirmButton> */}
</Grid>
</Flex>
<h2>{user.username}</h2>
<UserPermissionButtons user={user} />
</A>
</Show>
)}
</For>
</Grid>

View File

@@ -1,23 +0,0 @@
@use "../../style/colors.scss" as c;
.UsersContent {
padding: 1rem;
}
.Users {
width: fit-content;
min-width: 30rem;
height: fit-content;
}
.Username {
font-weight: 500;
}
.User {
background-color: c.$lightgrey;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
box-sizing: border-box;
}

View File

@@ -11,6 +11,7 @@ import { UserProvider } from "./state/UserProvider";
import { Client } from "./util/client";
import { Router } from "@solidjs/router";
import { AppStateProvider } from "./state/StateProvider";
import { Operation } from "./types";
export const TOPBAR_HEIGHT = 50;
export const MAX_PAGE_WIDTH = 1200;
@@ -29,6 +30,10 @@ const token =
export const client = new Client(MONITOR_BASE_URL, token);
export const OPERATIONS = Object.values(Operation)
.filter((e) => e !== "none" && !e.includes("user"))
.map((e) => e.replaceAll("_", " "));
export const { Notifications, pushNotification } = makeNotifications();
client.initialize().then(() => {

View File

@@ -6,15 +6,18 @@ import {
useDeployments,
useGroups,
useProcedures,
useServerDockerAccounts,
useServerGithubAccounts,
useServerInfo,
useServers,
useServerSecrets,
useServerStats,
useUpdates,
useUsernames,
} from "./hooks";
import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { AwsBuilderConfig, PermissionLevel } from "../types";
import { AwsBuilderConfig, PermissionLevel, UpdateTarget } from "../types";
import { client } from "..";
export type State = {
@@ -23,6 +26,9 @@ export type State = {
getPermissionOnServer: (id: string) => PermissionLevel;
serverStats: ReturnType<typeof useServerStats>;
serverInfo: ReturnType<typeof useServerInfo>;
serverDockerAccounts: ReturnType<typeof useServerDockerAccounts>;
serverGithubAccounts: ReturnType<typeof useServerGithubAccounts>;
serverSecrets: ReturnType<typeof useServerSecrets>;
ungroupedServerIds: () => string[] | undefined;
builds: ReturnType<typeof useBuilds>;
getPermissionOnBuild: (id: string) => PermissionLevel;
@@ -34,6 +40,9 @@ export type State = {
getPermissionOnProcedure: (id: string) => PermissionLevel;
updates: ReturnType<typeof useUpdates>;
aws_builder_config: Resource<AwsBuilderConfig>;
docker_organizations: Resource<string[]>;
github_webhook_base_url: Resource<string>;
name_from_update_target: (target: UpdateTarget) => string;
};
const context = createContext<
@@ -54,6 +63,8 @@ export const AppStateProvider: ParentComponent = (p) => {
const deployments = useDeployments();
const usernames = useUsernames();
const [aws_builder_config] = createResource(() => client.get_aws_builder_defaults());
const [docker_organizations] = createResource(() => client.get_docker_organizations());
const [github_webhook_base_url] = createResource(() => client.get_github_webhook_base_url());
const state: State = {
usernames,
servers,
@@ -107,6 +118,9 @@ export const AppStateProvider: ParentComponent = (p) => {
},
serverStats: useServerStats(servers),
serverInfo: useServerInfo(servers),
serverDockerAccounts: useServerDockerAccounts(servers),
serverGithubAccounts: useServerGithubAccounts(servers),
serverSecrets: useServerSecrets(servers),
groups,
getPermissionOnGroup: (id: string) => {
const group = groups.get(id)!;
@@ -133,6 +147,19 @@ export const AppStateProvider: ParentComponent = (p) => {
},
updates: useUpdates(),
aws_builder_config,
docker_organizations,
github_webhook_base_url,
name_from_update_target: (target) => {
if (target.type === "Deployment" && deployments) {
return deployments.get(target.id!)?.deployment.name || "deleted";
} else if (target.type === "Server" && servers) {
return servers.get(target.id)?.server.name || "deleted";
} else if (target.type === "Build" && builds) {
return builds.get(target.id)?.name || "deleted";
} else {
return "admin";
}
}
};
// createEffect(() => {

View File

@@ -112,6 +112,106 @@ export function useServerInfo(servers: ReturnType<typeof useServers>) {
};
}
export function useServerGithubAccounts(servers: ReturnType<typeof useServers>) {
const [accounts, set] = createSignal<
Record<string, string[] | undefined>
>({});
const load = async (serverID: string) => {
if (servers.get(serverID)?.status === ServerStatus.Ok) {
try {
const info = await client.get_server_github_accounts(serverID);
set((s) => ({ ...s, [serverID]: info }));
} catch (error) {
console.log("error getting server github accounts", error);
}
}
};
const loading: Record<string, boolean> = {};
return {
get: (serverID: string, serverStatus?: ServerStatus) => {
const accts = accounts()[serverID];
if (
accts === undefined &&
!loading[serverID] &&
(serverStatus ? serverStatus === ServerStatus.Ok : true)
) {
loading[serverID] = true;
load(serverID);
}
return accts;
},
load,
};
}
export function useServerDockerAccounts(
servers: ReturnType<typeof useServers>
) {
const [accounts, set] = createSignal<Record<string, string[] | undefined>>(
{}
);
const load = async (serverID: string) => {
if (servers.get(serverID)?.status === ServerStatus.Ok) {
try {
const info = await client.get_server_docker_accounts(serverID);
set((s) => ({ ...s, [serverID]: info }));
} catch (error) {
console.log("error getting server docker accounts", error);
}
}
};
const loading: Record<string, boolean> = {};
return {
get: (serverID: string, serverStatus?: ServerStatus) => {
const accts = accounts()[serverID];
if (
accts === undefined &&
!loading[serverID] &&
(serverStatus ? serverStatus === ServerStatus.Ok : true)
) {
loading[serverID] = true;
load(serverID);
}
return accts;
},
load,
};
}
export function useServerSecrets(
servers: ReturnType<typeof useServers>
) {
const [accounts, set] = createSignal<Record<string, string[] | undefined>>(
{}
);
const load = async (serverID: string) => {
if (servers.get(serverID)?.status === ServerStatus.Ok) {
try {
const info = await client.get_server_available_secrets(serverID);
set((s) => ({ ...s, [serverID]: info }));
} catch (error) {
console.log("error getting server github_accounts", error);
}
}
};
const loading: Record<string, boolean> = {};
return {
get: (serverID: string, serverStatus?: ServerStatus) => {
const accts = accounts()[serverID];
if (
accts === undefined &&
!loading[serverID] &&
(serverStatus ? serverStatus === ServerStatus.Ok : true)
) {
loading[serverID] = true;
load(serverID);
}
return accts;
},
load,
};
}
export function useUsernames() {
const [usernames, set] = createSignal<Record<string, string | undefined>>({});
const load = async (userID: string) => {
@@ -188,7 +288,7 @@ export function useUpdates(target?: UpdateTarget, show_builds?: boolean) {
operations
);
updates.addManyToEnd(newUpdates);
if (newUpdates.length !== 10) {
if (newUpdates.length !== 20) {
setNoMore(true);
}
}
@@ -232,13 +332,18 @@ export function useArrayWithId<T, O>(
idPath: string[],
options?: O
) {
let is_loaded = false;
const [collection, set] = createSignal<T[]>();
const load = (options?: O) => {
query(options).then(set);
const load = (_options?: O) => {
if (!is_loaded || _options !== options) {
query(_options).then((r) => {
is_loaded = true;
options = _options;
set(r);
});
}
};
createEffect(() => {
load(options);
});
load(options);
const addOrUpdate = (item: T) => {
set((items: T[] | undefined) => {
if (items) {

View File

@@ -110,6 +110,11 @@ async function handleMessage(
const deployment = await client.get_deployment(update.target.id!);
deployments.update(deployment);
}
} else if (update.operation === Operation.RenameDeployment) {
if (update.status === UpdateStatus.Complete) {
const deployment = await client.get_deployment(update.target.id!);
deployments.update(deployment);
}
} else if (
[
Operation.DeployContainer,

View File

@@ -14,12 +14,12 @@ $lightgreen: #4f8d5c;
$green: #41764c;
$darkgreen: #2b4f33;
$textred: #f04633;
$textred: #f76858;
$lightred: #b13a2d;
$red: #952E23;
$darkred: #631F17;
$textorange: #984f2d;
$textorange: #e77e4e;
$lightorange: #d56b3a;
$orange: #ac5c36;
$darkorange: #984f2d;

View File

@@ -0,0 +1,31 @@
export const COLORS = {
"app-color": "#fceade",
lightgrey: "#3f454d",
grey: "#25292e",
darkgrey: "#16181b",
textblue: "#5f9af4",
lightblue: "#1c63cd",
blue: "#184e9f",
darkblue: "#12366d",
textgreen: "#80ea97",
lightgreen: "#4f8d5c",
green: "#41764c",
darkgreen: "#2b4f33",
textred: "#f76858",
lightred: "#b13a2d",
red: "#952E23",
darkred: "#631F17",
textorange: "#e77e4e",
lightorange: "#d56b3a",
orange: "#ac5c36",
darkorange: "#984f2d",
lightpurple: "#720e61",
purple: "#5A0B4D",
darkpurple: "#3b0732",
};

View File

@@ -314,6 +314,26 @@ svg {
flex-wrap: wrap;
}
.dimmed {
opacity: 0.7;
}
.full-size {
width: 100%;
height: 100%;
box-sizing: border-box;
}
.full-width {
width: 100%;
box-sizing: border-box;
}
.full-height {
height: 100%;
box-sizing: border-box;
}
// .hoverable {
// transition: all 250ms ease-in-out;
// }

View File

@@ -25,6 +25,7 @@ export interface Build {
name: string;
description?: string;
permissions?: PermissionsMap;
skip_secret_interp?: boolean;
server_id?: string;
aws_config?: AwsBuilderBuildConfig;
version: Version;
@@ -55,6 +56,8 @@ export interface DockerBuildArgs {
build_path: string;
dockerfile_path?: string;
build_args?: EnvironmentVar[];
extra_args?: string[];
use_buildx?: boolean;
}
export interface BuildVersionsReponse {
@@ -91,6 +94,7 @@ export interface AmiAccounts {
ami_id: string;
github?: string[];
docker?: string[];
secrets?: string[];
}
export interface Deployment {
@@ -99,6 +103,7 @@ export interface Deployment {
description?: string;
server_id: string;
permissions?: PermissionsMap;
skip_secret_interp?: boolean;
docker_run_args: DockerRunArgs;
build_id?: string;
build_version?: Version;
@@ -126,6 +131,7 @@ export interface DeploymentActionState {
pulling: boolean;
recloning: boolean;
updating: boolean;
renaming: boolean;
}
export interface DockerRunArgs {
@@ -382,7 +388,7 @@ export interface User {
export interface ApiSecret {
name: string;
hash: string;
hash?: string;
created_at: string;
expires?: string;
}
@@ -419,6 +425,7 @@ export enum Operation {
PruneImagesServer = "prune_images_server",
PruneContainersServer = "prune_containers_server",
PruneNetworksServer = "prune_networks_server",
RenameServer = "rename_server",
CreateBuild = "create_build",
UpdateBuild = "update_build",
DeleteBuild = "delete_build",
@@ -432,6 +439,7 @@ export enum Operation {
RemoveContainer = "remove_container",
PullDeployment = "pull_deployment",
RecloneDeployment = "reclone_deployment",
RenameDeployment = "rename_deployment",
CreateProcedure = "create_procedure",
UpdateProcedure = "update_procedure",
DeleteProcedure = "delete_procedure",

View File

@@ -43,17 +43,29 @@ import {
ModifyUserCreateServerBody,
ModifyUserEnabledBody,
PermissionsUpdateBody,
RenameDeploymentBody,
UpdateDescriptionBody,
} from "./client_types";
import { generateQuery, QueryObject } from "./helpers";
export class Client {
loginOptions: LoginOptions | undefined;
monitorTitle: string | undefined;
secrets_cache: Record<string, string[]> = {};
github_accounts_cache: Record<string, string[]> = {};
docker_accounts_cache: Record<string, string[]> = {};
server_version_cache: Record<string, string> = {};
constructor(private baseURL: string, public token: string | null) {}
async initialize() {
this.loginOptions = await this.get_login_options();
const [loginOptions, monitorTitle] = await Promise.all([
this.get_login_options(),
this.get_monitor_title(),
]);
this.loginOptions = loginOptions;
this.monitorTitle = monitorTitle;
document.title = monitorTitle;
const params = new URLSearchParams(location.search);
const exchange_token = params.get("token");
if (exchange_token) {
@@ -116,10 +128,16 @@ export class Client {
return this.get(`/api/username/${user_id}`);
}
// admin only
list_users(): Promise<User[]> {
return this.get("/api/users");
}
// admin only
get_user_by_id(user_id: string): Promise<User> {
return this.get(`/api/user/${user_id}`);
}
exchange_for_jwt(exchange_token: string): Promise<string> {
return this.post("/auth/exchange", { token: exchange_token });
}
@@ -132,6 +150,10 @@ export class Client {
return this.post("/api/update_description", body);
}
get_monitor_title(): Promise<string> {
return this.get("/api/title");
}
// deployment
list_deployments(
@@ -183,6 +205,10 @@ export class Client {
return this.patch("/api/deployment/update", deployment);
}
rename_deployment(deployment_id: string, new_name: string) {
return this.patch(`/api/deployment/${deployment_id}/rename`, { new_name });
}
reclone_deployment(deployment_id: string): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/reclone`);
}
@@ -237,14 +263,51 @@ export class Client {
}
get_server_github_accounts(id: string): Promise<string[]> {
// if (this.github_accounts_cache[id]) {
// return this.github_accounts_cache[id];
// } else {
// this.github_accounts_cache[id] = [];
// }
// this.github_accounts_cache[id] = await this.get(
// `/api/server/${id}/github_accounts`
// );
// return this.github_accounts_cache[id];
return this.get(`/api/server/${id}/github_accounts`);
}
get_server_docker_accounts(id: string): Promise<string[]> {
// if (this.docker_accounts_cache[id]) {
// return this.docker_accounts_cache[id];
// } else {
// this.docker_accounts_cache[id] = [];
// };
// this.docker_accounts_cache[id] = await this.get(
// `/api/server/${id}/docker_accounts`
// );
// return this.docker_accounts_cache[id];
return this.get(`/api/server/${id}/docker_accounts`);
}
get_server_available_secrets(id: string): Promise<string[]> {
// if (this.secrets_cache[id]) {
// return this.secrets_cache[id];
// } else {
// this.secrets_cache[id] = [];
// };
// console.log("loading");
// this.secrets_cache[id] = await this.get(`/api/server/${id}/secrets`);
// return this.secrets_cache[id];
return this.get(`/api/server/${id}/secrets`);
}
get_server_version(id: string): Promise<string> {
// if (this.server_version_cache[id]) {
// return this.server_version_cache[id];
// } else {
// this.server_version_cache[id] = "loading...";
// };
// this.server_version_cache[id] = await this.get(`/api/server/${id}/version`);
// return this.server_version_cache[id];
return this.get(`/api/server/${id}/version`);
}
@@ -334,7 +397,7 @@ export class Client {
get_build_versions(
id: string,
query?: BuildVersionsQuery
): Promise<BuildVersionsReponse> {
): Promise<BuildVersionsReponse[]> {
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
}

View File

@@ -29,6 +29,10 @@ export interface CopyDeploymentBody {
server_id: string;
}
export interface RenameDeploymentBody {
new_name: string;
}
export interface GetContainerLogQuery {
tail?: number;
}

View File

@@ -1,8 +1,15 @@
import {
Build,
Deployment,
DeploymentWithContainerState,
DockerContainerState,
EnvironmentVar,
Server,
ServerStatus,
ServerWithStatus,
Timelength,
UpdateTarget,
User,
Version,
} from "../types";
@@ -234,3 +241,13 @@ export function readableStorageAmount(gb: number) {
export function readableVersion(version: Version) {
return `v${version.major}.${version.minor}.${version.patch}`;
}
export function readableUserType(user: User) {
if (user.github_id) {
return "github";
} else if (user.google_id) {
return "google";
} else {
return "local";
}
}

View File

@@ -435,6 +435,18 @@
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.6.0.tgz#27583cd0aa81a99482e0e7eddae5e214bd8bf6b6"
integrity sha512-7ug2fzXXhvvDBL4CQyMvMM9o3dgBE6PoRh38T8UTmMnYz4rcCfROqSZc9yq+YEC96qWt5OvJgZ1Uj/4EAQXlfA==
"@tanstack/query-core@4.26.0":
version "4.26.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.0.tgz#fc65c8c117e72baead3a82a1f272a4ec210c7650"
integrity sha512-9CRqXmCH82KZDKmezoGU4FOn1Oqbzlp2/zf71n+9nC58e7NSqCIjfSCMpqQWcu9YqUcUykxZEUunOyKHVc6BJA==
"@tanstack/solid-query@^4.26.0":
version "4.26.0"
resolved "https://registry.yarnpkg.com/@tanstack/solid-query/-/solid-query-4.26.0.tgz#77aa1b60e47719075802891a1f29a0a25385bd48"
integrity sha512-2+dXfIHy8pU0GlrkxjXi8i7z9Ff1C7dbspAo3t6X6jJK57kesGhpE8AjmgIOUrTH1XsoHkTnBKxmoq/I7ewMCQ==
dependencies:
"@tanstack/query-core" "4.26.0"
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"

View File

@@ -1,16 +0,0 @@
apt-get update
apt-get install -y ca-certificates curl gnupg lsb-release make cmake g++ python3 node-gyp build-essential libssl-dev git
git config --global pull.rebase false
# install docker cli
# mkdir -p /etc/apt/keyrings
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# chmod a+r /etc/apt/keyrings/docker.gpg
# apt-get update
# apt-get install -y docker-ce docker-ce-cli containerd.io
curl -fsSL https://get.docker.com | sh
# install nodejs and enable yarn
curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
corepack enable

View File

@@ -1,11 +0,0 @@
apt-get update
apt-get install -y ca-certificates curl gnupg lsb-release git
git config --global pull.rebase false
# install docker cli
# mkdir -p /etc/apt/keyrings
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# apt-get update
# apt-get install docker-ce docker-ce-cli
curl -fsSL https://get.docker.com | sh

View File

@@ -1,11 +1,11 @@
[package]
name = "db_client"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
types = { package = "monitor_types", path = "../types" }
mungos = "0.3.0"
mungos = "0.3.8"
anyhow = "1.0"

View File

@@ -1,5 +1,3 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use collections::{
actions_collection, builds_collection, deployments_collection, groups_collection,
@@ -29,7 +27,10 @@ pub struct DbClient {
impl DbClient {
pub async fn new(config: MongoConfig) -> DbClient {
let db_name = &config.db_name;
let mungos = Mungos::new(&config.uri, &config.app_name, Duration::from_secs(3), None)
let mungos = Mungos::builder()
.uri(&config.uri)
.app_name(&config.app_name)
.build()
.await
.expect("failed to initialize mungos");
DbClient {

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_helpers"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
authors = ["MoghTech"]
description = "helpers used as dependency for mogh tech monitor"

View File

@@ -133,10 +133,7 @@ pub fn to_monitor_name(name: &str) -> String {
}
pub fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal Error: {err:#?}"),
)
(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#?}"))
}
pub fn generate_secret(length: usize) -> String {

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_client"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
authors = ["MoghTech"]
description = "a client to interact with the monitor system"
@@ -9,8 +9,7 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monitor_types = "0.2.7"
# monitor_types = { path = "../types" }
monitor_types = "0.2.12"
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = { version = "1.25", features = ["full"] }

View File

@@ -124,6 +124,15 @@ impl MonitorClient {
.context("failed at updating deployment")
}
pub async fn rename_deployment(&self, id: &str, new_name: &str) -> anyhow::Result<Update> {
self.patch(
&format!("/api/deployment/{id}/rename"),
json!({ "new_name": new_name }),
)
.await
.context("failed at renaming deployment")
}
pub async fn reclone_deployment(&self, id: &str) -> anyhow::Result<Update> {
self.post::<(), _>(&format!("/api/deployment/{id}/reclone"), None)
.await

View File

@@ -58,6 +58,17 @@ impl MonitorClient {
.await
}
pub async fn get_server_available_secrets(
&self,
server_id: &str,
) -> anyhow::Result<Vec<String>> {
self.get(
&format!("/api/server/{server_id}/secrets"),
Option::<()>::None,
)
.await
}
pub async fn create_server(&self, name: &str, address: &str) -> anyhow::Result<Server> {
self.post(
"/api/server/create",

View File

@@ -1,6 +1,6 @@
[package]
name = "periphery_client"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -70,6 +70,21 @@ impl PeripheryClient {
.context("failed to remove container on periphery")
}
pub async fn container_rename(
&self,
server: &Server,
curr_name: &str,
new_name: &str,
) -> anyhow::Result<Log> {
self.post_json(
server,
"/container/rename",
&json!({ "curr_name": curr_name, "new_name": new_name }),
)
.await
.context("failed to rename container on periphery")
}
pub async fn deploy(&self, server: &Server, deployment: &Deployment) -> anyhow::Result<Log> {
self.post_json(server, "/container/deploy", deployment)
.await

View File

@@ -51,6 +51,12 @@ impl PeripheryClient {
.context("failed to get docker accounts from periphery")
}
pub async fn get_available_secrets(&self, server: &Server) -> anyhow::Result<Vec<String>> {
self.get_json(server, "/secrets")
.await
.context("failed to get secret variable names from periphery")
}
pub async fn get_system_information(
&self,
server: &Server,

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_types"
version = "0.2.7"
version = "0.2.12"
edition = "2021"
authors = ["MoghTech"]
description = "types for the mogh tech monitor"

View File

@@ -37,6 +37,11 @@ pub struct Build {
#[builder(setter(skip))]
pub permissions: PermissionsMap,
#[serde(default)]
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub skip_secret_interp: bool,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub server_id: Option<String>, // server which this image should be built on
@@ -150,6 +155,12 @@ pub struct DockerBuildArgs {
#[serde(default)]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
#[serde(default)]
#[builder(default)]
pub use_buildx: bool,
}
#[typeshare]

View File

@@ -17,6 +17,9 @@ pub type SecretsMap = HashMap<String, String>; // these are used for injection i
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreConfig {
#[serde(default = "default_title")]
pub title: String,
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
pub host: String,
@@ -71,6 +74,10 @@ pub struct CoreConfig {
pub aws: AwsBuilderConfig,
}
fn default_title() -> String {
String::from("monitor")
}
fn default_core_port() -> u16 {
9000
}
@@ -161,6 +168,8 @@ pub struct AmiAccounts {
pub github: Vec<String>,
#[serde(default)]
pub docker: Vec<String>,
#[serde(default)]
pub secrets: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]

View File

@@ -37,6 +37,11 @@ pub struct Deployment {
#[builder(setter(skip))]
pub permissions: PermissionsMap,
#[serde(default)]
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub skip_secret_interp: bool,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
pub docker_run_args: DockerRunArgs,
@@ -102,6 +107,7 @@ pub struct DeploymentActionState {
pub pulling: bool,
pub recloning: bool,
pub updating: bool,
pub renaming: bool,
}
#[typeshare]

View File

@@ -100,6 +100,7 @@ pub enum Operation {
PruneImagesServer,
PruneContainersServer,
PruneNetworksServer,
RenameServer,
// build
CreateBuild,
@@ -117,6 +118,7 @@ pub enum Operation {
RemoveContainer,
PullDeployment,
RecloneDeployment,
RenameDeployment,
// procedure
CreateProcedure,

View File

@@ -115,11 +115,11 @@ impl Default for Server {
}
fn default_cpu_alert() -> f32 {
50.0
95.0
}
fn default_mem_alert() -> f64 {
75.0
80.0
}
fn default_disk_alert() -> f64 {

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