From 3ba1947c6542fc5b21cacb51fc354a4ad0d4673f Mon Sep 17 00:00:00 2001 From: George Leung Date: Tue, 23 Jan 2024 15:51:23 -0500 Subject: [PATCH] polishing CLI UX (#144) create-moose-app creates the project in a new directory link to console if data comes from curl shows help if command is missing spinners around tasks that take a while --- README.md | 4 +- apps/create-moose-app/src/index.ts | 6 +++ apps/igloo-kit-cli/Cargo.lock | 46 +++++++++++++++++++ apps/igloo-kit-cli/Cargo.toml | 1 + apps/igloo-kit-cli/src/cli.rs | 21 +++++---- apps/igloo-kit-cli/src/cli/display.rs | 15 +++++- apps/igloo-kit-cli/src/cli/local_webserver.rs | 27 +++++++++-- apps/igloo-kit-cli/src/cli/routines.rs | 8 +++- apps/igloo-kit-cli/src/cli/routines/start.rs | 26 +++++++---- apps/igloo-kit-cli/src/cli/routines/stop.rs | 16 +++++-- apps/igloo-kit-cli/src/cli/settings.rs | 21 +++++++-- apps/igloo-kit-cli/src/cli/watcher.rs | 1 + .../src/infrastructure/console.rs | 8 ++-- apps/igloo-kit-cli/src/utilities/constants.rs | 2 +- apps/igloo-kit-cli/src/utilities/docker.rs | 2 + 15 files changed, 167 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index a09eaa258..b08032051 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ $ npm install -g @514labs/moose-cli ## Config file -The config file is located in `~/.igloo-config.toml` +The config file is located in `~/.igloo/config.toml` You can create one with the following content @@ -36,7 +36,7 @@ You can create one with the following content # Coming soon wall on all the CLI commands as we build the MVP. # if you want to try features as they are built, set this to false -coming_soon_wall=true +coming_soon_wall=false ``` ## Versioning diff --git a/apps/create-moose-app/src/index.ts b/apps/create-moose-app/src/index.ts index 484ba54e4..4d6b4a5e5 100644 --- a/apps/create-moose-app/src/index.ts +++ b/apps/create-moose-app/src/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { spawnSync } from "child_process"; +import { mkdirSync } from "fs"; /** * Returns the executable path which is located inside `node_modules` @@ -36,8 +37,13 @@ function getExePath() { */ function run() { const args = process.argv.slice(2); + const name = args[0]; + if (name !== undefined) { + mkdirSync(name); + } const processResult = spawnSync(getExePath(), ["init"].concat(args), { stdio: "inherit", + cwd: name, }); process.exit(processResult.status ?? 0); } diff --git a/apps/igloo-kit-cli/Cargo.lock b/apps/igloo-kit-cli/Cargo.lock index c99ea750f..1238e1621 100644 --- a/apps/igloo-kit-cli/Cargo.lock +++ b/apps/igloo-kit-cli/Cargo.lock @@ -1036,6 +1036,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "spinners", "tinytemplate", "tokio", "toml", @@ -1239,6 +1240,12 @@ dependencies = [ "libc", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "match_cfg" version = "0.1.0" @@ -1848,6 +1855,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -2152,6 +2165,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spinners" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef947f358b9c238923f764c72a4a9d42f2d637c46e059dbd319d6e7cfb4f82" +dependencies = [ + "lazy_static", + "maplit", + "strum", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2164,6 +2188,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/apps/igloo-kit-cli/Cargo.toml b/apps/igloo-kit-cli/Cargo.toml index 9d8199ac2..9e777d0f5 100644 --- a/apps/igloo-kit-cli/Cargo.toml +++ b/apps/igloo-kit-cli/Cargo.toml @@ -37,6 +37,7 @@ hyper-util = { version = "0.1", features = ["full"] } http-body-util = "0.1" lazy_static = "1.4.0" anyhow = "1.0" +spinners = "4.1.1" [dev-dependencies] clickhouse = { version = "0.11.5", features = ["uuid", "test-util"] } diff --git a/apps/igloo-kit-cli/src/cli.rs b/apps/igloo-kit-cli/src/cli.rs index 5124464b2..b204a54c6 100644 --- a/apps/igloo-kit-cli/src/cli.rs +++ b/apps/igloo-kit-cli/src/cli.rs @@ -19,6 +19,7 @@ use self::{ RunMode, }, }; +use crate::cli::settings::init_config_file; use crate::project::Project; use clap::Parser; use commands::Commands; @@ -27,7 +28,7 @@ use settings::{read_settings, Settings}; use std::path::Path; #[derive(Parser)] -#[command(author, version, about, long_about = None)] +#[command(author, version, about, long_about = None, arg_required_else_help(true))] struct Cli { /// Optional name to operate on name: Option, @@ -42,17 +43,17 @@ struct Cli { debug: bool, #[command(subcommand)] - command: Option, + command: Commands, } -async fn top_command_handler(settings: Settings, commands: &Option) { +async fn top_command_handler(settings: Settings, commands: &Commands) { if !settings.features.coming_soon_wall { match commands { - Some(Commands::Init { + Commands::Init { name, language, location, - }) => { + } => { info!( "Running init command with name: {}, language: {}, location: {}", name, language, location @@ -74,7 +75,7 @@ async fn top_command_handler(settings: Settings, commands: &Option) { .write_to_file() .expect("Failed to write project to file"); } - Some(Commands::Dev {}) => { + Commands::Dev {} => { info!("Running dev command"); let project = Project::load_from_current_dir() @@ -91,17 +92,17 @@ async fn top_command_handler(settings: Settings, commands: &Option) { routines::start_development_mode(&project).await.unwrap(); } - Some(Commands::Update {}) => { + Commands::Update {} => { // This command may not be needed if we have incredible automation todo!("Will update the project's underlying infrastructure based on any added objects") } - Some(Commands::Stop {}) => { + Commands::Stop {} => { let mut controller = RoutineController::new(); let run_mode = RunMode::Explicit {}; controller.add_routine(Box::new(StopLocalInfrastructure::new(run_mode))); controller.run_routines(run_mode); } - Some(Commands::Clean {}) => { + Commands::Clean {} => { let run_mode = RunMode::Explicit {}; let project = Project::load_from_current_dir() .expect("No project found, please run `igloo init` to create a project"); @@ -110,7 +111,6 @@ async fn top_command_handler(settings: Settings, commands: &Option) { controller.add_routine(Box::new(CleanProject::new(project, run_mode))); controller.run_routines(run_mode); } - None => {} } } else { show_message!(MessageType::Banner, Message { @@ -122,6 +122,7 @@ async fn top_command_handler(settings: Settings, commands: &Option) { pub async fn cli_run() { setup_user_directory().expect("Failed to setup igloo user directory"); + init_config_file().unwrap(); let config = read_settings().unwrap(); setup_logging(config.logger.clone()).expect("Failed to setup logging"); diff --git a/apps/igloo-kit-cli/src/cli/display.rs b/apps/igloo-kit-cli/src/cli/display.rs index bdf4f651e..6dec4e1f9 100644 --- a/apps/igloo-kit-cli/src/cli/display.rs +++ b/apps/igloo-kit-cli/src/cli/display.rs @@ -1,5 +1,6 @@ use console::style; use lazy_static::lazy_static; +use spinners::{Spinner, Spinners}; use std::sync::{Arc, RwLock}; /// # Display Module @@ -14,7 +15,7 @@ use std::sync::{Arc, RwLock}; /// MessageType::Info, /// Message { /// action: "Loading Config".to_string(), -/// details: "Reading configuration from ~/.igloo-config.toml".to_string(), +/// details: "Reading configuration from ~/.igloo/config.toml".to_string(), /// }); /// ``` /// @@ -31,7 +32,7 @@ use std::sync::{Arc, RwLock}; /// ``` /// Message { /// action: "Loading Config".to_string(), -/// details: "Reading configuration from ~/.igloo-config.toml".to_string(), +/// details: "Reading configuration from ~/.igloo/config.toml".to_string(), /// } /// ``` /// @@ -176,3 +177,13 @@ macro_rules! show_message { }; }; } + +pub fn with_spinner(message: &str, f: F) -> R +where + F: FnOnce() -> R, +{ + let mut sp = Spinner::new(Spinners::Dots9, message.into()); + let res = f(); + sp.stop_with_newline(); + res +} diff --git a/apps/igloo-kit-cli/src/cli/local_webserver.rs b/apps/igloo-kit-cli/src/cli/local_webserver.rs index a416778ea..c50dbf1a0 100644 --- a/apps/igloo-kit-cli/src/cli/local_webserver.rs +++ b/apps/igloo-kit-cli/src/cli/local_webserver.rs @@ -9,6 +9,7 @@ use crate::framework::controller::RouteMeta; use crate::infrastructure::stream::redpanda; use crate::infrastructure::stream::redpanda::ConfiguredProducer; +use crate::infrastructure::console::ConsoleConfig; use crate::project::Project; use http_body_util::BodyExt; use http_body_util::Full; @@ -64,6 +65,7 @@ impl Default for LocalWebserverConfig { struct RouteService { route_table: Arc>>, configured_producer: Arc>, + console_config: ConsoleConfig, } impl Service> for RouteService { @@ -76,6 +78,7 @@ impl Service> for RouteService { req, self.route_table.clone(), self.configured_producer.clone(), + self.console_config.clone(), )) } } @@ -100,6 +103,7 @@ async fn ingest_route( route: PathBuf, configured_producer: Arc>, route_table: Arc>>, + console_config: ConsoleConfig, ) -> Result>, hyper::http::Error> { show_message!( MessageType::Info, @@ -110,6 +114,15 @@ async fn ingest_route( ); if route_table.lock().await.contains_key(&route) { + let is_curl = req.headers().get("User-Agent").map_or_else( + || false, + |user_agent| { + user_agent + .to_str() + .map_or_else(|_| false, |s| s.starts_with("curl")) + }, + ); + let body = req.collect().await.unwrap().to_bytes().to_vec(); let guard = route_table.lock().await; @@ -136,7 +149,12 @@ async fn ingest_route( details: route.to_str().unwrap().to_string(), } ); - Ok(Response::new(Full::new(Bytes::from("SUCCESS")))) + let response_bytes = if is_curl { + Bytes::from(format!("Success! Go to http://localhost:{}/infrastructure/views to view your data!", console_config.host_port)) + } else { + Bytes::from("SUCCESS") + }; + Ok(Response::new(Full::new(response_bytes))) } Err(e) => { println!("Error: {:?}", e); @@ -156,6 +174,7 @@ async fn router( req: Request, route_table: Arc>>, configured_producer: Arc>, + console_config: ConsoleConfig, ) -> Result>, hyper::http::Error> { debug!( "HTTP Request Received: {:?}, with Route Table {:?}", @@ -178,7 +197,7 @@ async fn router( match (req.method(), &route_split[..]) { (&hyper::Method::POST, ["ingest", _]) => { - ingest_route(req, route, configured_producer, route_table).await + ingest_route(req, route, configured_producer, route_table, console_config).await } (&hyper::Method::OPTIONS, _) => options_route(), @@ -255,6 +274,7 @@ impl Webserver { let route_table = route_table.clone(); let producer = producer.clone(); + let console_config = project.console_config.clone(); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { @@ -263,7 +283,8 @@ impl Webserver { io, RouteService { route_table, - configured_producer: producer + configured_producer: producer, + console_config, }, ).await { error!("server error: {}", e); diff --git a/apps/igloo-kit-cli/src/cli/routines.rs b/apps/igloo-kit-cli/src/cli/routines.rs index 31b2707ee..cffc57ca3 100644 --- a/apps/igloo-kit-cli/src/cli/routines.rs +++ b/apps/igloo-kit-cli/src/cli/routines.rs @@ -271,7 +271,13 @@ async fn initialize_project_state( .await; let route_table_clone = route_table.clone(); - let _ = post_current_state_to_console(&configured_client, &producer, route_table_clone).await; + let _ = post_current_state_to_console( + &configured_client, + &producer, + route_table_clone, + project.console_config.clone(), + ) + .await; match crawl_result { Ok(_) => { diff --git a/apps/igloo-kit-cli/src/cli/routines/start.rs b/apps/igloo-kit-cli/src/cli/routines/start.rs index 3e711eb40..aaa6bb697 100644 --- a/apps/igloo-kit-cli/src/cli/routines/start.rs +++ b/apps/igloo-kit-cli/src/cli/routines/start.rs @@ -7,6 +7,7 @@ use super::{ }, Routine, RoutineFailure, RoutineSuccess, RunMode, }; +use crate::cli::display::with_spinner; use crate::cli::routines::initialize::CreateIglooTempDirectoryTree; use crate::cli::routines::util::ensure_docker_running; use crate::utilities::constants::CLI_PROJECT_INTERNAL_DIR; @@ -77,7 +78,10 @@ impl Routine for RunRedPandaContainer { .internal_dir() .map_err(|err| RoutineFailure::new(FAILED_TO_CREATE_INTERNAL_DIR.clone(), err))?; - let output = docker::safe_start_redpanda_container(igloo_dir).map_err(|err| { + let output = with_spinner("Starting redpanda container", || { + docker::safe_start_redpanda_container(igloo_dir) + }) + .map_err(|err| { RoutineFailure::new( Message::new( "Failed".to_string(), @@ -115,10 +119,12 @@ impl Routine for RunClickhouseContainer { .internal_dir() .map_err(|err| RoutineFailure::new(FAILED_TO_CREATE_INTERNAL_DIR.clone(), err))?; - let output = docker::safe_start_clickhouse_container( - igloo_dir, - self.project.clickhouse_config.clone(), - ) + let output = with_spinner("Starting clickhouse container", || { + docker::safe_start_clickhouse_container( + igloo_dir, + self.project.clickhouse_config.clone(), + ) + }) .map_err(|err| { RoutineFailure::new( Message::new( @@ -149,10 +155,12 @@ impl RunConsoleContainer { impl Routine for RunConsoleContainer { fn run_silent(&self) -> Result { - let output = docker::safe_start_console_container( - &self.project.console_config, - &self.project.clickhouse_config, - ) + let output = with_spinner("Starting console container", || { + docker::safe_start_console_container( + &self.project.console_config, + &self.project.clickhouse_config, + ) + }) .map_err(|err| { RoutineFailure::new( Message::new("Failed".to_string(), "to run console container".to_string()), diff --git a/apps/igloo-kit-cli/src/cli/routines/stop.rs b/apps/igloo-kit-cli/src/cli/routines/stop.rs index 4e3b54d90..fa98418a6 100644 --- a/apps/igloo-kit-cli/src/cli/routines/stop.rs +++ b/apps/igloo-kit-cli/src/cli/routines/stop.rs @@ -1,4 +1,5 @@ use super::{Routine, RoutineFailure, RoutineSuccess, RunMode}; +use crate::cli::display::with_spinner; use crate::cli::routines::util::ensure_docker_running; use crate::utilities::constants::{ CLICKHOUSE_CONTAINER_NAME, CONSOLE_CONTAINER_NAME, REDPANDA_CONTAINER_NAME, @@ -37,7 +38,10 @@ impl StopRedPandaContainer { } impl Routine for StopRedPandaContainer { fn run_silent(&self) -> Result { - docker::stop_container(REDPANDA_CONTAINER_NAME).map_err(|err| { + with_spinner("Stopping redpanda container", || { + docker::stop_container(REDPANDA_CONTAINER_NAME) + }) + .map_err(|err| { RoutineFailure::new( Message::new( "Failed".to_string(), @@ -62,7 +66,10 @@ impl StopClickhouseContainer { } impl Routine for StopClickhouseContainer { fn run_silent(&self) -> Result { - docker::stop_container(CLICKHOUSE_CONTAINER_NAME).map_err(|err| { + with_spinner("Stopping clickhouse container", || { + docker::stop_container(CLICKHOUSE_CONTAINER_NAME) + }) + .map_err(|err| { RoutineFailure::new( Message::new( "Failed".to_string(), @@ -87,7 +94,10 @@ impl StopConsoleContainer { } impl Routine for StopConsoleContainer { fn run_silent(&self) -> Result { - docker::stop_container(CONSOLE_CONTAINER_NAME).map_err(|err| { + with_spinner("Stopping console container", || { + docker::stop_container(CONSOLE_CONTAINER_NAME) + }) + .map_err(|err| { RoutineFailure::new( Message::new( "Failed".to_string(), diff --git a/apps/igloo-kit-cli/src/cli/settings.rs b/apps/igloo-kit-cli/src/cli/settings.rs index 94d147d22..87d5ca614 100644 --- a/apps/igloo-kit-cli/src/cli/settings.rs +++ b/apps/igloo-kit-cli/src/cli/settings.rs @@ -40,15 +40,15 @@ pub struct Settings { } fn config_path() -> PathBuf { - let mut path: PathBuf = home_dir().unwrap(); + let mut path: PathBuf = user_directory(); path.push(CLI_CONFIG_FILE); - path.to_owned() + path } pub fn user_directory() -> PathBuf { let mut path: PathBuf = home_dir().unwrap(); path.push(CLI_USER_DIRECTORY); - path.to_owned() + path } pub fn setup_user_directory() -> Result<(), std::io::Error> { @@ -84,3 +84,18 @@ pub fn read_settings() -> Result { s.try_deserialize() } + +pub fn init_config_file() -> Result<(), std::io::Error> { + let path = config_path(); + if !path.exists() { + let contents_toml = r#"# Feature flags to hide ongoing feature work on the CLI +[features] + +# Coming soon wall on all the CLI commands as we build the MVP. +# if you want to try features as they are built, set this to false +coming_soon_wall=true +"#; + std::fs::write(path, contents_toml)?; + } + Ok(()) +} diff --git a/apps/igloo-kit-cli/src/cli/watcher.rs b/apps/igloo-kit-cli/src/cli/watcher.rs index b80ee94e2..ded3ec354 100644 --- a/apps/igloo-kit-cli/src/cli/watcher.rs +++ b/apps/igloo-kit-cli/src/cli/watcher.rs @@ -233,6 +233,7 @@ async fn watch( &configured_client, &configured_producer, route_table.clone(), + project.console_config.clone(), ) .await; } diff --git a/apps/igloo-kit-cli/src/infrastructure/console.rs b/apps/igloo-kit-cli/src/infrastructure/console.rs index a099ded9e..bbb1e9543 100644 --- a/apps/igloo-kit-cli/src/infrastructure/console.rs +++ b/apps/igloo-kit-cli/src/infrastructure/console.rs @@ -22,7 +22,7 @@ use std::str; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ConsoleConfig { - pub host_port: i32, // ex. 18123 + pub host_port: u16, // ex. 18123 } impl Default for ConsoleConfig { @@ -35,6 +35,7 @@ pub async fn post_current_state_to_console( configured_db_client: &ConfiguredDBClient, configured_producer: &ConfiguredProducer, route_table: Arc>>, + console_config: ConsoleConfig, ) -> Result<(), anyhow::Error> { let tables = olap::clickhouse::fetch_all_tables(configured_db_client) .await @@ -59,9 +60,10 @@ pub async fn post_current_state_to_console( .collect(); // TODO this should be configurable - let url = "http://localhost:3001/api/console".parse::()?; + let url = format!("http://localhost:{}/api/console", console_config.host_port) + .parse::()?; let host = url.host().expect("uri has no host"); - let port = url.port_u16().unwrap_or(3001); + let port = url.port_u16().unwrap(); let address = format!("{}:{}", host, port); debug!("Connecting to moose console at: {}", address); diff --git a/apps/igloo-kit-cli/src/utilities/constants.rs b/apps/igloo-kit-cli/src/utilities/constants.rs index 21e534d44..18985c479 100644 --- a/apps/igloo-kit-cli/src/utilities/constants.rs +++ b/apps/igloo-kit-cli/src/utilities/constants.rs @@ -2,7 +2,7 @@ pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const PROJECT_CONFIG_FILE: &str = "project.toml"; -pub const CLI_CONFIG_FILE: &str = ".igloo-config.toml"; +pub const CLI_CONFIG_FILE: &str = "config.toml"; pub const CLI_USER_DIRECTORY: &str = ".igloo"; pub const CLI_PROJECT_INTERNAL_DIR: &str = ".igloo"; diff --git a/apps/igloo-kit-cli/src/utilities/docker.rs b/apps/igloo-kit-cli/src/utilities/docker.rs index bf6af9d8d..f5c2bcee4 100644 --- a/apps/igloo-kit-cli/src/utilities/docker.rs +++ b/apps/igloo-kit-cli/src/utilities/docker.rs @@ -359,6 +359,8 @@ fn run_console( let child = Command::new("docker") .arg("run") .arg("-d") + .arg("--platform") + .arg("linux/amd64") .arg(format!("--name={CONSOLE_CONTAINER_NAME}")) .arg(format!("--env=CLICKHOUSE_DB={}", clickhouse_config.db_name)) .arg(format!("--env=CLICKHOUSE_USER={}", clickhouse_config.user))