Compare commits

..

60 Commits

Author SHA1 Message Date
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
mbecker20
5579ba869c v0.2.7 remove passkeys from periphery startup log 2023-03-03 17:27:42 +00:00
mbecker20
210940038c hide passkeys on periphery startup config log 2023-03-03 17:24:28 +00:00
mbecker20
98a1a60362 /home/ubuntu/example 2023-03-03 08:15:17 +00:00
mbecker20
86cf9116ba update builds and deployments docs with link to file paths doc 2023-03-03 08:09:41 +00:00
mbecker20
8b2defe0d9 add doc about file paths 2023-03-03 07:58:09 +00:00
mbecker20
50b14b3ce5 0.2.6 store ami name instead of ami_id (because the id has to change sometimes) 2023-03-03 07:11:55 +00:00
122 changed files with 2958 additions and 1196 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",

121
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.5"
version = "0.2.11"
dependencies = [
"anyhow",
"async_timing_util",
@@ -753,7 +759,7 @@ dependencies = [
"hmac",
"jwt",
"monitor_helpers",
"monitor_types 0.2.5",
"monitor_types 0.2.11",
"mungos",
"periphery_client",
"serde",
@@ -987,10 +993,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "db_client"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"monitor_types 0.2.5",
"monitor_types 0.2.11",
"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,18 +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.5"
version = "0.2.11"
dependencies = [
"async_timing_util",
"clap",
"colored",
"monitor_types 0.2.11",
"rand",
"run_command",
"serde",
@@ -1853,12 +1874,12 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"envy",
"futures-util",
"monitor_types 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
@@ -1870,11 +1891,11 @@ dependencies = [
[[package]]
name = "monitor_helpers"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"axum",
"monitor_types 0.2.5",
"monitor_types 0.2.11",
"rand",
"serde",
"serde_json",
@@ -1883,7 +1904,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"async_timing_util",
@@ -1895,11 +1916,12 @@ dependencies = [
"envy",
"futures",
"monitor_helpers",
"monitor_types 0.2.5",
"monitor_types 0.2.11",
"run_command",
"serde",
"serde_derive",
"serde_json",
"svi",
"sysinfo",
"tokio",
"toml",
@@ -1908,7 +1930,7 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"bollard",
@@ -1925,9 +1947,9 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.2.5"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de46e03ba424cb9f70a57a5dd38b81399cca131cdb2edf8bf7d03f829e02c140"
checksum = "b2b2809cdf9e2c1f1faa0093e6da57e6e4d5833f7dd492df490cc4c66f73a383"
dependencies = [
"anyhow",
"bollard",
@@ -1944,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",
@@ -2121,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"
@@ -2168,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",
]
@@ -2183,11 +2196,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "periphery_client"
version = "0.2.5"
version = "0.2.11"
dependencies = [
"anyhow",
"futures-util",
"monitor_types 0.2.5",
"monitor_types 0.2.11",
"reqwest",
"serde",
"serde_json",
@@ -2497,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"
@@ -2861,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"
@@ -3011,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",
@@ -3026,7 +3039,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -3094,6 +3107,7 @@ checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@@ -3418,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.5"
version = "0.2.11"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"
@@ -13,6 +13,7 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monitor_types = { path = "../lib/types" }
clap = "4.0"
async_timing_util = "0.1.14"
rand = "0.8"

View File

@@ -7,15 +7,13 @@ use std::{
str::FromStr,
};
use async_timing_util::Timelength;
use clap::ArgMatches;
use colored::Colorize;
use monitor_types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode, Timelength};
use rand::{distributions::Alphanumeric, Rng};
use run_command::run_command_pipe_to_terminal;
use serde::Serialize;
use crate::types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode};
const CORE_IMAGE_NAME: &str = "mbecker2020/monitor_core";
const PERIPHERY_IMAGE_NAME: &str = "mbecker2020/monitor_periphery";
const PERIPHERY_CRATE: &str = "monitor_periphery";
@@ -64,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,
@@ -321,7 +320,9 @@ pub fn gen_periphery_config(sub_matches: &ArgMatches) {
.map(|p| p.as_str())
.unwrap_or("~/.monitor/repos")
.to_string()
.replace("~", env::var("HOME").unwrap().as_str());
.replace("~", env::var("HOME").unwrap().as_str())
.parse()
.expect("failed to parse --repo_dir as path");
let config = PeripheryConfig {
port,

View File

@@ -3,7 +3,6 @@
use clap::{arg, Arg, Command};
mod helpers;
mod types;
use helpers::*;

View File

@@ -1,207 +0,0 @@
use std::{collections::HashMap, net::IpAddr};
use async_timing_util::Timelength;
use serde_derive::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoreConfig {
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
pub host: String,
// port the core web server runs on
#[serde(default = "default_core_port")]
pub port: u16,
// daily utc offset in hours to run daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
#[serde(default)]
pub daily_offset_hours: u8,
// number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
#[serde(default)]
pub keep_stats_for_days: u64, // 0 means never prune
pub jwt_secret: String,
#[serde(default = "default_jwt_valid_for")]
pub jwt_valid_for: Timelength,
// interval at which to collect server stats and alert for out of bounds
pub monitoring_interval: Timelength,
// used to verify validity from github webhooks
pub github_webhook_secret: String,
// used to form the frontend listener url, if None will use 'host'.
pub github_webhook_base_url: Option<String>,
// sent in auth header with req to periphery
pub passkey: String,
// integration with slack app
pub slack_url: Option<String>,
// enable login with local auth
pub local_auth: bool,
// allowed docker orgs used with monitor. first in this list will be default for build
#[serde(default)]
pub docker_organizations: Vec<String>,
pub mongo: MongoConfig,
#[serde(default)]
pub github_oauth: OauthCredentials,
#[serde(default)]
pub google_oauth: OauthCredentials,
#[serde(default)]
pub aws: AwsBuilderConfig,
}
fn default_core_port() -> u16 {
9000
}
fn default_jwt_valid_for() -> Timelength {
Timelength::OneWeek
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct OauthCredentials {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub id: String,
#[serde(default)]
pub secret: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MongoConfig {
pub uri: String,
#[serde(default = "default_core_mongo_app_name")]
pub app_name: String,
#[serde(default = "default_core_mongo_db_name")]
pub db_name: String,
}
fn default_core_mongo_app_name() -> String {
"monitor_core".to_string()
}
fn default_core_mongo_db_name() -> String {
"monitor".to_string()
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AwsBuilderConfig {
#[serde(skip_serializing)]
pub access_key_id: String,
#[serde(skip_serializing)]
pub secret_access_key: String,
pub default_ami_id: String,
pub default_subnet_id: String,
pub default_key_pair_name: String,
#[serde(default)]
pub available_ami_accounts: AvailableAmiAccounts,
#[serde(default = "default_aws_region")]
pub default_region: String,
#[serde(default = "default_volume_gb")]
pub default_volume_gb: i32,
#[serde(default = "default_instance_type")]
pub default_instance_type: String,
#[serde(default)]
pub default_security_group_ids: Vec<String>,
#[serde(default)]
pub default_assign_public_ip: bool,
}
fn default_aws_region() -> String {
String::from("us-east-1")
}
fn default_volume_gb() -> i32 {
8
}
fn default_instance_type() -> String {
String::from("m5.2xlarge")
}
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AmiAccounts {
pub name: String,
#[serde(default)]
pub github: Vec<String>,
#[serde(default)]
pub docker: Vec<String>,
}
pub type GithubUsername = String;
pub type GithubToken = String;
pub type GithubAccounts = HashMap<GithubUsername, GithubToken>;
pub type DockerUsername = String;
pub type DockerToken = String;
pub type DockerAccounts = HashMap<DockerUsername, DockerToken>;
pub type SecretsMap = HashMap<String, String>;
#[derive(Serialize, Deserialize, Debug)]
pub struct PeripheryConfig {
#[serde(default = "default_periphery_port")]
pub port: u16,
#[serde(default = "default_repo_dir")]
pub repo_dir: String,
#[serde(default = "default_stats_refresh_interval")]
pub stats_polling_rate: Timelength,
#[serde(default)]
pub allowed_ips: Vec<IpAddr>,
#[serde(default)]
pub passkeys: Vec<String>,
#[serde(default)]
pub secrets: SecretsMap,
#[serde(default)]
pub github_accounts: GithubAccounts,
#[serde(default)]
pub docker_accounts: DockerAccounts,
}
fn default_periphery_port() -> u16 {
8000
}
fn default_repo_dir() -> String {
"/repos".to_string()
}
fn default_stats_refresh_interval() -> Timelength {
Timelength::FiveSeconds
}
#[derive(Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy)]
pub enum RestartMode {
#[serde(rename = "no")]
#[strum(serialize = "no")]
NoRestart,
#[serde(rename = "on-failure")]
#[strum(serialize = "on-failure")]
OnFailure,
#[serde(rename = "always")]
#[strum(serialize = "always")]
Always,
#[serde(rename = "unless-stopped")]
#[strum(serialize = "unless-stopped")]
UnlessStopped,
}

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.5"
version = "0.2.11"
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

@@ -7,7 +7,8 @@ use mungos::{doc, to_bson};
use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version, AwsBuilderBuildConfig,
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
UpdateTarget, Version,
};
use crate::{
@@ -442,10 +443,17 @@ impl State {
self.config.aws.secret_access_key.clone(),
)
.await;
let ami_id = aws_config
.ami_id
let ami_name = aws_config
.ami_name
.as_ref()
.unwrap_or(&self.config.aws.default_ami_id);
.unwrap_or(&self.config.aws.default_ami_name);
let ami_id = &self
.config
.aws
.available_ami_accounts
.get(ami_name)
.ok_or(anyhow!("no ami id associated with ami name {ami_name}"))?
.ami_id;
let instance_type = aws_config
.instance_type
.as_ref()

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

@@ -2,6 +2,8 @@
Monitor builds docker images by cloning the source repository from Github, running ```docker build```, and pushing the resulting image to docker hub. Any repo containing a 'Dockerfile' is buildable using this method.
Build configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
## repo configuration
To specify the github repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like ```mbecker20/monitor```, it includes the username / organization that owns the repo.

View File

@@ -2,6 +2,8 @@
Monitor can deploy any docker images that it can access with the configured docker accounts. It works by parsing the deployment configuration into a ```docker run``` command. The configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well.
Deployment configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
## configuring the image
There are two options to configure the deployed image.

38
docs/paths.md Normal file
View File

@@ -0,0 +1,38 @@
# File Paths
When working with monitor, you might have to configure file or directory paths.
## Relative Paths
Where possible, it is better to use relative file paths. Using relative file paths removes the connection between the process being run and the particular server it runs one, making it easier to move things between servers.
Where you see relative paths:
- setting the build directory and path of the Dockerfile
- setting a pre build command path
- configuring a frontend mount (used for web apps)
For all of the above, the path can be given relative to the root of the configured repo
The one exception is the Dockerfile path, which is given relative to the build directory (This is done by Docker itself, and this pattern matches usage of the Docker CLI).
There are 3 kinds of paths to pass:
1. to specify the root of the repo, use ```.``` as the path
2. to specify a folder in the repo, pass it with **no** preceding ```/```. For example, ```example_folder``` or ```folder1/folder2```
3. to specify an absolute path on the servers filesystem, use a preceding slash, eg. ```/home/ubuntu/example```. This way should only be used if absolutely necessary.
### Implementation
relative file paths are joined with the path of the repo on the system using a Rust [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push).
## Docker Volume Paths
These are passed directly to the Docker CLI using ```--volume /path/on/system:/path/in/container```. So for these, the same rules apply as when using Docker on the command line. Paths here should be given as absolute, don't use ```~``` or even ```$HOME```.

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

@@ -22,22 +22,9 @@ const AwsBuilderConfig: Component<{}> = (p) => {
const Ami: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
const default_ami_id = () => aws_builder_config()?.default_ami_id;
const get_ami_id = () => {
if (build.aws_config?.ami_id) {
return build.aws_config.ami_id;
} else {
return default_ami_id() || "unknown";
}
};
const get_ami_name = (ami_id: string) => {
if (aws_builder_config() === undefined || ami_id === "unknown")
return "unknown";
return (
aws_builder_config()!.available_ami_accounts![ami_id]?.name || "unknown"
);
};
const ami_ids = () => {
const default_ami_name = () => aws_builder_config()?.default_ami_name;
const get_ami_name = () => build.aws_config?.ami_name || aws_builder_config()?.default_ami_name || "unknown";
const ami_names = () => {
if (aws_builder_config() === undefined) return [];
return Object.keys(aws_builder_config()!.available_ami_accounts!);
};
@@ -50,16 +37,16 @@ const Ami: Component = () => {
<h1>ami</h1>
<Selector
targetClass="blue"
selected={get_ami_id()}
items={ami_ids()}
onSelect={(ami_id) => {
if (ami_id === default_ami_id()) {
setBuild("aws_config", "ami_id", undefined);
selected={get_ami_name()}
items={ami_names()}
onSelect={(ami_name) => {
if (ami_name === default_ami_name()) {
setBuild("aws_config", "ami_name", undefined);
} else {
setBuild("aws_config", "ami_id", ami_id);
setBuild("aws_config", "ami_name", ami_name);
}
}}
itemMap={get_ami_name}
itemMap={(i) => i.replaceAll("_", " ")}
position="bottom right"
disabled={!userCanUpdate()}
useSearch

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,26 +14,16 @@ 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_id =
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
return ami_id
? aws_builder_config()?.available_ami_accounts![ami_id].docker || []
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].docker || []
: [];
} else return [];
};
@@ -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,34 +1,29 @@
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_id =
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
return ami_id
? aws_builder_config()?.available_ami_accounts![ami_id].github || []
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].github || []
: [];
} else return [];
};

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 {
@@ -65,7 +68,7 @@ export interface BuildVersionsReponse {
export interface AwsBuilderBuildConfig {
region?: string;
instance_type?: string;
ami_id?: string;
ami_name?: string;
volume_gb?: number;
subnet_id?: string;
security_group_ids?: string[];
@@ -76,7 +79,7 @@ export interface AwsBuilderBuildConfig {
export interface AwsBuilderConfig {
access_key_id: string;
secret_access_key: string;
default_ami_id: string;
default_ami_name: string;
default_subnet_id: string;
default_key_pair_name: string;
available_ami_accounts?: AvailableAmiAccounts;
@@ -88,9 +91,10 @@ export interface AwsBuilderConfig {
}
export interface AmiAccounts {
name: string;
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.5"
version = "0.2.11"
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.5"
version = "0.2.11"
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.5"
version = "0.2.11"
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.5"
# monitor_types = { path = "../types" }
monitor_types = "0.2.11"
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.5"
version = "0.2.11"
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

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