begone broken nix
This commit is contained in:
parent
9e857e1d57
commit
3f62e8302e
8 changed files with 134 additions and 28 deletions
|
@ -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(())
|
||||
|
|
|
@ -74,6 +74,9 @@ async fn create_stack(
|
|||
State(state): State<AppState>,
|
||||
Form(form): Form<CreateStackForm>,
|
||||
) -> Result<Redirect, AppError> {
|
||||
// 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?;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -122,3 +122,8 @@ textarea {
|
|||
color: var(--text);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
47
static/js/compose.mjs
Normal file
47
static/js/compose.mjs
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Checks if a specificied source is a valid arion-compose.nix file
|
||||
* @param {string} source Source to check
|
||||
* @returns {Promise<CheckResult>} 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
|
||||
*/
|
45
static/js/enhancements/check.mjs
Normal file
45
static/js/enhancements/check.mjs
Normal file
|
@ -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;
|
||||
}
|
|
@ -37,6 +37,7 @@
|
|||
<section class="editor">
|
||||
<h2>Editor</h2>
|
||||
<form method="POST" action="./edit" id="editor-form">
|
||||
<div class="error"></div>
|
||||
<textarea name="source" id="editor">{{file_contents}}</textarea>
|
||||
<div class="row">
|
||||
<input style="flex:1" name="commit_message" type="text"
|
||||
|
@ -60,11 +61,6 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.containers {
|
||||
& .status {
|
||||
text-align: center;
|
||||
|
@ -101,31 +97,15 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import Editor from "/static/js/ace.mjs";
|
||||
import { add_check } from "/static/js/enhancements/check.mjs";
|
||||
|
||||
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);
|
||||
/* Enforce check pre-submit */
|
||||
const form = document.getElementById("editor-form");
|
||||
add_check(form, editor);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -12,6 +12,7 @@
|
|||
<div class="row">
|
||||
<input style="flex:1" name="name" type="text" placeholder="Stack name" required />
|
||||
</div>
|
||||
<div class="error"></div>
|
||||
<textarea name="source" id="editor" required>{}</textarea>
|
||||
<div class="row">
|
||||
<button type="submit">Create & Deploy</button>
|
||||
|
@ -31,8 +32,13 @@
|
|||
|
||||
<script type="module">
|
||||
import Editor from "/static/js/ace.mjs";
|
||||
import { add_check } from "/static/js/enhancements/check.mjs";
|
||||
|
||||
const editor = new Editor("editor");
|
||||
|
||||
/* Enforce check pre-submit */
|
||||
const form = document.getElementById("editor-form");
|
||||
add_check(form, editor);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
Reference in a new issue