diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f5d72cd1..ec2ed74ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,12 @@ jobs: working-directory: pgml-extension steps: - uses: actions/checkout@v3 + - name: Changed files in pgml-extension + id: pgml_extension_changed + run: | + echo "PGML_EXTENSION_CHANGED_FILES=$(git diff --name-only HEAD HEAD~1 . | wc -l)" >> $GITHUB_OUTPUT - name: Install dependencies + if: steps.pgml_extension_changed.outputs.PGML_EXTENSION_CHANGED_FILES != '0' run: | sudo apt-get update && \ DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC sudo apt-get install -y \ @@ -29,6 +34,7 @@ jobs: sudo pip3 install -r requirements.txt - name: Cache dependencies uses: buildjet/cache@v3 + if: steps.pgml_extension_changed.outputs.PGML_EXTENSION_CHANGED_FILES != '0' with: path: | ~/.cargo @@ -36,9 +42,11 @@ jobs: ~/.pgrx key: ${{ runner.os }}-rust-3-${{ hashFiles('pgml-extension/Cargo.lock') }} - name: Submodules + if: steps.pgml_extension_changed.outputs.PGML_EXTENSION_CHANGED_FILES != '0' run: | git submodule update --init --recursive - name: Run tests + if: steps.pgml_extension_changed.outputs.PGML_EXTENSION_CHANGED_FILES != '0' run: | curl https://sh.rustup.rs -sSf | sh -s -- -y source ~/.cargo/env diff --git a/pgml-apps/cargo-pgml-components/Cargo.lock b/pgml-apps/cargo-pgml-components/Cargo.lock new file mode 100644 index 000000000..3c5ee69e9 --- /dev/null +++ b/pgml-apps/cargo-pgml-components/Cargo.lock @@ -0,0 +1,449 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cargo-pgml-components" +version = "0.1.5" +dependencies = [ + "clap", + "convert_case", + "env_logger", + "glob", + "log", + "md5", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "clap" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustix" +version = "0.38.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/pgml-apps/cargo-pgml-components/Cargo.toml b/pgml-apps/cargo-pgml-components/Cargo.toml new file mode 100644 index 000000000..dcb4cdd23 --- /dev/null +++ b/pgml-apps/cargo-pgml-components/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cargo-pgml-components" +version = "0.1.6" +edition = "2021" +authors = ["PostgresML "] +license = "MIT" +description = "A tool for bundling SCSS and JavaScript Stimulus components like Rails does." + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +glob = "0.3" +convert_case = "0.6" +clap = { version = "4", features = ["derive"] } +md5 = "0.7" +log = "0.4" +env_logger = "0.10" diff --git a/pgml-apps/cargo-pgml-components/src/main.rs b/pgml-apps/cargo-pgml-components/src/main.rs new file mode 100644 index 000000000..4ed3305d8 --- /dev/null +++ b/pgml-apps/cargo-pgml-components/src/main.rs @@ -0,0 +1,428 @@ +//! A tool to assemble and bundle our frontend components. + +use clap::{Args, Parser, Subcommand}; +use convert_case::{Case, Casing}; +use glob::glob; +use std::env::{current_dir, set_current_dir}; +use std::fs::{create_dir_all, read_to_string, remove_file, File, read_dir}; +use std::io::Write; +use std::path::Path; +use std::process::{exit, Command}; + +#[macro_use] +extern crate log; + +/// These paths are exepcted to exist in the project directory. +static PROJECT_PATHS: &[&str] = &["src", "static/js", "static/css"]; + +//// These executables are required to be installed globally. +static REQUIRED_EXECUTABLES: &[&str] = &["sass", "rollup"]; + +static COMPONENT_TEMPLATE_RS: &'static str = r#" +use sailfish::TemplateOnce; +use crate::components::component; + +#[derive(TemplateOnce, Default)] +#[template(path = "{component_path}/template.html")] +pub struct {component_name} { + value: String, +} + +impl {component_name} { + pub fn new() -> {component_name} { + {component_name}::default() + } +} + +component!({component_name}); +"#; + +static COMPONENT_STIMULUS_JS: &'static str = r#" +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static targets = [] + static outlets = [] + + initialize() { + console.log('Initialized {controller_name}') + } + + connect() {} + + disconnect() {} +} +"#; + +static COMPONENT_HTML: &'static str = r#" +
+ <%= value %> +
+"#; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None, propagate_version = true, bin_name = "cargo", name = "cargo")] +struct Cli { + #[command(subcommand)] + subcomand: CargoSubcommands, +} + +#[derive(Subcommand, Debug)] +enum CargoSubcommands { + PgmlComponents(PgmlCommands), +} + +#[derive(Args, Debug)] +struct PgmlCommands { + #[command(subcommand)] + command: Commands, + + #[arg(short, long)] + project_path: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Bundle SASS and JavaScript into neat bundle files. + Bundle {}, + + /// Add a new component. + AddComponent { + name: String, + + #[arg(short, long, default_value = "false")] + overwrite: bool, + }, + + UpdateComponents {}, +} + +fn main() { + env_logger::init(); + let cli = Cli::parse(); + + match cli.subcomand { + CargoSubcommands::PgmlComponents(pgml_commands) => match pgml_commands.command { + Commands::Bundle {} => bundle(pgml_commands.project_path), + Commands::AddComponent { name, overwrite } => add_component(name, overwrite), + Commands::UpdateComponents {} => update_components(), + + }, + } +} + +fn execute_command(command: &mut Command) -> std::io::Result { + let output = match command.output() { + Ok(output) => output, + Err(err) => { + return Err(err); + } + }; + + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let stdout = String::from_utf8_lossy(&output.stderr).to_string(); + + if !output.status.success() { + error!( + "{} failed: {}", + command.get_program().to_str().unwrap(), + String::from_utf8_lossy(&output.stderr).to_string(), + ); + exit(1); + } + + if !stderr.is_empty() { + warn!("{}", stderr); + } + + if !stdout.is_empty() { + info!("{}", stdout); + } + + Ok(stdout) +} + +fn check_executables() { + for executable in REQUIRED_EXECUTABLES { + match execute_command(Command::new(executable).arg("--version")) { + Ok(_) => (), + Err(err) => { + error!( + "'{}' is not installed. Install it with 'npm install -g {}'", + executable, executable + ); + debug!( + "Failed to execute '{} --version': {}", + executable, + err.to_string() + ); + exit(1); + } + } + } +} + +/// Bundle SASS and JavaScript into neat bundle files. +fn bundle(project_path: Option) { + check_executables(); + + // Validate that the required project paths exist. + let cwd = if let Some(project_path) = project_path { + project_path + } else { + current_dir().unwrap().to_str().unwrap().to_owned() + }; + + let path = Path::new(&cwd); + + for project_path in PROJECT_PATHS { + let check = path.join(project_path); + + if !check.exists() { + error!( + "Project path '{}/{}' does not exist but is required", + path.display(), + project_path + ); + exit(1); + } + } + + set_current_dir(path).expect("failed to change paths"); + + // Assemble SCSS. + let scss = glob("src/components/**/*.scss").expect("failed to glob scss files"); + + let mut modules = + File::create("static/css/modules.scss").expect("failed to create modules.scss"); + + for stylesheet in scss { + let stylesheet = stylesheet.expect("failed to glob stylesheet"); + + debug!("Adding '{}' to SCSS bundle", stylesheet.display()); + + let line = format!(r#"@import "../../{}";"#, stylesheet.display()); + + writeln!(&mut modules, "{}", line).expect("failed to write line to modules.scss"); + } + + drop(modules); + + // Clean up old bundles + for file in glob("static/css/style.*.css").expect("failed to glob") { + let file = file.expect("failed to glob file"); + debug!("Removing '{}'", file.display()); + let _ = remove_file(file); + } + + // Bundle SCSS. + // Build Bootstrap + execute_command( + Command::new("sass") + .arg("static/css/bootstrap-theme.scss") + .arg("static/css/style.css"), + ) + .unwrap(); + + // Hash the bundle. + let bundle = read_to_string("static/css/style.css").expect("failed to read bundle.css"); + let hash = format!("{:x}", md5::compute(bundle)) + .chars() + .take(8) + .collect::(); + + execute_command( + Command::new("cp") + .arg("static/css/style.css") + .arg(format!("static/css/style.{}.css", hash)), + ) + .unwrap(); + + let mut hash_file = + File::create("static/css/.pgml-bundle").expect("failed to create .pgml-bundle"); + writeln!(&mut hash_file, "{}", hash).expect("failed to write hash to .pgml-bundle"); + drop(hash_file); + + debug!("Created css .pgml-bundle with hash {}", hash); + + // Assemble JavaScript. + + // Remove prebuilt files. + for file in glob::glob("static/js/*.*.js").expect("failed to glob") { + let _ = remove_file(file.expect("failed to glob file")); + } + + let js = glob("src/components/**/*.js").expect("failed to glob js files"); + let js = js.chain(glob("static/js/*.js").expect("failed to glob static/js/*.js")); + let js = js.filter(|path| { + let path = path.as_ref().unwrap(); + let path = path.display().to_string(); + + !path.contains("main.js") && !path.contains("bundle.js") && !path.contains("modules.js") + }); + + let mut modules = File::create("static/js/modules.js").expect("failed to create modules.js"); + + writeln!(&mut modules, "// Build with --bin components").unwrap(); + writeln!( + &mut modules, + "import {{ Application }} from '@hotwired/stimulus'" + ) + .expect("failed to write to modules.js"); + writeln!(&mut modules, "const application = Application.start()") + .expect("failed to write to modules.js"); + + for source in js { + let source = source.expect("failed to glob js file"); + + let full_path = source.display(); + let stem = source.file_stem().unwrap().to_str().unwrap(); + let upper_camel = stem.to_case(Case::UpperCamel); + + let mut controller_name = stem.split("_").collect::>(); + + if stem.contains("controller") { + let _ = controller_name.pop().unwrap(); + } + + let controller_name = controller_name.join("-"); + + writeln!( + &mut modules, + "import {{ default as {} }} from '../../{}'", + upper_camel, full_path + ) + .unwrap(); + writeln!( + &mut modules, + "application.register('{}', {})", + controller_name, upper_camel + ) + .unwrap(); + } + + drop(modules); + + // Bundle JavaScript. + execute_command( + Command::new("rollup") + .arg("static/js/modules.js") + .arg("--file") + .arg("static/js/bundle.js") + .arg("--format") + .arg("es"), + ) + .unwrap(); + + // Hash the bundle. + let bundle = read_to_string("static/js/bundle.js").expect("failed to read bundle.js"); + let hash = format!("{:x}", md5::compute(bundle)) + .chars() + .take(8) + .collect::(); + + execute_command( + Command::new("cp") + .arg("static/js/bundle.js") + .arg(format!("static/js/bundle.{}.js", hash)), + ) + .unwrap(); + + let mut hash_file = + File::create("static/js/.pgml-bundle").expect("failed to create .pgml-bundle"); + writeln!(&mut hash_file, "{}", hash).expect("failed to write hash to .pgml-bundle"); + drop(hash_file); + + println!("Finished bundling CSS and JavaScript successfully"); +} + +fn add_component(name: String, overwrite: bool) { + let component_name = name.as_str().to_case(Case::UpperCamel); + let component_path = name.as_str().to_case(Case::Snake); + let folder = Path::new("src/components").join(&component_path); + + if !folder.exists() { + match create_dir_all(folder.clone()) { + Ok(_) => (), + Err(err) => { + error!( + "Failed to create path '{}' for component '{}': {}", + folder.display(), + name, + err + ); + exit(1); + } + } + } else if !overwrite { + error!("Component '{}' already exists", folder.display()); + exit(1); + } + + // Create mod.rs + let mod_file = format!( + "{}", + COMPONENT_TEMPLATE_RS + .replace("{component_name}", &component_name) + .replace("{component_path}", &component_path) + ); + + let mod_path = folder.join("mod.rs"); + + let mut mod_file_fd = File::create(mod_path).expect("failed to create mod.rs"); + writeln!(&mut mod_file_fd, "{}", mod_file.trim()).expect("failed to write mod.rs"); + drop(mod_file_fd); + + // Create template.html + let template_path = folder.join("template.html"); + let mut template_file = File::create(template_path).expect("failed to create template.html"); + let template_source = + COMPONENT_HTML.replace("{controller_name}", &component_path.replace("_", "-")); + writeln!(&mut template_file, "{}", template_source.trim(),) + .expect("failed to write template.html"); + drop(template_file); + + // Create Stimulus controller + let stimulus_path = folder.join(&format!("{}_controller.js", component_path)); + let mut template_file = + File::create(stimulus_path).expect("failed to create stimulus controller"); + let controller_source = + COMPONENT_STIMULUS_JS.replace("{controller_name}", &component_path.replace("_", "-")); + writeln!(&mut template_file, "{}", controller_source.trim()) + .expect("failed to write stimulus controller"); + drop(template_file); + + // Create SASS file + let sass_path = folder.join(&format!("{}.scss", component_path)); + let sass_file = File::create(sass_path).expect("failed to create sass file"); + drop(sass_file); + + println!("Component '{}' created successfully", folder.display()); + update_components(); +} + +fn update_components() { + let mut file = File::create("src/components/mod.rs").expect("failed to create mod.rs"); + + writeln!(&mut file, "// This file is automatically generated by cargo-pgml-components.").expect("failed to write to mod.rs"); + writeln!(&mut file, "// Do not modify it directly.").expect("failed to write to mod.rs"); + writeln!(&mut file, "mod component;").expect("failed to write to mod.rs"); + writeln!(&mut file, "pub(crate) use component::{{component, Component}};").expect("failed to write to mod.rs"); + + for component in read_dir("src/components").expect("failed to read components directory") { + let path = component.expect("dir entry").path(); + + if path.is_file() { + continue; + } + + let components = path.components(); + let component_name = components.clone().last().expect("component_name").as_os_str().to_str().unwrap(); + let module = components.skip(2).map(|c| c.as_os_str().to_str().unwrap()).collect::>().join("::"); + // let module = format!("crate::{}", module); + let component_name = component_name.to_case(Case::UpperCamel); + + writeln!(&mut file, "pub mod {};", module).expect("failed to write to mod.rs"); + writeln!(&mut file, "pub use {}::{};", module, component_name).expect("failed to write to mod.rs"); + } +} diff --git a/pgml-dashboard/Cargo.lock b/pgml-dashboard/Cargo.lock index fa7fa787f..f48f3617c 100644 --- a/pgml-dashboard/Cargo.lock +++ b/pgml-dashboard/Cargo.lock @@ -1013,7 +1013,7 @@ dependencies = [ "atomic", "pear", "serde", - "toml 0.7.6", + "toml", "uncased", "version_check", ] @@ -1785,12 +1785,6 @@ dependencies = [ "digest", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "measure_time" version = "0.8.2" @@ -2192,7 +2186,6 @@ dependencies = [ "itertools", "lazy_static", "log", - "md5", "num-traits", "once_cell", "parking_lot 0.12.1", @@ -2872,9 +2865,9 @@ checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] name = "sailfish" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79aef0b4612749106d372dfdeee715082f2f0fe24263be08e19db9b00b694bf9" +checksum = "7519b7521780097b0183bb4b0c7c2165b924f5f1d44c3ef765bde8c2f8008fd1" dependencies = [ "itoap", "ryu", @@ -2884,9 +2877,9 @@ dependencies = [ [[package]] name = "sailfish-compiler" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "787ef14715822299715d98d6eb6157f03a57a5258ffbd3321847f7450853dd64" +checksum = "535500faca492ee8054fbffdfca6447ca97fa495e0ede9f28fa473e1a44f9d5c" dependencies = [ "filetime", "home", @@ -2894,15 +2887,15 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 1.0.109", - "toml 0.5.11", + "syn 2.0.26", + "toml", ] [[package]] name = "sailfish-macros" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d39ce164c9e19147bcc4fa9ce9dcfc0a451e6cd0a996bb896fc7dee92887a4" +checksum = "06a95a6b8a0f59bf66f430a4ed37ece23fcefcd26898399573043e56fb202be2" dependencies = [ "proc-macro2", "sailfish-compiler", @@ -3874,15 +3867,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.7.6" diff --git a/pgml-dashboard/Cargo.toml b/pgml-dashboard/Cargo.toml index f42aa14b1..64dc8e909 100644 --- a/pgml-dashboard/Cargo.toml +++ b/pgml-dashboard/Cargo.toml @@ -8,6 +8,7 @@ description = "Web dashboard for PostgresML, an end-to-end machine learning plat homepage = "https://postgresml.org" repository = "https://github.com/postgremsl/postgresml" include = ["src/", "sqlx-data.json", "templates/", "migrations/", "static/"] +default-run = "pgml-dashboard" [dependencies] anyhow = "1" @@ -18,7 +19,6 @@ chrono = "0.4" csv-async = "1" dotenv = "0.15" env_logger = "0.10" -glob = "0.3" itertools = "0.10" parking_lot = "0.12" lazy_static = "1.4" @@ -28,7 +28,7 @@ once_cell = "1.18" rand = "0.8" regex = "1.9" rocket = { git = "https://github.com/SergioBenitez/Rocket", features = ["secrets", "json"] } -sailfish = "0.5" +sailfish = "0.8" scraper = "0.17" serde = "1" sentry = "0.31" @@ -43,7 +43,4 @@ yaml-rust = "0.4" zoomies = { git="https://github.com/HyperparamAI/zoomies.git", branch="master" } pgvector = { version = "0.2.2", features = [ "sqlx", "postgres" ] } console-subscriber = "*" - -[build-dependencies] -md5 = "0.7" -glob = "0.3" +glob = "*" diff --git a/pgml-dashboard/Dockerfile b/pgml-dashboard/Dockerfile index 7c76db74d..a72f9ecd1 100644 --- a/pgml-dashboard/Dockerfile +++ b/pgml-dashboard/Dockerfile @@ -1,6 +1,7 @@ FROM rust:1 RUN cargo install sqlx-cli RUN apt-get update && apt-get install -y nodejs npm -RUN npm install -g sass +RUN npm install -g sass rollup +RUN cargo install cargo-pgml-components COPY . /app WORKDIR /app diff --git a/pgml-dashboard/build.rs b/pgml-dashboard/build.rs index 3e24d9751..9cbc9e68b 100644 --- a/pgml-dashboard/build.rs +++ b/pgml-dashboard/build.rs @@ -1,8 +1,10 @@ -use std::fs::{read_to_string, remove_file}; +use std::fs::read_to_string; use std::process::Command; fn main() { println!("cargo:rerun-if-changed=migrations"); + println!("cargo:rerun-if-changed=static/css/.pgml-bundle"); + println!("cargo:rerun-if-changed=static/js/.pgml-bundle"); let output = Command::new("git") .args(&["rev-parse", "HEAD"]) @@ -11,77 +13,31 @@ fn main() { let git_hash = String::from_utf8(output.stdout).unwrap(); println!("cargo:rustc-env=GIT_SHA={}", git_hash); - // Build Bootstrap - let status = Command::new("npm") - .arg("exec") - .arg("sass") - .arg("static/css/bootstrap-theme.scss") - .arg("static/css/style.css") + let status = Command::new("cargo") + .arg("pgml-components") + .arg("bundle") .status() - .expect("`npm exec sass` failed"); + .expect("failed to run 'cargo pgml-bundle'"); if !status.success() { - println!("SCSS compilation failed to run"); + panic!("failed to run 'cargo pgml-bundle'"); } - // Bundle CSS to bust cache. - let contents = read_to_string("static/css/style.css") - .unwrap() - .as_bytes() - .to_vec(); - let css_version = format!("{:x}", md5::compute(contents)) - .chars() - .take(8) - .collect::(); + let css_version = + read_to_string("static/css/.pgml-bundle").expect("failed to read .pgml-bundle"); + let css_version = css_version.trim(); - if !Command::new("cp") - .arg("static/css/style.css") - .arg(format!("static/css/style.{}.css", css_version)) - .status() - .expect("cp static/css/style.css failed to run") - .success() - { - println!("Bundling CSS failed"); - } - - let mut js_version = Vec::new(); - - // Remove all bundled files - for file in glob::glob("static/js/*.*.js").expect("failed to glob") { - let _ = remove_file(file.expect("failed to glob file")); - } - - // Build JS to bust cache - for file in glob::glob("static/js/*.js").expect("failed to glob") { - let file = file.expect("failed to glob path"); - let contents = read_to_string(file) - .expect("failed to read js file") - .as_bytes() - .to_vec(); + let js_version = read_to_string("static/js/.pgml-bundle").expect("failed to read .pgml-bundle"); + let js_version = js_version.trim(); - js_version.push(format!("{:x}", md5::compute(contents))); - } - - let js_version = format!("{:x}", md5::compute(js_version.join("").as_bytes())) - .chars() - .take(8) - .collect::(); - - for file in glob::glob("static/js/*.js").expect("failed to glob JS") { - let filename = file.expect("failed to glob path").display().to_string(); - let name = filename.split(".").collect::>(); - let name = name[0..name.len() - 1].join("."); - let output_name = format!("{}.{}.js", name, js_version); + let status = Command::new("cp") + .arg("static/js/main.js") + .arg(&format!("static/js/main.{}.js", js_version)) + .status() + .expect("failed to bundle main.js"); - if !Command::new("cp") - .arg(&filename) - .arg(&output_name) - .status() - .expect("failed to cp js file") - .success() - { - println!("Bundling JS failed"); - } + if !status.success() { + panic!("failed to bundle main.js"); } println!("cargo:rustc-env=CSS_VERSION={css_version}"); diff --git a/pgml-dashboard/content/docs/guides/setup/v2/installation.md b/pgml-dashboard/content/docs/guides/setup/v2/installation.md index dec066ed7..e5f128450 100644 --- a/pgml-dashboard/content/docs/guides/setup/v2/installation.md +++ b/pgml-dashboard/content/docs/guides/setup/v2/installation.md @@ -279,6 +279,7 @@ python3 python3-pip libpython3 lld +mold ``` ##### Rust @@ -352,7 +353,7 @@ cargo sqlx database setup ### Frontend dependencies -The dashboard frontend is using Sass which requires Node & the Sass compiler. You can install Node from Brew, your package repository, or by using [Node Version Manager](https://github.com/nvm-sh/nvm). +The dashboard frontend is using Sass and Rollup, which require Node. You can install Node from Brew, your package repository, or by using [Node Version Manager](https://github.com/nvm-sh/nvm). If using nvm, you can install the latest stable Node version with: @@ -360,10 +361,11 @@ If using nvm, you can install the latest stable Node version with: nvm install stable ``` -Once you have Node installed, you can install the Sass compiler globally: +Once you have Node installed, you can install the remaining requirements globally: ```bash -npm install -g sass +npm install -g sass rollup +cargo install cargo-pgml-components ``` ### Compile and run diff --git a/pgml-dashboard/sailfish.toml b/pgml-dashboard/sailfish.toml new file mode 100644 index 000000000..a86fbf322 --- /dev/null +++ b/pgml-dashboard/sailfish.toml @@ -0,0 +1 @@ +template_dirs = ["templates", "src/templates", "src/components"] diff --git a/pgml-dashboard/src/components/breadcrumbs/mod.rs b/pgml-dashboard/src/components/breadcrumbs/mod.rs new file mode 100644 index 000000000..9f711dd64 --- /dev/null +++ b/pgml-dashboard/src/components/breadcrumbs/mod.rs @@ -0,0 +1,17 @@ +use crate::components::component; +use crate::components::NavLink; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "breadcrumbs/template.html")] +pub struct Breadcrumbs<'a> { + pub links: Vec>, +} + +impl<'a> Breadcrumbs<'a> { + pub fn render(links: Vec>) -> String { + Breadcrumbs { links }.render_once().unwrap() + } +} + +component!(Breadcrumbs, 'a); diff --git a/pgml-dashboard/templates/components/breadcrumbs.html b/pgml-dashboard/src/components/breadcrumbs/template.html similarity index 100% rename from pgml-dashboard/templates/components/breadcrumbs.html rename to pgml-dashboard/src/components/breadcrumbs/template.html diff --git a/pgml-dashboard/src/components/component.rs b/pgml-dashboard/src/components/component.rs new file mode 100644 index 000000000..a07af3ebf --- /dev/null +++ b/pgml-dashboard/src/components/component.rs @@ -0,0 +1,54 @@ +//! A basic UI component. Any other component can accept this +//! as a parameter and render it. + +use sailfish::TemplateOnce; + +#[derive(Default, Clone, TemplateOnce)] +#[template(path = "components/component.html")] +pub struct Component { + pub value: String, +} + +macro_rules! component { + ($name:tt) => { + impl From<$name> for crate::components::Component { + fn from(thing: $name) -> crate::components::Component { + use sailfish::TemplateOnce; + + crate::components::Component { + value: thing.render_once().unwrap(), + } + } + } + }; + + ($name:tt, $lifetime:lifetime) => { + impl<$lifetime> From<$name<$lifetime>> for crate::components::Component { + fn from(thing: $name<$lifetime>) -> crate::components::Component { + use sailfish::TemplateOnce; + + crate::components::Component { + value: thing.render_once().unwrap(), + } + } + } + }; +} + +pub(crate) use component; + +// Render any string. +impl From<&str> for Component { + fn from(value: &str) -> Component { + Component { + value: value.to_owned(), + } + } +} + +// Render any string. +impl From for Component { + fn from(value: String) -> Component { + Component { value } + } +} diff --git a/pgml-dashboard/src/components/confirm_modal/mod.rs b/pgml-dashboard/src/components/confirm_modal/mod.rs new file mode 100644 index 000000000..e2d9b4ec5 --- /dev/null +++ b/pgml-dashboard/src/components/confirm_modal/mod.rs @@ -0,0 +1,31 @@ +use crate::components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "confirm_modal/template.html")] +pub struct ConfirmModal { + confirm_question: String, + confirm_text: String, + confirm_action: String, + decline_text: String, + decline_action: String, +} + +impl ConfirmModal { + pub fn new(confirm_question: &str) -> ConfirmModal { + ConfirmModal { + confirm_question: confirm_question.to_owned(), + confirm_text: "Yes".to_owned(), + confirm_action: "".to_owned(), + decline_text: "No".to_owned(), + decline_action: "".to_owned(), + } + } + + pub fn confirm_action(mut self, confirm_action: &str) -> ConfirmModal { + self.confirm_action = confirm_action.to_owned(); + self + } +} + +component!(ConfirmModal); diff --git a/pgml-dashboard/src/components/confirm_modal/template.html b/pgml-dashboard/src/components/confirm_modal/template.html new file mode 100644 index 000000000..e38618fc8 --- /dev/null +++ b/pgml-dashboard/src/components/confirm_modal/template.html @@ -0,0 +1,10 @@ + +

<%= confirm_question %>

+
+ + +
diff --git a/pgml-dashboard/src/components/github_icon/mod.rs b/pgml-dashboard/src/components/github_icon/mod.rs new file mode 100644 index 000000000..d3dfe5b17 --- /dev/null +++ b/pgml-dashboard/src/components/github_icon/mod.rs @@ -0,0 +1,16 @@ +use crate::components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "github_icon/template.html")] +pub struct GithubIcon { + pub show_stars: bool, +} + +impl GithubIcon { + pub fn new() -> GithubIcon { + GithubIcon::default() + } +} + +component!(GithubIcon); diff --git a/pgml-dashboard/templates/components/github_icon.html b/pgml-dashboard/src/components/github_icon/template.html similarity index 100% rename from pgml-dashboard/templates/components/github_icon.html rename to pgml-dashboard/src/components/github_icon/template.html diff --git a/pgml-dashboard/src/components/left_nav_menu/left_nav_menu.scss b/pgml-dashboard/src/components/left_nav_menu/left_nav_menu.scss new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/src/components/left_nav_menu/mod.rs b/pgml-dashboard/src/components/left_nav_menu/mod.rs new file mode 100644 index 000000000..ef1d86c5a --- /dev/null +++ b/pgml-dashboard/src/components/left_nav_menu/mod.rs @@ -0,0 +1,17 @@ +use crate::components::component; +use crate::components::StaticNav; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "left_nav_menu/template.html")] +pub struct LeftNavMenu { + pub nav: StaticNav, +} + +impl LeftNavMenu { + pub fn new(nav: StaticNav) -> LeftNavMenu { + LeftNavMenu { nav } + } +} + +component!(LeftNavMenu); diff --git a/pgml-dashboard/templates/components/left_nav_menu.html b/pgml-dashboard/src/components/left_nav_menu/template.html similarity index 100% rename from pgml-dashboard/templates/components/left_nav_menu.html rename to pgml-dashboard/src/components/left_nav_menu/template.html diff --git a/pgml-dashboard/src/components/left_nav_web_app/left_nav_web_app.scss b/pgml-dashboard/src/components/left_nav_web_app/left_nav_web_app.scss new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/src/components/left_nav_web_app/mod.rs b/pgml-dashboard/src/components/left_nav_web_app/mod.rs new file mode 100644 index 000000000..663761696 --- /dev/null +++ b/pgml-dashboard/src/components/left_nav_web_app/mod.rs @@ -0,0 +1,26 @@ +use crate::components::component; +use sailfish::TemplateOnce; + +use crate::components::StaticNav; + +#[derive(TemplateOnce)] +#[template(path = "left_nav_web_app/template.html")] +pub struct LeftNavWebApp { + pub upper_nav: StaticNav, + pub lower_nav: StaticNav, + pub dropdown_nav: StaticNav, +} + +impl LeftNavWebApp { + pub fn render(upper_nav: StaticNav, lower_nav: StaticNav, dropdown_nav: StaticNav) -> String { + LeftNavWebApp { + upper_nav, + lower_nav, + dropdown_nav, + } + .render_once() + .unwrap() + } +} + +component!(LeftNavWebApp); diff --git a/pgml-dashboard/src/components/left_nav_web_app/template.html b/pgml-dashboard/src/components/left_nav_web_app/template.html new file mode 100644 index 000000000..c2713ba37 --- /dev/null +++ b/pgml-dashboard/src/components/left_nav_web_app/template.html @@ -0,0 +1,40 @@ +<% use crate::components::LeftNavMenu; %> + diff --git a/pgml-dashboard/src/components/mod.rs b/pgml-dashboard/src/components/mod.rs new file mode 100644 index 000000000..1c5737be0 --- /dev/null +++ b/pgml-dashboard/src/components/mod.rs @@ -0,0 +1,32 @@ +// This file is automatically generated by cargo-pgml-components. +// Do not modify it directly. +mod component; +pub(crate) use component::{component, Component}; +pub mod navbar_web_app; +pub use navbar_web_app::NavbarWebApp; +pub mod navbar; +pub use navbar::Navbar; +pub mod postgres_logo; +pub use postgres_logo::PostgresLogo; +pub mod static_nav_link; +pub use static_nav_link::StaticNavLink; +pub mod modal; +pub use modal::Modal; +pub mod static_nav; +pub use static_nav::StaticNav; +pub mod test_component; +pub use test_component::TestComponent; +pub mod nav; +pub use nav::Nav; +pub mod left_nav_web_app; +pub use left_nav_web_app::LeftNavWebApp; +pub mod github_icon; +pub use github_icon::GithubIcon; +pub mod confirm_modal; +pub use confirm_modal::ConfirmModal; +pub mod left_nav_menu; +pub use left_nav_menu::LeftNavMenu; +pub mod nav_link; +pub use nav_link::NavLink; +pub mod breadcrumbs; +pub use breadcrumbs::Breadcrumbs; diff --git a/pgml-dashboard/src/components/modal/mod.rs b/pgml-dashboard/src/components/modal/mod.rs new file mode 100644 index 000000000..67167cd3e --- /dev/null +++ b/pgml-dashboard/src/components/modal/mod.rs @@ -0,0 +1,61 @@ +use crate::components::{component, Component}; +use sailfish::TemplateOnce; + +/// A component that renders a Bootstrap modal. +#[derive(TemplateOnce, Default)] +#[template(path = "modal/template.html")] +pub struct Modal { + pub id: String, + pub size_class: String, + pub header: Option, + pub body: Component, +} + +component!(Modal); + +impl Modal { + /// Create a new x-large modal with the given body. + pub fn new(body: Component) -> Self { + let modal = Modal::default(); + let id = format!("modal-{}", crate::utils::random_string(10)); + + modal.id(&id).body(body).xlarge() + } + + /// Set the modal's id. + pub fn id(mut self, id: &str) -> Modal { + self.id = id.into(); + self + } + + /// Set the modal's body. + pub fn body(mut self, body: Component) -> Modal { + self.body = body; + self + } + + /// Make the modal x-large. + pub fn xlarge(mut self) -> Modal { + self.size_class = "modal-xl".into(); + self + } + + /// Set the modal's header. + pub fn header(mut self, header: Component) -> Modal { + self.header = Some(header); + self + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_modal_with_string() { + let modal = Modal::new("some random string".into()); + let rendering = modal.render_once().unwrap(); + + assert!(rendering.contains("some random string")); + } +} diff --git a/pgml-dashboard/src/components/modal/modal.scss b/pgml-dashboard/src/components/modal/modal.scss new file mode 100644 index 000000000..c16ad5064 --- /dev/null +++ b/pgml-dashboard/src/components/modal/modal.scss @@ -0,0 +1,32 @@ +.modal { + --bs-modal-margin: 1.65rem; + --bs-modal-header-padding: 0; + --bs-modal-width: 75vw; + + @include media-breakpoint-up(lg) { + --bs-modal-width: 40rem; + } + + .input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + border-radius: 0rem 2rem 2rem 0rem; + } + + .input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), .input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), .input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, .input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { + border-radius: 2rem 0rem 0rem 2rem; + } + + .modal-content { + box-shadow: none; + background-color: transparent; + border: none; + } + + .modal-header { + border: none; + } + + .input-group { + width: 100%; + display: flex; + } +} diff --git a/pgml-dashboard/src/components/modal/modal_controller.js b/pgml-dashboard/src/components/modal/modal_controller.js new file mode 100644 index 000000000..5c411dbd8 --- /dev/null +++ b/pgml-dashboard/src/components/modal/modal_controller.js @@ -0,0 +1,19 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static targets = [ + 'modal', + ]; + + connect() { + this.modal = new bootstrap.Modal(this.modalTarget) + } + + show() { + this.modal.show() + } + + hide() { + this.modal.hide() + } +} diff --git a/pgml-dashboard/src/components/modal/template.html b/pgml-dashboard/src/components/modal/template.html new file mode 100644 index 000000000..9d40e6e39 --- /dev/null +++ b/pgml-dashboard/src/components/modal/template.html @@ -0,0 +1,16 @@ + diff --git a/pgml-dashboard/src/components/nav/mod.rs b/pgml-dashboard/src/components/nav/mod.rs new file mode 100644 index 000000000..a95374dfa --- /dev/null +++ b/pgml-dashboard/src/components/nav/mod.rs @@ -0,0 +1,22 @@ +use crate::components::component; +use crate::components::nav_link::NavLink; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Clone, Default, Debug)] +#[template(path = "nav/template.html")] +pub struct Nav<'a> { + pub links: Vec>, +} + +impl<'a> Nav<'a> { + pub fn render(links: Vec>) -> String { + Nav { links }.render_once().unwrap() + } + + pub fn add_link(&mut self, link: NavLink<'a>) -> &mut Self { + self.links.push(link); + self + } +} + +component!(Nav, 'a); diff --git a/pgml-dashboard/templates/components/nav.html b/pgml-dashboard/src/components/nav/template.html similarity index 100% rename from pgml-dashboard/templates/components/nav.html rename to pgml-dashboard/src/components/nav/template.html diff --git a/pgml-dashboard/src/components/nav_link/mod.rs b/pgml-dashboard/src/components/nav_link/mod.rs new file mode 100644 index 000000000..71c5f7d7b --- /dev/null +++ b/pgml-dashboard/src/components/nav_link/mod.rs @@ -0,0 +1,46 @@ +use crate::components::nav::Nav; + +#[derive(Clone, Debug)] +pub struct NavLink<'a> { + pub href: String, + pub name: String, + pub target_blank: bool, + pub active: bool, + pub nav: Option>, + pub icon: Option<&'a str>, + pub disabled: bool, +} + +impl<'a> NavLink<'a> { + pub fn new(name: &str, href: &str) -> NavLink<'a> { + NavLink { + name: name.to_owned(), + href: href.to_owned(), + target_blank: false, + active: false, + nav: None, + icon: None, + disabled: false, + } + } + + pub fn active(mut self) -> NavLink<'a> { + self.active = true; + self + } + + pub fn disable(mut self, disabled: bool) -> NavLink<'a> { + self.disabled = disabled; + self + } + + pub fn nav(mut self, nav: Nav<'a>) -> NavLink<'a> { + self.nav = Some(nav); + self + } + + pub fn icon(mut self, icon: &'a str) -> NavLink<'a> { + self.icon = Some(icon); + self + } +} diff --git a/pgml-dashboard/src/components/navbar/mod.rs b/pgml-dashboard/src/components/navbar/mod.rs new file mode 100644 index 000000000..4dc023e34 --- /dev/null +++ b/pgml-dashboard/src/components/navbar/mod.rs @@ -0,0 +1,24 @@ +use crate::components::component; +use crate::models; +use crate::utils::config; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "layout/nav/top.html")] +pub struct Navbar { + pub current_user: Option, + pub standalone_dashboard: bool, +} + +impl Navbar { + pub fn render(user: Option) -> String { + Navbar { + current_user: user, + standalone_dashboard: config::standalone_dashboard(), + } + .render_once() + .unwrap() + } +} + +component!(Navbar); diff --git a/pgml-dashboard/src/components/navbar/navbar.scss b/pgml-dashboard/src/components/navbar/navbar.scss new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/src/components/navbar/template.html b/pgml-dashboard/src/components/navbar/template.html new file mode 100644 index 000000000..e4d1362d7 --- /dev/null +++ b/pgml-dashboard/src/components/navbar/template.html @@ -0,0 +1,72 @@ +<% use crate::templates::components::GithubIcon; %> +<% use crate::templates::components::PostgresLogo; %> + +
+ +
+ + <% include!("../../../templates/components/search_modal.html");%> diff --git a/pgml-dashboard/src/components/navbar_web_app/mod.rs b/pgml-dashboard/src/components/navbar_web_app/mod.rs new file mode 100644 index 000000000..e814fc15d --- /dev/null +++ b/pgml-dashboard/src/components/navbar_web_app/mod.rs @@ -0,0 +1,26 @@ +use crate::components::component; +use crate::components::{StaticNav, StaticNavLink}; +use crate::utils::config; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce)] +#[template(path = "navbar_web_app/template.html")] +pub struct NavbarWebApp { + pub standalone_dashboard: bool, + pub links: Vec, + pub account_management_nav: StaticNav, +} + +impl NavbarWebApp { + pub fn render(links: Vec, account_management_nav: StaticNav) -> String { + NavbarWebApp { + standalone_dashboard: config::standalone_dashboard(), + links, + account_management_nav, + } + .render_once() + .unwrap() + } +} + +component!(NavbarWebApp); diff --git a/pgml-dashboard/src/components/navbar_web_app/template.html b/pgml-dashboard/src/components/navbar_web_app/template.html new file mode 100644 index 000000000..070cf9730 --- /dev/null +++ b/pgml-dashboard/src/components/navbar_web_app/template.html @@ -0,0 +1,161 @@ +<% use crate::templates::components::GithubIcon; %> +<% use crate::templates::components::PostgresLogo; %> + +
+ +
+ + <% include!("../../../templates/components/search_modal.html");%> diff --git a/pgml-dashboard/src/components/postgres_logo/mod.rs b/pgml-dashboard/src/components/postgres_logo/mod.rs new file mode 100644 index 000000000..ee525c8a2 --- /dev/null +++ b/pgml-dashboard/src/components/postgres_logo/mod.rs @@ -0,0 +1,18 @@ +use crate::components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "postgres_logo/template.html")] +pub struct PostgresLogo { + link: String, +} + +impl PostgresLogo { + pub fn new(link: &str) -> PostgresLogo { + PostgresLogo { + link: link.to_owned(), + } + } +} + +component!(PostgresLogo); diff --git a/pgml-dashboard/src/components/postgres_logo/postgres_logo.scss b/pgml-dashboard/src/components/postgres_logo/postgres_logo.scss new file mode 100644 index 000000000..132c90b98 --- /dev/null +++ b/pgml-dashboard/src/components/postgres_logo/postgres_logo.scss @@ -0,0 +1,6 @@ +.postgres-logo { + display: flex; + align-items: center; + gap: calc($spacer / 2); + font-size: 24px; +} diff --git a/pgml-dashboard/templates/components/postgres_logo.html b/pgml-dashboard/src/components/postgres_logo/template.html similarity index 100% rename from pgml-dashboard/templates/components/postgres_logo.html rename to pgml-dashboard/src/components/postgres_logo/template.html diff --git a/pgml-dashboard/src/components/static_nav/mod.rs b/pgml-dashboard/src/components/static_nav/mod.rs new file mode 100644 index 000000000..54ee2c669 --- /dev/null +++ b/pgml-dashboard/src/components/static_nav/mod.rs @@ -0,0 +1,19 @@ +use crate::components::StaticNavLink; + +#[derive(Debug, Clone, Default)] +pub struct StaticNav { + pub links: Vec, +} + +impl StaticNav { + pub fn add_link(&mut self, link: StaticNavLink) { + self.links.push(link); + } + + pub fn get_active(self) -> StaticNavLink { + match self.links.iter().find(|item| item.active) { + Some(item) => item.clone(), + None => StaticNavLink::default(), + } + } +} diff --git a/pgml-dashboard/src/components/static_nav/static_nav.scss b/pgml-dashboard/src/components/static_nav/static_nav.scss new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/src/components/static_nav/template.html b/pgml-dashboard/src/components/static_nav/template.html new file mode 100644 index 000000000..26f720323 --- /dev/null +++ b/pgml-dashboard/src/components/static_nav/template.html @@ -0,0 +1,3 @@ +
+ <%= value %> +
diff --git a/pgml-dashboard/src/components/static_nav_link/mod.rs b/pgml-dashboard/src/components/static_nav_link/mod.rs new file mode 100644 index 000000000..7de950cdd --- /dev/null +++ b/pgml-dashboard/src/components/static_nav_link/mod.rs @@ -0,0 +1,42 @@ +#[derive(Debug, Clone, Default)] +pub struct StaticNavLink { + pub name: String, + pub href: String, + pub active: bool, + pub disabled: bool, + pub icon: Option, + pub hide_for_lg_screens: bool, +} + +impl StaticNavLink { + pub fn new(name: String, href: String) -> StaticNavLink { + StaticNavLink { + name, + href, + active: false, + disabled: false, + icon: None, + hide_for_lg_screens: false, + } + } + + pub fn active(mut self, active: bool) -> Self { + self.active = active; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn icon(mut self, icon: &str) -> Self { + self.icon = Some(icon.to_string()); + self + } + + pub fn hide_for_lg_screens(mut self, hide: bool) -> Self { + self.hide_for_lg_screens = hide; + self + } +} diff --git a/pgml-dashboard/src/components/test_component/mod.rs b/pgml-dashboard/src/components/test_component/mod.rs new file mode 100644 index 000000000..3b29ed573 --- /dev/null +++ b/pgml-dashboard/src/components/test_component/mod.rs @@ -0,0 +1,16 @@ +use crate::components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "test_component/template.html")] +pub struct TestComponent { + value: String, +} + +impl TestComponent { + pub fn new() -> TestComponent { + TestComponent::default() + } +} + +component!(TestComponent); diff --git a/pgml-dashboard/src/components/test_component/template.html b/pgml-dashboard/src/components/test_component/template.html new file mode 100644 index 000000000..c46dc82dd --- /dev/null +++ b/pgml-dashboard/src/components/test_component/template.html @@ -0,0 +1,3 @@ +
+ <%= value %> +
diff --git a/pgml-dashboard/src/lib.rs b/pgml-dashboard/src/lib.rs index 96e15468a..18900f1e7 100644 --- a/pgml-dashboard/src/lib.rs +++ b/pgml-dashboard/src/lib.rs @@ -10,6 +10,7 @@ use sqlx::PgPool; use std::collections::HashMap; pub mod api; +pub mod components; pub mod fairings; pub mod forms; pub mod guards; @@ -658,6 +659,12 @@ pub async fn dashboard( )) } +#[get("/playground")] +pub async fn playground(cluster: &Cluster) -> Result { + let mut layout = crate::templates::WebAppBase::new("Playground", &cluster.context); + Ok(ResponseOk(layout.render(templates::Playground {}))) +} + pub fn routes() -> Vec { routes![ notebook_index, diff --git a/pgml-dashboard/src/main.rs b/pgml-dashboard/src/main.rs index 7cbbf69d1..e26f837b3 100644 --- a/pgml-dashboard/src/main.rs +++ b/pgml-dashboard/src/main.rs @@ -1,4 +1,5 @@ use log::{error, info, warn}; + use rocket::{ catch, catchers, fs::FileServer, get, http::Status, request::Request, response::Redirect, }; @@ -99,26 +100,6 @@ async fn main() { // it's important to hang on to sentry so it isn't dropped and stops reporting let _sentry = configure_reporting().await; - if config::dev_mode() { - warn!("============================================"); - warn!("PostgresML is set to run in development mode"); - warn!("============================================"); - - let status = tokio::process::Command::new("npm") - .arg("exec") - .arg("sass") - .arg("static/css/bootstrap-theme.scss") - .arg("static/css/style.css") - .status() - .await - .unwrap(); - - if !status.success() { - error!("SCSS compilation failed. Do you have `node`, `npm`, and `sass` installed and working globally?"); - std::process::exit(1); - } - } - markdown::SearchIndex::build().await.unwrap(); pgml_dashboard::migrate(&guards::Cluster::default().pool()) @@ -131,6 +112,7 @@ async fn main() { .mount("/dashboard/static", FileServer::from(&config::static_dir())) .mount("/dashboard", pgml_dashboard::routes()) .mount("/", pgml_dashboard::api::docs::routes()) + .mount("/", rocket::routes![pgml_dashboard::playground]) .register( "/", catchers![error_catcher, not_authorized_catcher, not_found_handler], diff --git a/pgml-dashboard/src/templates/components.rs b/pgml-dashboard/src/templates/components.rs deleted file mode 100644 index 42449f11c..000000000 --- a/pgml-dashboard/src/templates/components.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::templates::models; -use crate::utils::config; -use sailfish::TemplateOnce; - -#[derive(TemplateOnce)] -#[template(path = "components/box.html")] -pub struct Box<'a> { - name: &'a str, - value: String, -} - -impl<'a> Box<'a> { - pub fn new(name: &'a str, value: &str) -> Box<'a> { - Box { - name, - value: value.to_owned(), - } - } -} - -#[derive(Clone, Debug)] -pub struct NavLink<'a> { - pub href: String, - pub name: String, - pub target_blank: bool, - pub active: bool, - pub nav: Option>, - pub icon: Option<&'a str>, - pub disabled: bool, -} - -impl<'a> NavLink<'a> { - pub fn new(name: &str, href: &str) -> NavLink<'a> { - NavLink { - name: name.to_owned(), - href: href.to_owned(), - target_blank: false, - active: false, - nav: None, - icon: None, - disabled: false, - } - } - - pub fn active(mut self) -> NavLink<'a> { - self.active = true; - self - } - - pub fn disable(mut self, disabled: bool) -> NavLink<'a> { - self.disabled = disabled; - self - } - - pub fn nav(mut self, nav: Nav<'a>) -> NavLink<'a> { - self.nav = Some(nav); - self - } - - pub fn icon(mut self, icon: &'a str) -> NavLink<'a> { - self.icon = Some(icon); - self - } -} - -#[derive(TemplateOnce, Clone, Default, Debug)] -#[template(path = "components/nav.html")] -pub struct Nav<'a> { - pub links: Vec>, -} - -impl<'a> Nav<'a> { - pub fn render(links: Vec>) -> String { - Nav { links }.render_once().unwrap() - } - - pub fn add_link(&mut self, link: NavLink<'a>) -> &mut Self { - self.links.push(link); - self - } -} - -#[derive(TemplateOnce)] -#[template(path = "layout/nav/left_web_app.html")] -pub struct LeftNavWebApp { - pub upper_nav: StaticNav, - pub lower_nav: StaticNav, - pub dropdown_nav: StaticNav, -} - -impl LeftNavWebApp { - pub fn render(upper_nav: StaticNav, lower_nav: StaticNav, dropdown_nav: StaticNav) -> String { - LeftNavWebApp { - upper_nav, - lower_nav, - dropdown_nav, - } - .render_once() - .unwrap() - } -} - -#[derive(TemplateOnce)] -#[template(path = "components/breadcrumbs.html")] -pub struct Breadcrumbs<'a> { - pub links: Vec>, -} - -impl<'a> Breadcrumbs<'a> { - pub fn render(links: Vec>) -> String { - Breadcrumbs { links }.render_once().unwrap() - } -} - -#[derive(TemplateOnce)] -#[template(path = "components/boxes.html")] -pub struct Boxes<'a> { - pub boxes: Vec>, -} - -#[derive(TemplateOnce)] -#[template(path = "layout/nav/top.html")] -pub struct Navbar { - pub current_user: Option, - pub standalone_dashboard: bool, -} - -impl Navbar { - pub fn render(user: Option) -> String { - Navbar { - current_user: user, - standalone_dashboard: config::standalone_dashboard(), - } - .render_once() - .unwrap() - } -} - -#[derive(TemplateOnce)] -#[template(path = "layout/nav/top_web_app.html")] -pub struct NavbarWebApp { - pub standalone_dashboard: bool, - pub links: Vec, - pub account_management_nav: StaticNav, -} - -impl NavbarWebApp { - pub fn render(links: Vec, account_management_nav: StaticNav) -> String { - NavbarWebApp { - standalone_dashboard: config::standalone_dashboard(), - links, - account_management_nav, - } - .render_once() - .unwrap() - } -} - -#[derive(TemplateOnce)] -#[template(path = "components/github_icon.html")] -pub struct GithubIcon { - pub show_stars: bool, -} - -#[derive(TemplateOnce)] -#[template(path = "components/postgres_logo.html")] -pub struct PostgresLogo { - link: String, -} - -#[derive(Debug, Clone, Default)] -pub struct StaticNav { - pub links: Vec, -} - -impl StaticNav { - pub fn add_link(&mut self, link: StaticNavLink) { - self.links.push(link); - } - - pub fn get_active(self) -> StaticNavLink { - match self.links.iter().find(|item| item.active) { - Some(item) => item.clone(), - None => StaticNavLink { - ..Default::default() - }, - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct StaticNavLink { - pub name: String, - pub href: String, - pub active: bool, - pub disabled: bool, - pub icon: Option, - pub hide_for_lg_screens: bool, -} - -impl StaticNavLink { - pub fn new(name: String, href: String) -> StaticNavLink { - StaticNavLink { - name, - href, - active: false, - disabled: false, - icon: None, - hide_for_lg_screens: false, - } - } - - pub fn active(mut self, active: bool) -> Self { - self.active = active; - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - pub fn icon(mut self, icon: &str) -> Self { - self.icon = Some(icon.to_string()); - self - } - - pub fn hide_for_lg_screens(mut self, hide: bool) -> Self { - self.hide_for_lg_screens = hide; - self - } -} - -#[derive(TemplateOnce)] -#[template(path = "components/left_nav_menu.html")] -pub struct LeftNavMenu { - pub nav: StaticNav, -} diff --git a/pgml-dashboard/src/templates/mod.rs b/pgml-dashboard/src/templates/mod.rs index 032db2d96..b1bb25fb7 100644 --- a/pgml-dashboard/src/templates/mod.rs +++ b/pgml-dashboard/src/templates/mod.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use components::{NavLink, StaticNav, StaticNavLink}; +pub use crate::components::{self, NavLink, StaticNav, StaticNavLink}; use sailfish::TemplateOnce; use sqlx::postgres::types::PgMoney; @@ -10,7 +10,6 @@ use sqlx::{Column, Executor, PgPool, Row, Statement, TypeInfo, ValueRef}; use crate::models; use crate::utils::tabs; -pub mod components; pub mod docs; pub mod head; @@ -503,3 +502,7 @@ pub struct SnapshotTab { pub struct UploaderTab { pub table_name: Option, } + +#[derive(TemplateOnce)] +#[template(path = "content/playground.html")] +pub struct Playground; diff --git a/pgml-dashboard/src/utils/config.rs b/pgml-dashboard/src/utils/config.rs index 6a25e14e2..56dc30e48 100644 --- a/pgml-dashboard/src/utils/config.rs +++ b/pgml-dashboard/src/utils/config.rs @@ -63,10 +63,6 @@ pub fn deployment() -> String { } pub fn css_url() -> String { - if dev_mode() { - return "/dashboard/static/css/style.css".to_string(); - } - let filename = format!("style.{}.css", env!("CSS_VERSION")); let path = format!("/dashboard/static/css/{filename}"); @@ -78,13 +74,9 @@ pub fn css_url() -> String { } pub fn js_url(name: &str) -> String { - let name = if dev_mode() { - name.to_string() - } else { - let name = name.split(".").collect::>(); - let name = name[0..name.len() - 1].join("."); - format!("{name}.{}.js", env!("JS_VERSION")) - }; + let name = name.split(".").collect::>(); + let name = name[0..name.len() - 1].join("."); + let name = format!("{name}.{}.js", env!("JS_VERSION")); let path = format!("/dashboard/static/js/{name}"); diff --git a/pgml-dashboard/static/css/.gitignore b/pgml-dashboard/static/css/.gitignore index 9f2fa7c8e..22e3489d3 100644 --- a/pgml-dashboard/static/css/.gitignore +++ b/pgml-dashboard/static/css/.gitignore @@ -1,3 +1,4 @@ style.css.map style.*.css style.css +.pgml-bundle diff --git a/pgml-dashboard/static/css/.ignore b/pgml-dashboard/static/css/.ignore index b3a526711..9a4be7bc3 100644 --- a/pgml-dashboard/static/css/.ignore +++ b/pgml-dashboard/static/css/.ignore @@ -1 +1,2 @@ *.css +modules.scss diff --git a/pgml-dashboard/static/css/bootstrap-theme.scss b/pgml-dashboard/static/css/bootstrap-theme.scss index d73195381..fa1426ddc 100644 --- a/pgml-dashboard/static/css/bootstrap-theme.scss +++ b/pgml-dashboard/static/css/bootstrap-theme.scss @@ -74,7 +74,6 @@ @import 'scss/components/badges'; @import 'scss/components/buttons'; @import 'scss/components/cards'; -@import 'scss/components/modals'; @import 'scss/components/tooltips'; @import 'scss/components/alerts'; @import 'scss/components/images'; @@ -88,3 +87,6 @@ @import 'scss/base/base'; @import 'scss/base/animations'; @import 'scss/base/typography'; + +// Automatically generated by the builder +@import 'modules.scss'; diff --git a/pgml-dashboard/static/css/modules.scss b/pgml-dashboard/static/css/modules.scss new file mode 100644 index 000000000..e15d16010 --- /dev/null +++ b/pgml-dashboard/static/css/modules.scss @@ -0,0 +1,6 @@ +@import "../../src/components/left_nav_menu/left_nav_menu.scss"; +@import "../../src/components/left_nav_web_app/left_nav_web_app.scss"; +@import "../../src/components/modal/modal.scss"; +@import "../../src/components/navbar/navbar.scss"; +@import "../../src/components/postgres_logo/postgres_logo.scss"; +@import "../../src/components/static_nav/static_nav.scss"; diff --git a/pgml-dashboard/static/css/scss/components/_buttons.scss b/pgml-dashboard/static/css/scss/components/_buttons.scss index f9f6e9947..0dd97c365 100644 --- a/pgml-dashboard/static/css/scss/components/_buttons.scss +++ b/pgml-dashboard/static/css/scss/components/_buttons.scss @@ -244,13 +244,6 @@ font-weight: $font-weight-medium; } -.postgres-logo { - display: flex; - align-items: center; - gap: calc($spacer / 2); - font-size: 24px; -} - .btn-dropdown { @extend .btn; border-radius: $border-radius; diff --git a/pgml-dashboard/static/js/.gitignore b/pgml-dashboard/static/js/.gitignore index b3c44d549..cda6269f1 100644 --- a/pgml-dashboard/static/js/.gitignore +++ b/pgml-dashboard/static/js/.gitignore @@ -1 +1,4 @@ /*.*.js +modules.js +bundle.js +.pgml-bundle diff --git a/pgml-dashboard/static/js/notebook.js b/pgml-dashboard/static/js/notebook.js index 8e400e3e3..cf2d58d89 100644 --- a/pgml-dashboard/static/js/notebook.js +++ b/pgml-dashboard/static/js/notebook.js @@ -7,12 +7,13 @@ export default class extends Controller { 'cellButton', 'stopButton', 'playAllButton', - 'deleteModal', 'newCell', 'syntaxName', 'playButtonText', ]; + static outlets = ['modal']; + cellCheckIntervalMillis = 500 connect() { @@ -21,7 +22,7 @@ export default class extends Controller { const innerHeight = window.innerHeight this.scrollerTarget.style.maxHeight = `${innerHeight - rect.top - 10}px` - this.confirmDeleteModal = new bootstrap.Modal(this.deleteModalTarget) + // this.confirmDeleteModal = new bootstrap.Modal(this.deleteModalTarget) this.sortable = Sortable.create(this.scrollerTarget, { onUpdate: this.updateCellOrder.bind(this), @@ -202,7 +203,7 @@ export default class extends Controller { } deleteCellConfirm() { - this.confirmDeleteModal.show() + this.modalOutlet.show() } deleteCell() { diff --git a/pgml-dashboard/templates/components/box.html b/pgml-dashboard/templates/components/box.html deleted file mode 100644 index 761779585..000000000 --- a/pgml-dashboard/templates/components/box.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
-
<%= name %>
-

<%- value %>

-
-
-
diff --git a/pgml-dashboard/templates/components/boxes.html b/pgml-dashboard/templates/components/boxes.html deleted file mode 100644 index eec37dc18..000000000 --- a/pgml-dashboard/templates/components/boxes.html +++ /dev/null @@ -1,5 +0,0 @@ -
- <% for b in boxes { %> - <%- b.render_once().unwrap() %> - <% } %> -
diff --git a/pgml-dashboard/templates/components/component.html b/pgml-dashboard/templates/components/component.html new file mode 100644 index 000000000..d4c8df92e --- /dev/null +++ b/pgml-dashboard/templates/components/component.html @@ -0,0 +1 @@ +<%- value %> diff --git a/pgml-dashboard/templates/content/dashboard/panels/notebook.html b/pgml-dashboard/templates/content/dashboard/panels/notebook.html index 75e98eeeb..4bb6ee256 100644 --- a/pgml-dashboard/templates/content/dashboard/panels/notebook.html +++ b/pgml-dashboard/templates/content/dashboard/panels/notebook.html @@ -1,5 +1,15 @@ +<% use crate::templates::components::{ConfirmModal, Modal}; + +let modal = Modal::new( + ConfirmModal::new( + "Are you sure you want to delete this cell?" + ).confirm_action("notebook#deleteCell").into() +); + + +%> -
+
@@ -70,6 +80,8 @@
+ +
<% for cell in cells { @@ -79,26 +91,9 @@ include!("cell.html"); } %>
- + <%+ modal %> +
diff --git a/pgml-dashboard/templates/content/dashboard/panels/notebook_modal.html b/pgml-dashboard/templates/content/dashboard/panels/notebook_modal.html new file mode 100644 index 000000000..e69de29bb diff --git a/pgml-dashboard/templates/content/playground.html b/pgml-dashboard/templates/content/playground.html new file mode 100644 index 000000000..792f7621d --- /dev/null +++ b/pgml-dashboard/templates/content/playground.html @@ -0,0 +1 @@ +

Playground

diff --git a/pgml-dashboard/templates/layout/head.html b/pgml-dashboard/templates/layout/head.html index 88ae09e10..2dd0f2cf7 100644 --- a/pgml-dashboard/templates/layout/head.html +++ b/pgml-dashboard/templates/layout/head.html @@ -70,44 +70,8 @@ } } - - import ClickReplace from '<%= config::js_url("click-replace.js") %>' - import Search from '<%= config::js_url("search.js") %>' - import BtnSecondary from '<%= config::js_url("btn-secondary.js") %>' - import AutoreloadFrame from '<%= config::js_url("autoreload-frame.js") %>' - import XScrollerDrag from '<%= config::js_url("x-scroller-drag.js") %>' - import DocsToc from '<%= config::js_url("docs-toc.js") %>' - import Timeseries from '<%= config::js_url("timeseries.js") %>' - import EnableTooltip from '<%= config::js_url("enable-tooltip.js") %>' - import Copy from '<%= config::js_url("copy.js") %>' - import NewProject from '<%= config::js_url("new-project.js") %>' - import Notebook from '<%= config::js_url("notebook.js") %>' - import NotebookCell from '<%= config::js_url("notebook-cell.js") %>' - import QuickPrediction from '<%= config::js_url("quick-prediction.js") %>' - import TopnavStyling from '<%= config::js_url("topnav-styling.js") %>' - import TopnavWebApp from '<%= config::js_url("topnav-web-app.js") %>' - import ExtendBSCollapse from '<%= config::js_url("extend-bs-collapse.js") %>' - - const application = Application.start() - application.register('click-replace', ClickReplace) - application.register('search', Search) - application.register('btn-secondary', BtnSecondary) - application.register('autoreload-frame', AutoreloadFrame) - application.register('x-scroller-drag', XScrollerDrag) - application.register('docs-toc', DocsToc) - application.register('timeseries', Timeseries) - application.register('enable-tooltip', EnableTooltip) - application.register('copy', Copy) - application.register('new-project', NewProject) - application.register('notebook', Notebook) - application.register('notebook-cell', NotebookCell) - application.register('quick-prediction', QuickPrediction) - application.register('topnav-styling', TopnavStyling) - application.register('topnav-web-app', TopnavWebApp) - application.register('extend-bs-collapse', ExtendBSCollapse) - <% if config::dev_mode() { %> diff --git a/pgml-dashboard/templates/layout/nav/top.html b/pgml-dashboard/templates/layout/nav/top.html index 0d08b40d5..3e1970ab8 100644 --- a/pgml-dashboard/templates/layout/nav/top.html +++ b/pgml-dashboard/templates/layout/nav/top.html @@ -5,7 +5,7 @@