diff --git a/src/node/stack.rs b/src/node/stack.rs index f6bf662..d6bea7c 100644 --- a/src/node/stack.rs +++ b/src/node/stack.rs @@ -190,10 +190,10 @@ 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 { + .map_err(|err| AppError::Client { status: StatusCode::NOT_ACCEPTABLE, code: "failed-nix-parse", - message: "The provided source is not valid Nix".to_string(), + message: format!("Parse error: {}", err), })?; // Create a temporary stack and check that it generates a YAML tree @@ -215,7 +215,7 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> { Err(AppError::Client { status: StatusCode::NOT_ACCEPTABLE, code: "failed-arion-check", - message: err, + message: format!("Arion {}", err), }) } else { Ok(()) diff --git a/src/route/stack.rs b/src/route/stack.rs index fc36157..29315de 100644 --- a/src/route/stack.rs +++ b/src/route/stack.rs @@ -74,6 +74,9 @@ async fn create_stack( State(state): State, Form(form): Form, ) -> Result { + // Make sure body is is ok + check_compose(&state.arion_bin, &form.source).await?; + create_new(state.repository, &form.name, &form.source).await?; Ok(Redirect::to(format!("/stack/{}/", form.name).as_str())) @@ -107,6 +110,9 @@ async fn edit_stack( // Cleanup source (like line endings) let source = form.source.replace("\r\n", "\n"); + // Make sure file is ok + check_compose(&state.arion_bin, &source).await?; + // Write compose file write_compose(&state.stack_dir, &stack_name, &source).await?; diff --git a/static/css/editor.css b/static/css/editor.css index c5835c6..6eafaff 100644 --- a/static/css/editor.css +++ b/static/css/editor.css @@ -17,6 +17,15 @@ } } } + + + & .error { + border-radius: 3px; + background-color: #500F1C; + color: #FF9592; + padding: 4px 8px; + display: none; + } } #editor { @@ -28,4 +37,12 @@ min-height: 50vh; border: 3px solid #5958B1; border-radius: 3px; + + &.checked { + border-color: #46A758; + } + + &.err { + border-color: #E5484D; + } } \ No newline at end of file diff --git a/static/css/screen.css b/static/css/screen.css index 0e10bb4..db1b494 100644 --- a/static/css/screen.css +++ b/static/css/screen.css @@ -121,4 +121,9 @@ textarea { background-color: var(--bg-raised); color: var(--text); padding: 4px; +} + +pre { + margin: 0; + padding: 0; } \ No newline at end of file diff --git a/static/js/compose.mjs b/static/js/compose.mjs new file mode 100644 index 0000000..81fb0c5 --- /dev/null +++ b/static/js/compose.mjs @@ -0,0 +1,47 @@ +/** + * Checks if a specificied source is a valid arion-compose.nix file + * @param {string} source Source to check + * @returns {Promise} Result of the check + */ +export async function check_compose(source) { + const response = await fetch("/stack/_/check", { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "text/plain", + }, + body: source + }); + + if (response.ok) { + return { ok: true }; + } else { + return { ok: false, error: await response.json() }; + } +} + +/** + * @typedef {Object} APIError + * @property {string} code - Error code + */ + +/** + * @typedef {Object} CheckSuccess + * @property { true } ok - Indicates that the check was successful + * / + +/** + * @typedef {Object} APIError + * @property {string} code - Unique error code + * @property {string} message - Human readable error details + */ + +/** + * @typedef {Object} CheckFailure + * @property {false} ok - Indicates that the check has failed + * @property {APIError} error - Info about the encountered error + */ + +/** + * @typedef {CheckSuccess | CheckFailure} CheckResult + */ \ No newline at end of file diff --git a/static/js/enhancements/check.mjs b/static/js/enhancements/check.mjs new file mode 100644 index 0000000..505c581 --- /dev/null +++ b/static/js/enhancements/check.mjs @@ -0,0 +1,45 @@ +import Editor from "../ace.mjs"; +import { check_compose } from "/static/js/compose.mjs"; + +/** + * Makes the form require the stack to be checked before submitting + * @param {HTMLFormElement} form + * @param {Editor} editor + */ +export function add_check(form, editor) { + form.addEventListener("submit", (ev) => { + ev.preventDefault(); + check_stack(editor).then((result) => { + if (result) { + form.submit(); + } + }) + return false; + }); +} + +/** + * Runs the check function and updates some DOM elements + * @param {Editor} editor Editor instance + * @returns true if the stack is valid, false otherwise + */ +export async function check_stack(editor) { + const source = editor.editor.getValue(); + const check_result = await check_compose(source); + + const editorEl = document.querySelector(".ace_editor"); + const editorErrorEl = document.querySelector("#editor-form .error"); + editorEl.classList.remove("err", "checked"); + editorErrorEl.style.display = "block"; + editorErrorEl.classList.add("pending"); + editorErrorEl.innerHTML = "Checking..."; + if (check_result.ok) { + editorEl.classList.add("checked"); + editorErrorEl.style.display = ""; + } else { + editorEl.classList.add("err"); + editorErrorEl.classList.remove("pending"); + editorErrorEl.innerHTML = check_result.error.message; + } + return check_result.ok; +} \ No newline at end of file diff --git a/templates/stack/get-one.html b/templates/stack/get-one.html index a66fc36..7a37702 100644 --- a/templates/stack/get-one.html +++ b/templates/stack/get-one.html @@ -37,6 +37,7 @@

Editor

+
- {% endblock %} \ No newline at end of file diff --git a/templates/stack/new-form.html b/templates/stack/new-form.html index a282d1a..13e46cc 100644 --- a/templates/stack/new-form.html +++ b/templates/stack/new-form.html @@ -12,6 +12,7 @@
+
@@ -31,8 +32,13 @@ {% endblock %} \ No newline at end of file