forked from github-starred/komodo
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6559814b1 | ||
|
|
c8c080183f | ||
|
|
597b67f799 | ||
|
|
ec52d5f422 | ||
|
|
34806304d6 | ||
|
|
87953d5495 | ||
|
|
b6c7c80c95 | ||
|
|
77e568d5c3 | ||
|
|
699fc51cf7 | ||
|
|
21029c90b7 | ||
|
|
6b0530eb7f | ||
|
|
f7061c7225 | ||
|
|
750f698369 | ||
|
|
ec5ef42298 | ||
|
|
46820b0044 | ||
|
|
425a6648f7 | ||
|
|
349fc297ce | ||
|
|
5ad87c03ed | ||
|
|
d16006f28f | ||
|
|
7f0452a5f5 | ||
|
|
c605b2f6fc | ||
|
|
6c2d8a8494 | ||
|
|
874691f729 | ||
|
|
cdf702e17d | ||
|
|
25fdb32627 | ||
|
|
e976ea0a3a | ||
|
|
34e6b4fc69 | ||
|
|
a2d77567b3 | ||
|
|
ecb460f9b5 | ||
|
|
63444b089c | ||
|
|
c787984b77 | ||
|
|
bf3d03e801 | ||
|
|
bc2e69b975 | ||
|
|
7b94fcf3da | ||
|
|
9cf03b8b88 | ||
|
|
a288edcf61 | ||
|
|
89cc18ad37 | ||
|
|
ffa3b671e1 | ||
|
|
f32eeb413b | ||
|
|
b5a5103cfc | ||
|
|
c5697e59f3 | ||
|
|
f030667ff4 | ||
|
|
e9fef5d97c | ||
|
|
f5818ac7ea | ||
|
|
c85ab4110d | ||
|
|
9690ea35b8 | ||
|
|
6300c8011b | ||
|
|
97f582b381 | ||
|
|
5135a9c228 | ||
|
|
b7d1212a82 | ||
|
|
7d9d0a9fc4 | ||
|
|
ed9aef4321 | ||
|
|
0aa638bdf4 | ||
|
|
0ec39d793d | ||
|
|
5579ba869c | ||
|
|
210940038c | ||
|
|
98a1a60362 | ||
|
|
86cf9116ba | ||
|
|
8b2defe0d9 | ||
|
|
50b14b3ce5 |
8
.vscode/tasks.json
vendored
8
.vscode/tasks.json
vendored
@@ -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
121
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use clap::{arg, Arg, Command};
|
||||
|
||||
mod helpers;
|
||||
mod types;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
|
||||
207
cli/src/types.rs
207
cli/src/types.rs
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
38
docs/paths.md
Normal 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```.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
157
frontend/src/components/Updates.tsx
Normal file
157
frontend/src/components/Updates.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
41
frontend/src/components/account/Account.tsx
Normal file
41
frontend/src/components/account/Account.tsx
Normal 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;
|
||||
124
frontend/src/components/account/Resources.tsx
Normal file
124
frontend/src/components/account/Resources.tsx
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
59
frontend/src/components/build/tabs/config/ExtraArgs.tsx
Normal file
59
frontend/src/components/build/tabs/config/ExtraArgs.tsx
Normal 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;
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
30
frontend/src/components/build/tabs/config/UseBuildx.tsx
Normal file
30
frontend/src/components/build/tabs/config/UseBuildx.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
// ]}
|
||||
// />
|
||||
// );
|
||||
// };
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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!)}`;
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
32
frontend/src/components/shared/CheckBox.tsx
Normal file
32
frontend/src/components/shared/CheckBox.tsx
Normal 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;
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
241
frontend/src/components/shared/PieChart.tsx
Normal file
241
frontend/src/components/shared/PieChart.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
330
frontend/src/components/users/User.tsx
Normal file
330
frontend/src/components/users/User.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
frontend/src/style/colors.tsx
Normal file
31
frontend/src/style/colors.tsx
Normal 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",
|
||||
};
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ export interface CopyDeploymentBody {
|
||||
server_id: string;
|
||||
}
|
||||
|
||||
export interface RenameDeploymentBody {
|
||||
new_name: string;
|
||||
}
|
||||
|
||||
export interface GetContainerLogQuery {
|
||||
tail?: number;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user