Compare commits
No commits in common. "a8d2ed380e6ae2a9a33a669126bcf37a3a65514d" and "9a38fd539f1771bf0df272c801dc11819708c2a6" have entirely different histories.
a8d2ed380e
...
9a38fd539f
18 changed files with 61 additions and 269 deletions
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-Clink-arg=-fuse-ld=mold"]
|
|
@ -97,13 +97,13 @@
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701048361,
|
"lastModified": 1701048361,
|
||||||
"narHash": "sha256-MIAFa7npdx7a3mSXrlzwrxNY7GyfRdEfgLgHzQUjfrA=",
|
"narHash": "sha256-MIAFa7npdx7a3mSXrlzwrxNY7GyfRdEfgLgHzQUjfrA=",
|
||||||
"owner": "nix-community",
|
"owner": "kolloch",
|
||||||
"repo": "crate2nix",
|
"repo": "crate2nix",
|
||||||
"rev": "08a455fb7dee71720a9bb413fe498d5304708380",
|
"rev": "08a455fb7dee71720a9bb413fe498d5304708380",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "kolloch",
|
||||||
"repo": "crate2nix",
|
"repo": "crate2nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
10
flake.nix
10
flake.nix
|
@ -5,7 +5,7 @@
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
rust-crate2nix = {
|
rust-crate2nix = {
|
||||||
url = "github:nix-community/crate2nix";
|
url = "github:kolloch/crate2nix";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -56,14 +56,6 @@
|
||||||
staxman = project.rootCrate.build;
|
staxman = project.rootCrate.build;
|
||||||
default = packages.staxman;
|
default = packages.staxman;
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
rustc
|
|
||||||
cargo-watch
|
|
||||||
clang
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,13 +91,13 @@ impl ThreadSafeRepository {
|
||||||
Ok(oid)
|
Ok(oid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_files<P: AsRef<Path>>(&self, paths: &[P], 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()?;
|
||||||
for path in paths {
|
for path in paths {
|
||||||
index.add_path(path.as_ref())?;
|
index.add_path(path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.commit(repository, index, message)?;
|
self.commit(repository, index, message)?;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
|
@ -10,13 +8,13 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::error::{AppError, Result},
|
http::error::{AppError, Result},
|
||||||
stack::{self, arion, compose, FileList, COMPOSE_FILE},
|
stack::{arion, compose},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct EditStackForm {
|
pub(super) struct EditStackForm {
|
||||||
files: HashMap<String, String>,
|
source: String,
|
||||||
commit_message: String,
|
commit_message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,21 +29,21 @@ pub(super) async fn edit_stack(
|
||||||
form.commit_message
|
form.commit_message
|
||||||
};
|
};
|
||||||
|
|
||||||
if form.files.is_empty() {
|
if form.source.trim().is_empty() {
|
||||||
return Err(AppError::Client {
|
return Err(AppError::Client {
|
||||||
status: StatusCode::BAD_REQUEST,
|
status: StatusCode::BAD_REQUEST,
|
||||||
code: "invalid-files",
|
code: "invalid-source",
|
||||||
message: "no files provided".to_string(),
|
message: "provided stack source is empty".to_string(),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Cleanup source (like line endings) in each file
|
// Cleanup source (like line endings)
|
||||||
let mut files = FileList::new();
|
let source = form.source.replace("\r\n", "\n");
|
||||||
for (name, source) in form.files {
|
|
||||||
files.insert(name, source.replace("\r\n", "\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
edit_stack_int(state, &stack_name, &files, &commit_message).await?;
|
// Make sure file is ok
|
||||||
|
compose::check(&state.arion_bin, &source).await?;
|
||||||
|
|
||||||
|
edit_stack_int(state, &stack_name, &source, &commit_message).await?;
|
||||||
|
|
||||||
Ok(Redirect::to("./"))
|
Ok(Redirect::to("./"))
|
||||||
}
|
}
|
||||||
|
@ -53,20 +51,17 @@ pub(super) async fn edit_stack(
|
||||||
pub(super) async fn edit_stack_int(
|
pub(super) async fn edit_stack_int(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
stack_name: &str,
|
stack_name: &str,
|
||||||
files: &FileList,
|
source: &str,
|
||||||
commit_message: &str,
|
commit_message: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Get compose
|
|
||||||
if let Some(compose_file) = files.get(COMPOSE_FILE) {
|
|
||||||
// Make sure file is ok
|
// Make sure file is ok
|
||||||
compose::check(&state.arion_bin, compose_file).await?;
|
compose::check(&state.arion_bin, &source).await?;
|
||||||
}
|
|
||||||
|
|
||||||
// Write compose file
|
// Write compose file
|
||||||
stack::write(&state.stack_dir, stack_name, files).await?;
|
compose::write(&state.stack_dir, stack_name, source).await?;
|
||||||
|
|
||||||
// Git commit
|
// Git commit
|
||||||
compose::commit(state.repository, stack_name, files, commit_message)?;
|
compose::commit(state.repository, stack_name, commit_message)?;
|
||||||
|
|
||||||
// Update stack
|
// Update stack
|
||||||
arion::command(
|
arion::command(
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
|
@ -65,13 +63,10 @@ pub(super) async fn restore(
|
||||||
let source = state.repository.get_file_at_commit(&path, &form.oid)?;
|
let source = state.repository.get_file_at_commit(&path, &form.oid)?;
|
||||||
let short_id = form.oid[..6].to_string();
|
let short_id = form.oid[..6].to_string();
|
||||||
|
|
||||||
let mut files = HashMap::new();
|
|
||||||
files.insert(path.to_string_lossy().into_owned(), source);
|
|
||||||
|
|
||||||
edit_stack_int(
|
edit_stack_int(
|
||||||
state,
|
state,
|
||||||
&stack_name,
|
&stack_name,
|
||||||
&files,
|
&source,
|
||||||
format!("Revert to {}", short_id).as_str(),
|
format!("Revert to {}", short_id).as_str(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -7,15 +6,14 @@ use crate::{
|
||||||
http::response::{reply, HandlerResponse},
|
http::response::{reply, HandlerResponse},
|
||||||
node::container::ContainerInfo,
|
node::container::ContainerInfo,
|
||||||
node::nix::parse_arion_compose,
|
node::nix::parse_arion_compose,
|
||||||
stack::{self, FileList, COMPOSE_FILE},
|
stack, AppState,
|
||||||
AppState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "stack/get-one.html")]
|
#[template(path = "stack/get-one.html")]
|
||||||
struct GetOneTemplate {
|
struct GetOneTemplate {
|
||||||
stack_name: String,
|
stack_name: String,
|
||||||
files: FileList,
|
file_contents: String,
|
||||||
containers: Vec<ContainerInfo>,
|
containers: Vec<ContainerInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,24 +21,20 @@ pub(super) async fn get_one(
|
||||||
Path(stack_name): Path<String>,
|
Path(stack_name): Path<String>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> HandlerResponse {
|
) -> HandlerResponse {
|
||||||
let files = stack::get(&state.stack_dir, &stack_name).await?;
|
let file_contents = stack::compose::get(&state.stack_dir, &stack_name).await?;
|
||||||
let compose_file = files
|
let info = parse_arion_compose(&file_contents)?;
|
||||||
.get(COMPOSE_FILE)
|
|
||||||
.ok_or(anyhow!("compose file not found in stack"))?;
|
|
||||||
|
|
||||||
let info = parse_arion_compose(compose_file)?;
|
|
||||||
let containers = stack::list::containers(&state.docker, &info.project).await?;
|
let containers = stack::list::containers(&state.docker, &info.project).await?;
|
||||||
|
|
||||||
reply(
|
reply(
|
||||||
json!({
|
json!({
|
||||||
"folder": stack_name,
|
"folder": stack_name,
|
||||||
"name": info.project,
|
"name": info.project,
|
||||||
"files": files,
|
"file": file_contents,
|
||||||
"containers": containers,
|
"containers": containers,
|
||||||
}),
|
}),
|
||||||
GetOneTemplate {
|
GetOneTemplate {
|
||||||
stack_name: info.project,
|
stack_name: info.project,
|
||||||
files,
|
file_contents,
|
||||||
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
node::{error::StackError, nix::parse_arion_compose},
|
node::{error::StackError, nix::parse_arion_compose},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{arion, utils, FileList, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
use super::{arion, utils, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
||||||
|
|
||||||
pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> {
|
pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> {
|
||||||
let dir = base_dir.join(stack_name);
|
let dir = base_dir.join(stack_name);
|
||||||
|
@ -23,19 +23,18 @@ pub async fn get(base_dir: &Path, stack_name: &str) -> Result<String> {
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit(
|
pub async fn write(base_dir: &Path, stack_name: &str, contents: &str) -> Result<()> {
|
||||||
repository: ThreadSafeRepository,
|
let dir = base_dir.join(stack_name);
|
||||||
stack_name: &str,
|
if !utils::is_stack(&dir).await? {
|
||||||
files: &FileList,
|
return Err(StackError::NotFound.into());
|
||||||
message: &str,
|
}
|
||||||
) -> Result<()> {
|
|
||||||
repository.commit_files(
|
Ok(fs::write(dir.join(COMPOSE_FILE), contents).await?)
|
||||||
&files
|
}
|
||||||
.keys()
|
|
||||||
.map(|key| PathBuf::from(format!("{}/{}", stack_name, key)))
|
pub fn commit(repository: ThreadSafeRepository, stack_name: &str, message: &str) -> Result<()> {
|
||||||
.collect::<Vec<_>>(),
|
let compose_path = path(stack_name);
|
||||||
message,
|
repository.commit_files(&[&compose_path], message)?;
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
use anyhow::Result;
|
|
||||||
use futures_util::future::join_all;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{collections::HashMap, path::Path};
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use crate::node::{container::ContainerInfo, error::StackError};
|
use crate::node::container::ContainerInfo;
|
||||||
|
|
||||||
pub mod arion;
|
pub mod arion;
|
||||||
pub mod compose;
|
pub mod compose;
|
||||||
|
@ -34,57 +30,7 @@ pub struct StackInfo {
|
||||||
pub services: Vec<ServiceInfo>,
|
pub services: Vec<ServiceInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const COMPOSE_FILE: &str = "arion-compose.nix";
|
const COMPOSE_FILE: &str = "arion-compose.nix";
|
||||||
pub const PACKAGE_FILE: &str = "arion-pkgs.nix";
|
const PACKAGE_FILE: &str = "arion-pkgs.nix";
|
||||||
const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; }
|
const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; }
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub type FileList = HashMap<String, String>;
|
|
||||||
|
|
||||||
pub async fn get(base_dir: &Path, stack_name: &str) -> Result<FileList> {
|
|
||||||
let dir = base_dir.join(stack_name);
|
|
||||||
if !utils::is_stack(&dir).await? {
|
|
||||||
return Err(StackError::NotFound.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut files = vec![];
|
|
||||||
let mut dirs = fs::read_dir(&dir).await?;
|
|
||||||
while let Some(entry) = dirs.next_entry().await? {
|
|
||||||
let meta = entry.metadata().await?;
|
|
||||||
if meta.is_dir() {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = dir.join(entry.path());
|
|
||||||
|
|
||||||
files.push(tokio::spawn(async move {
|
|
||||||
fs::read_to_string(path)
|
|
||||||
.await
|
|
||||||
.map(|content| (entry.file_name().to_string_lossy().into_owned(), content))
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(join_all(files)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Result<Result<_, _>, _>>()??)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn write(base_dir: &Path, stack_name: &str, files: &FileList) -> Result<()> {
|
|
||||||
let dir = base_dir.join(stack_name);
|
|
||||||
if !utils::is_stack(&dir).await? {
|
|
||||||
return Err(StackError::NotFound.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
join_all(
|
|
||||||
files
|
|
||||||
.iter()
|
|
||||||
.map(|(name, content)| (dir.join(name), content))
|
|
||||||
.map(|(path, content)| async move { fs::write(path, content).await }),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
import "/static/vendor/ace/ace.js";
|
|
||||||
import { $el } from "../../vendor/domutil/domutil.js";
|
|
||||||
|
|
||||||
class Editor extends HTMLElement {
|
|
||||||
editor;
|
|
||||||
|
|
||||||
/** @type {HTMLDivElement} Tab container */
|
|
||||||
tabEl;
|
|
||||||
|
|
||||||
/** @type {Map<string, AceSession>} File name to file session */
|
|
||||||
files;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.files = {};
|
|
||||||
this.attachShadow({ mode: "open" });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.tabEl = $el("div", { className: "tab-container" });
|
|
||||||
this.shadowRoot.append(this.tabEl);
|
|
||||||
|
|
||||||
const style = $el("style");
|
|
||||||
style.textContent = `
|
|
||||||
.tab-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
& .tab {
|
|
||||||
border: 2px solid var(--table-border-color);
|
|
||||||
padding: 0.5ch 0.8ch;
|
|
||||||
background-color: var(--button-bg);
|
|
||||||
cursor: pointer;
|
|
||||||
&.active {
|
|
||||||
background-color: var(--table-border-color);
|
|
||||||
}
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-left: -2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const slot = $el("slot", { name: "body" });
|
|
||||||
this.shadowRoot.append(style, slot);
|
|
||||||
|
|
||||||
const editorEl = $el("div", { slot: "body" });
|
|
||||||
this.append(editorEl);
|
|
||||||
|
|
||||||
// Create editor
|
|
||||||
ace.config.set('basePath', '/static/vendor/ace')
|
|
||||||
this.editor = ace.edit(editorEl);
|
|
||||||
|
|
||||||
// todo make this stuff customizable??
|
|
||||||
this.editor.setTheme("ace/theme/dracula");
|
|
||||||
this.editor.setOptions({
|
|
||||||
fontFamily: "Iosevka Web",
|
|
||||||
fontSize: "12pt",
|
|
||||||
});
|
|
||||||
this.editor.getSession().setUseWrapMode(true);
|
|
||||||
this.editor.setKeyboardHandler("ace/keyboard/vim");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a file editing session
|
|
||||||
* @param {string} name File name
|
|
||||||
* @param {string} content File contents
|
|
||||||
*/
|
|
||||||
addFile(name, content) {
|
|
||||||
// Add to session list
|
|
||||||
this.files[name] = ace.createEditSession(content);
|
|
||||||
|
|
||||||
// TODO replace this with auto-detection
|
|
||||||
this.files[name].setMode("ace/mode/nix");
|
|
||||||
|
|
||||||
// Create tab and set as active
|
|
||||||
const tab = this.#addTab(name);
|
|
||||||
tab.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new tab
|
|
||||||
* @param {string} name Tab name
|
|
||||||
* @returns Tab element
|
|
||||||
*/
|
|
||||||
#addTab(name) {
|
|
||||||
const tab = $el("div", {
|
|
||||||
className: "tab",
|
|
||||||
"@click": () => {
|
|
||||||
this.editor.setSession(this.files[name]);
|
|
||||||
this.tabEl.querySelectorAll(".tab").forEach(el => el.classList.remove("active"));
|
|
||||||
tab.classList.add("active");
|
|
||||||
}
|
|
||||||
}, name);
|
|
||||||
this.tabEl.append(tab);
|
|
||||||
return tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("file-editor", Editor);
|
|
|
@ -26,14 +26,9 @@
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
& noscript {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.nojs-editor {
|
#editor {
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
min-height: 50vh;
|
min-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
1
static/vendor/domutil/domutil.d.ts
vendored
1
static/vendor/domutil/domutil.d.ts
vendored
|
@ -1 +0,0 @@
|
||||||
export * from "./makeDOM.ts";
|
|
2
static/vendor/domutil/domutil.js
vendored
2
static/vendor/domutil/domutil.js
vendored
|
@ -1,2 +0,0 @@
|
||||||
var f=Object.defineProperty;var m=(t,n)=>f(t,"name",{value:n,configurable:!0});function d(t,...n){let l=t,o="",T="";t.includes("#")&&([l,T]=t.split("#")),t.includes(".")&&([l,o]=t.split("."));let a=document.createElement(l);o&&(a.className=o),T&&(a.id=T);let s=n[0];if(typeof s=="object"&&!(s instanceof Node)){for(let e in s){let i=s[e];if(e.startsWith("@"))a.addEventListener(e.substring(1),i);else if(e==="dataset")for(let r in i)a.dataset[r]=i[r];else a[e]=i}n.shift()}for(let e of n)a.appendChild(e instanceof Node?e:document.createTextNode(e));return a}m(d,"$el");export{d as $el};
|
|
||||||
//# sourceMappingURL=domutil.js.map
|
|
7
static/vendor/domutil/domutil.js.map
vendored
7
static/vendor/domutil/domutil.js.map
vendored
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"sources": ["../makeDOM.ts"],
|
|
||||||
"sourcesContent": ["// Inspired by `make` by Matthew Crumley (silentmatt.com) https://stackoverflow.com/a/2947012\r\n// Licensed under AGPL-3.0, check `LICENSE` for the full text.\r\n\r\ntype ElementProperties<T extends keyof HTMLElementTagNameMap> = {\r\n [key in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][key];\r\n} & {\r\n [key in keyof HTMLElementEventMap as `@${key}`]: (\r\n this: HTMLElement,\r\n ev: HTMLElementEventMap[key]\r\n ) => void;\r\n};\r\n\r\ntype DOMElem = Node | string;\r\n\r\ntype WithClassOrId<T extends keyof HTMLElementTagNameMap> =\r\n | `${T}#${string}`\r\n | `${T}.${string}`;\r\n\r\nexport function $el<T extends keyof HTMLElementTagNameMap>(\r\n name: T | WithClassOrId<T>,\r\n ...desc: [Partial<ElementProperties<T>>, ...DOMElem[]] | DOMElem[]\r\n): HTMLElementTagNameMap[T] {\r\n // Take off ID or class from the name\r\n let elementName = name as string;\r\n let className = \"\";\r\n let id = \"\";\r\n\r\n if (name.includes(\"#\")) {\r\n [elementName, id] = name.split(\"#\");\r\n }\r\n\r\n if (name.includes(\".\")) {\r\n [elementName, className] = name.split(\".\");\r\n }\r\n\r\n // Create the element and add the attributes (if found)\r\n const el = document.createElement(elementName as T);\r\n if (className) {\r\n el.className = className;\r\n }\r\n if (id) {\r\n el.id = id;\r\n }\r\n\r\n const attributes = desc[0];\r\n if (typeof attributes === \"object\" && !(attributes instanceof Node)) {\r\n for (const attr in attributes) {\r\n const value = (attributes as Record<string, any>)[attr];\r\n if (attr.startsWith(\"@\")) {\r\n el.addEventListener(attr.substring(1), value);\r\n } else if (attr === \"dataset\") {\r\n for (const key in value) {\r\n el.dataset[key] = value[key];\r\n }\r\n } else {\r\n el[attr as keyof HTMLElementTagNameMap[T]] = value;\r\n }\r\n }\r\n desc.shift();\r\n }\r\n\r\n for (const item of desc as DOMElem[]) {\r\n el.appendChild(item instanceof Node ? item : document.createTextNode(item));\r\n }\r\n\r\n return el;\r\n}\r\n\r\nexport default $el;\r\n"],
|
|
||||||
"mappings": "+EAkBO,SAASA,EACdC,KACGC,EACuB,CAE1B,IAAIC,EAAcF,EACdG,EAAY,GACZC,EAAK,GAELJ,EAAK,SAAS,GAAG,IACnB,CAACE,EAAaE,CAAE,EAAIJ,EAAK,MAAM,GAAG,GAGhCA,EAAK,SAAS,GAAG,IACnB,CAACE,EAAaC,CAAS,EAAIH,EAAK,MAAM,GAAG,GAI3C,IAAMK,EAAK,SAAS,cAAcH,CAAgB,EAC9CC,IACFE,EAAG,UAAYF,GAEbC,IACFC,EAAG,GAAKD,GAGV,IAAME,EAAaL,EAAK,CAAC,EACzB,GAAI,OAAOK,GAAe,UAAY,EAAEA,aAAsB,MAAO,CACnE,QAAWC,KAAQD,EAAY,CAC7B,IAAME,EAASF,EAAmCC,CAAI,EACtD,GAAIA,EAAK,WAAW,GAAG,EACrBF,EAAG,iBAAiBE,EAAK,UAAU,CAAC,EAAGC,CAAK,UACnCD,IAAS,UAClB,QAAWE,KAAOD,EAChBH,EAAG,QAAQI,CAAG,EAAID,EAAMC,CAAG,OAG7BJ,EAAGE,CAAsC,EAAIC,CAEjD,CACAP,EAAK,MAAM,CACb,CAEA,QAAWS,KAAQT,EACjBI,EAAG,YAAYK,aAAgB,KAAOA,EAAO,SAAS,eAAeA,CAAI,CAAC,EAG5E,OAAOL,CACT,CAhDgBM,EAAAZ,EAAA",
|
|
||||||
"names": ["$el", "name", "desc", "elementName", "className", "id", "el", "attributes", "attr", "value", "key", "item", "__name"]
|
|
||||||
}
|
|
9
static/vendor/domutil/makeDOM.d.ts
vendored
9
static/vendor/domutil/makeDOM.d.ts
vendored
|
@ -1,9 +0,0 @@
|
||||||
type ElementProperties<T extends keyof HTMLElementTagNameMap> = {
|
|
||||||
[key in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][key];
|
|
||||||
} & {
|
|
||||||
[key in keyof HTMLElementEventMap as `@${key}`]: (this: HTMLElement, ev: HTMLElementEventMap[key]) => void;
|
|
||||||
};
|
|
||||||
type DOMElem = Node | string;
|
|
||||||
type WithClassOrId<T extends keyof HTMLElementTagNameMap> = `${T}#${string}` | `${T}.${string}`;
|
|
||||||
export declare function $el<T extends keyof HTMLElementTagNameMap>(name: T | WithClassOrId<T>, ...desc: [Partial<ElementProperties<T>>, ...DOMElem[]] | DOMElem[]): HTMLElementTagNameMap[T];
|
|
||||||
export default $el;
|
|
|
@ -192,8 +192,8 @@
|
||||||
logSource.close();
|
logSource.close();
|
||||||
const err = document.createElement("div");
|
const err = document.createElement("div");
|
||||||
err.className = "error";
|
err.className = "error";
|
||||||
err.append(document.createTextNode("No logs available after this (container not running)"));
|
err.appendChild(document.createTextNode("No logs available after this (container not running)"));
|
||||||
logEl.append(err);
|
logEl.appendChild(err);
|
||||||
err.scrollIntoView();
|
err.scrollIntoView();
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
@ -204,8 +204,8 @@
|
||||||
logSource.close();
|
logSource.close();
|
||||||
const err = document.createElement("div");
|
const err = document.createElement("div");
|
||||||
err.className = "error";
|
err.className = "error";
|
||||||
err.append(document.createTextNode(data.error));
|
err.appendChild(document.createTextNode(data.error));
|
||||||
logEl.append(err);
|
logEl.appendChild(err);
|
||||||
err.scrollIntoView();
|
err.scrollIntoView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@
|
||||||
const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)];
|
const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)];
|
||||||
const lineEl = document.createElement("p");
|
const lineEl = document.createElement("p");
|
||||||
lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
|
lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
|
||||||
logEl.append(lineEl);
|
logEl.appendChild(lineEl);
|
||||||
lineEl.scrollIntoView();
|
lineEl.scrollIntoView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
{% block title %}Stack {{stack_name}}{% endblock %}
|
{% block title %}Stack {{stack_name}}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script type="module" src="/static/components/editor/script.mjs" defer></script>
|
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
|
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
|
||||||
|
@ -63,14 +62,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
<form method="POST" action="./edit" id="editor-form">
|
<form method="POST" action="./edit" id="editor-form">
|
||||||
<div class="error"></div>
|
<div class="error"></div>
|
||||||
{% for (filename, content) in files %}
|
<textarea name="source" id="editor">{{file_contents}}</textarea>
|
||||||
<script type="stackfile" data-name="{{filename}}">{{content|safe}}</script>
|
|
||||||
<noscript>
|
|
||||||
<h3>{{filename}}</h3>
|
|
||||||
<textarea name="file[{{filename}}]" class="nojs-editor">{{content|safe}}</textarea>
|
|
||||||
</noscript>
|
|
||||||
{% endfor %}
|
|
||||||
<file-editor></file-editor>
|
|
||||||
<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?" />
|
||||||
|
@ -113,16 +105,14 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
import Editor from "/static/js/ace.mjs";
|
||||||
import { add_check } from "/static/js/enhancements/check.mjs";
|
import { add_check } from "/static/js/enhancements/check.mjs";
|
||||||
|
|
||||||
const editor = document.querySelector("file-editor");
|
const editor = new Editor("editor");
|
||||||
document.querySelectorAll("script[type=stackfile]").forEach((script) => {
|
|
||||||
editor.addFile(script.dataset.name, script.innerText);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Enforce check pre-submit */
|
/* Enforce check pre-submit */
|
||||||
//const form = document.getElementById("editor-form");
|
const form = document.getElementById("editor-form");
|
||||||
//add_check(form, editor);
|
add_check(form, editor);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -129,12 +129,12 @@
|
||||||
|
|
||||||
let toggle = document.createElement("button");
|
let toggle = document.createElement("button");
|
||||||
toggle.type = "button";
|
toggle.type = "button";
|
||||||
toggle.append(document.createTextNode(toggle_text[0]));
|
toggle.appendChild(document.createTextNode(toggle_text[0]));
|
||||||
toggle.addEventListener("click", () => {
|
toggle.addEventListener("click", () => {
|
||||||
const index = toggle_text.indexOf(toggle.textContent);
|
const index = toggle_text.indexOf(toggle.textContent);
|
||||||
versions.forEach(version => { version.open = index == 1 });
|
versions.forEach(version => { version.open = index == 1 });
|
||||||
toggle.textContent = toggle_text[(index + 1) % 2];
|
toggle.textContent = toggle_text[(index + 1) % 2];
|
||||||
});
|
});
|
||||||
actions.append(toggle);
|
actions.appendChild(toggle);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in a new issue