add check
This commit is contained in:
parent
21dd617a4b
commit
9e857e1d57
10 changed files with 212 additions and 47 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
@ -451,6 +451,22 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -806,6 +822,12 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
|
@ -1108,6 +1130,19 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.4.1",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -1278,13 +1313,13 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"xshell",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1325,6 +1360,19 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fastrand",
|
||||||
|
"redox_syscall",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "text-size"
|
name = "text-size"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -1782,18 +1830,3 @@ name = "windows_x86_64_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xshell"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce2107fe03e558353b4c71ad7626d58ed82efaf56c54134228608893c77023ad"
|
|
||||||
dependencies = [
|
|
||||||
"xshell-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xshell-macros"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e2c411759b501fb9501aac2b1b2d287a6e93e5bdcf13c25306b23e1b716dd0e"
|
|
||||||
|
|
|
@ -19,10 +19,10 @@ rnix = "0.11"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sysinfo = "0.29"
|
sysinfo = "0.29"
|
||||||
|
tempfile = "3"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
time = { version = "0.3", features = ["serde"] }
|
time = { version = "0.3", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
xshell = "0.2"
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub enum AppError {
|
||||||
Client {
|
Client {
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
code: &'static str,
|
code: &'static str,
|
||||||
message: &'static str,
|
message: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("docker error: {0}")]
|
#[error("docker error: {0}")]
|
||||||
|
@ -22,9 +22,6 @@ pub enum AppError {
|
||||||
#[error("file error: {0}")]
|
#[error("file error: {0}")]
|
||||||
FileError(#[from] std::io::Error),
|
FileError(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("shell error: {0}")]
|
|
||||||
ShellError(#[from] xshell::Error),
|
|
||||||
|
|
||||||
#[error("unexpected internal error: {0}")]
|
#[error("unexpected internal error: {0}")]
|
||||||
Internal(#[from] anyhow::Error),
|
Internal(#[from] anyhow::Error),
|
||||||
|
|
||||||
|
@ -54,11 +51,6 @@ impl IntoResponse for AppError {
|
||||||
code: "file-error".to_string(),
|
code: "file-error".to_string(),
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
},
|
},
|
||||||
AppError::ShellError(err) => ErrorInfo {
|
|
||||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
code: "shell-error".to_string(),
|
|
||||||
message: err.to_string(),
|
|
||||||
},
|
|
||||||
AppError::Template(err) => ErrorInfo {
|
AppError::Template(err) => ErrorInfo {
|
||||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
code: "template-error".to_string(),
|
code: "template-error".to_string(),
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl From<StackError> for AppError {
|
||||||
StackError::NotFound => AppError::Client {
|
StackError::NotFound => AppError::Client {
|
||||||
status: StatusCode::NOT_FOUND,
|
status: StatusCode::NOT_FOUND,
|
||||||
code: "stack-not-found",
|
code: "stack-not-found",
|
||||||
message: "stack not found",
|
message: "stack not found".to_string(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,12 +71,14 @@ impl ThreadSafeRepository {
|
||||||
Repository::open(&self.path)
|
Repository::open(&self.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_file(&self, path: &Path, message: &str) -> Result<()> {
|
pub fn commit_files(&self, paths: &[&Path], message: &str) -> Result<()> {
|
||||||
let repository = self.repository()?;
|
let repository = self.repository()?;
|
||||||
|
|
||||||
// Commit file
|
// Commit file
|
||||||
let mut index = repository.index()?;
|
let mut index = repository.index()?;
|
||||||
index.add_path(path)?;
|
for path in paths {
|
||||||
|
index.add_path(path)?;
|
||||||
|
}
|
||||||
let oid = index.write_tree()?;
|
let oid = index.write_tree()?;
|
||||||
let tree = repository.find_tree(oid)?;
|
let tree = repository.find_tree(oid)?;
|
||||||
let head = repository.head()?;
|
let head = repository.head()?;
|
||||||
|
|
|
@ -2,17 +2,23 @@ use super::{
|
||||||
container::ContainerInfo, error::StackError, git::ThreadSafeRepository,
|
container::ContainerInfo, error::StackError, git::ThreadSafeRepository,
|
||||||
nix::parse_arion_compose,
|
nix::parse_arion_compose,
|
||||||
};
|
};
|
||||||
use crate::http::error::Result;
|
use crate::http::error::{AppError, Result};
|
||||||
|
use axum::http::StatusCode;
|
||||||
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||||
|
use futures_util::future::try_join;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
use tempfile::tempdir;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use xshell::Shell;
|
|
||||||
|
|
||||||
const COMPOSE_FILE: &str = "arion-compose.nix";
|
const COMPOSE_FILE: &str = "arion-compose.nix";
|
||||||
|
const PACKAGE_FILE: &str = "arion-pkgs.nix";
|
||||||
|
const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; }
|
||||||
|
"#;
|
||||||
|
|
||||||
async fn is_stack(dir: &Path) -> Result<bool> {
|
async fn is_stack(dir: &Path) -> Result<bool> {
|
||||||
Ok(fs::try_exists(dir.join(COMPOSE_FILE)).await?)
|
Ok(fs::try_exists(dir.join(COMPOSE_FILE)).await?)
|
||||||
|
@ -148,26 +154,108 @@ pub fn commit_compose(
|
||||||
message: &str,
|
message: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE);
|
let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE);
|
||||||
repository.commit_file(&PathBuf::from(compose_path), message)?;
|
repository.commit_files(&[&PathBuf::from(compose_path)], message)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_new(
|
||||||
|
repository: ThreadSafeRepository,
|
||||||
|
stack_name: &str,
|
||||||
|
source: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Calculate stack directory and create it
|
||||||
|
let stack_path = repository.path.join(stack_name);
|
||||||
|
fs::create_dir_all(&stack_path).await?;
|
||||||
|
|
||||||
|
// Create package file and compose file
|
||||||
|
try_join(
|
||||||
|
fs::write(stack_path.join(PACKAGE_FILE), PACKAGE_CONTENTS),
|
||||||
|
fs::write(stack_path.join(COMPOSE_FILE), source),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Commit everything
|
||||||
|
repository.commit_files(
|
||||||
|
&[
|
||||||
|
&PathBuf::from(format!("{}/{}", stack_name, PACKAGE_FILE)),
|
||||||
|
&PathBuf::from(format!("{}/{}", stack_name, COMPOSE_FILE)),
|
||||||
|
],
|
||||||
|
format!("Created stack {}", stack_name).as_str(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
|
||||||
|
// Check that it's a valid nix tree
|
||||||
|
rnix::Root::parse(source)
|
||||||
|
.ok()
|
||||||
|
.map_err(|_| AppError::Client {
|
||||||
|
status: StatusCode::NOT_ACCEPTABLE,
|
||||||
|
code: "failed-nix-parse",
|
||||||
|
message: "The provided source is not valid Nix".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Create a temporary stack and check that it generates a YAML tree
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let path = dir.path();
|
||||||
|
|
||||||
|
// Create package file and compose file
|
||||||
|
try_join(
|
||||||
|
fs::write(path.join(PACKAGE_FILE), PACKAGE_CONTENTS),
|
||||||
|
fs::write(path.join(COMPOSE_FILE), source),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cmd = arion(arion_bin, path, StackCommand::Test)?;
|
||||||
|
|
||||||
|
dir.close()?;
|
||||||
|
|
||||||
|
if let CommandStatus::Failure(_, err) = cmd {
|
||||||
|
Err(AppError::Client {
|
||||||
|
status: StatusCode::NOT_ACCEPTABLE,
|
||||||
|
code: "failed-arion-check",
|
||||||
|
message: err,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CommandStatus {
|
||||||
|
Success(String, String),
|
||||||
|
Failure(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arion(arion_bin: &Path, path: &Path, action: StackCommand) -> Result<CommandStatus> {
|
||||||
|
let output = Command::new(arion_bin)
|
||||||
|
.args(action.command())
|
||||||
|
.current_dir(path)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
// Convert stdout and stderr to String
|
||||||
|
let stdout_str = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(CommandStatus::Success(stdout_str, stderr_str))
|
||||||
|
} else {
|
||||||
|
Ok(CommandStatus::Failure(stdout_str, stderr_str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn command(
|
pub async fn command(
|
||||||
base_dir: &Path,
|
base_dir: &Path,
|
||||||
stack_name: &str,
|
stack_name: &str,
|
||||||
arion_bin: &Path,
|
arion_bin: &Path,
|
||||||
action: StackCommand,
|
action: StackCommand,
|
||||||
) -> Result<()> {
|
) -> Result<CommandStatus> {
|
||||||
let dir = base_dir.join(stack_name);
|
let dir = base_dir.join(stack_name);
|
||||||
if !is_stack(&dir).await? {
|
if !is_stack(&dir).await? {
|
||||||
return Err(StackError::NotFound.into());
|
return Err(StackError::NotFound.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let sh = Shell::new()?;
|
arion(arion_bin, &dir, action)
|
||||||
sh.change_dir(dir);
|
|
||||||
sh.cmd(arion_bin).args(action.command()).run()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StackCommand {
|
pub enum StackCommand {
|
||||||
|
@ -175,6 +263,7 @@ pub enum StackCommand {
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
Restart,
|
Restart,
|
||||||
|
Test,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StackCommand {
|
impl StackCommand {
|
||||||
|
@ -184,6 +273,7 @@ impl StackCommand {
|
||||||
StackCommand::Start => &["up", "-d"],
|
StackCommand::Start => &["up", "-d"],
|
||||||
StackCommand::Stop => &["stop"],
|
StackCommand::Stop => &["stop"],
|
||||||
StackCommand::Restart => &["restart"],
|
StackCommand::Restart => &["restart"],
|
||||||
|
StackCommand::Test => &["config"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crate::{
|
||||||
node::{
|
node::{
|
||||||
container::ContainerInfo,
|
container::ContainerInfo,
|
||||||
stack::{
|
stack::{
|
||||||
command, commit_compose, get_compose, get_containers, write_compose, StackCommand,
|
check_compose, command, commit_compose, create_new, get_compose, get_containers,
|
||||||
|
write_compose, StackCommand,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -63,6 +64,21 @@ async fn new_stack_page() -> impl IntoResponse {
|
||||||
CreateTemplate {}
|
CreateTemplate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateStackForm {
|
||||||
|
name: String,
|
||||||
|
source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_stack(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<CreateStackForm>,
|
||||||
|
) -> Result<Redirect, AppError> {
|
||||||
|
create_new(state.repository, &form.name, &form.source).await?;
|
||||||
|
|
||||||
|
Ok(Redirect::to(format!("/stack/{}/", form.name).as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct EditStackForm {
|
struct EditStackForm {
|
||||||
source: String,
|
source: String,
|
||||||
|
@ -84,7 +100,7 @@ async fn edit_stack(
|
||||||
return Err(AppError::Client {
|
return Err(AppError::Client {
|
||||||
status: StatusCode::BAD_REQUEST,
|
status: StatusCode::BAD_REQUEST,
|
||||||
code: "invalid-source",
|
code: "invalid-source",
|
||||||
message: "provided stack source is empty",
|
message: "provided stack source is empty".to_string(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,7 +122,15 @@ async fn edit_stack(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
Ok(Redirect::to("./"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_stack_file(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<StatusCode, AppError> {
|
||||||
|
check_compose(&state.arion_bin, &body).await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! stack_command {
|
macro_rules! stack_command {
|
||||||
|
@ -120,7 +144,8 @@ macro_rules! stack_command {
|
||||||
|
|
||||||
pub(super) fn router() -> Router<AppState> {
|
pub(super) fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/_/new", get(new_stack_page))
|
.route("/_/new", get(new_stack_page).post(create_stack))
|
||||||
|
.route("/_/check", post(check_stack_file))
|
||||||
.route(
|
.route(
|
||||||
"/:stack",
|
"/:stack",
|
||||||
get(|Path(stack_name): Path<String>| async move {
|
get(|Path(stack_name): Path<String>| async move {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import "../vendor/ace/ace.js";
|
||||||
import { findNearestParent } from "./node-utils.mjs";
|
import { findNearestParent } from "./node-utils.mjs";
|
||||||
|
|
||||||
export default class Editor {
|
export default class Editor {
|
||||||
|
editor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new editor
|
* Create a new editor
|
||||||
* @param {string} elementID ID of element to replace with the editor
|
* @param {string} elementID ID of element to replace with the editor
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input style="flex:1" name="commit_message" type="text"
|
<input style="flex:1" name="commit_message" type="text"
|
||||||
placeholder="What did you change?" />
|
placeholder="What did you change?" />
|
||||||
|
<div id="editor-extra"></div>
|
||||||
<button class="wide" type="submit">Deploy</button>
|
<button class="wide" type="submit">Deploy</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -104,7 +105,27 @@
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Editor from "/static/js/ace.mjs";
|
import Editor from "/static/js/ace.mjs";
|
||||||
|
|
||||||
new Editor("editor");
|
const editor = new Editor("editor");
|
||||||
|
|
||||||
|
/* Add extra buttons */
|
||||||
|
const extraContainer = document.getElementById("editor-extra");
|
||||||
|
|
||||||
|
/* Check button */
|
||||||
|
const checkButton = document.createElement("button");
|
||||||
|
checkButton.appendChild(document.createTextNode("Check"));
|
||||||
|
checkButton.type = "button";
|
||||||
|
checkButton.addEventListener("click", async () => {
|
||||||
|
const body = editor.editor.getValue();
|
||||||
|
const response = await fetch("/stack/_/check", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
})
|
||||||
|
extraContainer.appendChild(checkButton);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -8,11 +8,11 @@
|
||||||
<h1>New stack</h1>
|
<h1>New stack</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class="editor">
|
<section class="editor">
|
||||||
<form method="POST" action="./edit" id="editor-form">
|
<form method="POST" id="editor-form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input style="flex:1" name="name" type="text" placeholder="Stack name" />
|
<input style="flex:1" name="name" type="text" placeholder="Stack name" required />
|
||||||
</div>
|
</div>
|
||||||
<textarea name="source" id="editor">{}</textarea>
|
<textarea name="source" id="editor" required>{}</textarea>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button type="submit">Create & Deploy</button>
|
<button type="submit">Create & Deploy</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Editor from "/static/js/ace.mjs";
|
import Editor from "/static/js/ace.mjs";
|
||||||
|
|
||||||
new Editor("editor");
|
const editor = new Editor("editor");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in a new issue