Skip to content

Commit

Permalink
feat(turborepo): web ui (#8895)
Browse files Browse the repository at this point in the history
### Description

Implements a very very alpha version of a web UI. Uses a GraphQL query
to get the current run and tasks.

### Testing Instructions

Would love some ideas here on tests.

To try out manually, go to #35066 in the other repo and run
`turbo-studio`, then execute a turbo command with the `ui` flag set to
`web`
  • Loading branch information
NicholasLYang authored Sep 10, 2024
1 parent bd2bffa commit b7a00d3
Show file tree
Hide file tree
Showing 25 changed files with 889 additions and 188 deletions.
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ async-compression = { version = "0.3.13", default-features = false, features = [
"gzip",
"tokio",
] }
async-graphql = "7.0.7"
async-graphql-axum = "7.0.7"
async-trait = "0.1.64"
atty = "0.2.14"
axum = "0.7.5"
Expand Down
4 changes: 2 additions & 2 deletions crates/turborepo-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ turborepo-vercel-api-mock = { workspace = true }
workspace = true

[dependencies]
async-graphql = "7.0.7"
async-graphql-axum = "7.0.7"
async-graphql = { workspace = true }
async-graphql-axum = { workspace = true }
atty = { workspace = true }
axum = { workspace = true }
biome_deserialize = { workspace = true }
Expand Down
10 changes: 8 additions & 2 deletions crates/turborepo-lib/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::future::Future;

use tracing::error;
use turborepo_telemetry::events::command::CommandEventBuilder;
use turborepo_ui::sender::UISender;

use crate::{commands::CommandBase, run, run::builder::RunBuilder, signal::SignalHandler};

Expand Down Expand Up @@ -44,15 +45,20 @@ pub async fn run(base: CommandBase, telemetry: CommandEventBuilder) -> Result<i3
.build(&handler, telemetry)
.await?;

let (sender, handle) = run.start_experimental_ui()?.unzip();
let (sender, handle) = run.start_ui()?.unzip();

let result = run.run(sender.clone(), false).await;

if let Some(analytics_handle) = analytics_handle {
analytics_handle.close_with_timeout().await;
}

if let (Some(handle), Some(sender)) = (handle, sender) {
// We only stop if it's the TUI, for the web UI we don't need to stop
if let Some(UISender::Tui(sender)) = sender {
sender.stop();
}

if let Some(handle) = handle {
if let Err(e) = handle.await.expect("render thread panicked") {
error!("error encountered rendering tui: {e}");
}
Expand Down
5 changes: 5 additions & 0 deletions crates/turborepo-lib/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
commands::CommandBase,
config::ConfigurationOptions,
run::task_id::TaskId,
turbo_json::UIMode,
};

#[derive(Debug, Error)]
Expand Down Expand Up @@ -166,6 +167,7 @@ pub struct RunOpts {
pub summarize: Option<Option<bool>>,
pub(crate) experimental_space_id: Option<String>,
pub is_github_actions: bool,
pub ui_mode: UIMode,
}

impl RunOpts {
Expand Down Expand Up @@ -267,6 +269,7 @@ impl<'a> TryFrom<OptsInputs<'a>> for RunOpts {
env_mode: inputs.config.env_mode(),
cache_dir: inputs.config.cache_dir().into(),
is_github_actions,
ui_mode: inputs.config.ui(),
})
}
}
Expand Down Expand Up @@ -394,6 +397,7 @@ mod test {
use crate::{
cli::DryRunMode,
opts::{Opts, RunCacheOpts, ScopeOpts},
turbo_json::UIMode,
};

#[derive(Default)]
Expand Down Expand Up @@ -499,6 +503,7 @@ mod test {
only: opts_input.only,
dry_run: opts_input.dry_run,
graph: None,
ui_mode: UIMode::Stream,
single_package: false,
log_prefix: crate::opts::ResolvedLogPrefix::Task,
log_order: crate::opts::ResolvedLogOrder::Stream,
Expand Down
12 changes: 6 additions & 6 deletions crates/turborepo-lib/src/run/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ pub struct RunBuilder {
root_turbo_json_path: AbsoluteSystemPathBuf,
color_config: ColorConfig,
version: &'static str,
ui_mode: UIMode,
api_client: APIClient,
analytics_sender: Option<AnalyticsSender>,
// In watch mode, we can have a changed package that we want to serve as an entrypoint.
Expand All @@ -78,13 +77,12 @@ impl RunBuilder {
let allow_missing_package_manager = config.allow_no_package_manager();

let version = base.version();
let ui_mode = config.ui();
let processes = ProcessManager::new(
// We currently only use a pty if the following are met:
// - we're attached to a tty
atty::is(atty::Stream::Stdout) &&
// - if we're on windows, we're using the UI
(!cfg!(windows) || matches!(ui_mode, UIMode::Tui)),
(!cfg!(windows) || matches!(opts.run_opts.ui_mode, UIMode::Tui)),
);
let root_turbo_json_path = config.root_turbo_json_path(&base.repo_root);

Expand All @@ -101,7 +99,6 @@ impl RunBuilder {
repo_root,
color_config: ui,
version,
ui_mode,
api_auth,
analytics_sender: None,
entrypoint_packages: None,
Expand Down Expand Up @@ -413,7 +410,6 @@ impl RunBuilder {
Ok(Run {
version: self.version,
color_config: self.color_config,
ui_mode: self.ui_mode,
start_at,
processes: self.processes,
run_telemetry,
Expand Down Expand Up @@ -468,7 +464,11 @@ impl RunBuilder {

if !self.opts.run_opts.parallel {
engine
.validate(pkg_dep_graph, self.opts.run_opts.concurrency, self.ui_mode)
.validate(
pkg_dep_graph,
self.opts.run_opts.concurrency,
self.opts.run_opts.ui_mode,
)
.map_err(Error::EngineValidation)?;
}

Expand Down
2 changes: 2 additions & 0 deletions crates/turborepo-lib/src/run/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@ pub enum Error {
#[error(transparent)]
Daemon(#[from] daemon::DaemonError),
#[error(transparent)]
UI(#[from] turborepo_ui::Error),
#[error(transparent)]
Tui(#[from] tui::Error),
}
52 changes: 36 additions & 16 deletions crates/turborepo-lib/src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ use turborepo_env::EnvironmentVariableMap;
use turborepo_repository::package_graph::{PackageGraph, PackageName, PackageNode};
use turborepo_scm::SCM;
use turborepo_telemetry::events::generic::GenericEventBuilder;
use turborepo_ui::{cprint, cprintln, tui, tui::AppSender, ColorConfig, BOLD_GREY, GREY};
use turborepo_ui::{
cprint, cprintln, sender::UISender, tui, tui::TuiSender, wui::sender::WebUISender, ColorConfig,
BOLD_GREY, GREY,
};

pub use crate::run::error::Error;
use crate::{
Expand Down Expand Up @@ -69,9 +72,13 @@ pub struct Run {
task_access: TaskAccess,
daemon: Option<DaemonClient<DaemonConnector>>,
should_print_prelude: bool,
ui_mode: UIMode,
}

type UIResult<T> = Result<Option<(T, JoinHandle<Result<(), turborepo_ui::Error>>)>, Error>;

type WuiResult = UIResult<WebUISender>;
type TuiResult = UIResult<TuiSender>;

impl Run {
fn has_persistent_tasks(&self) -> bool {
self.engine.has_persistent_tasks
Expand Down Expand Up @@ -195,24 +202,41 @@ impl Run {
}

pub fn has_tui(&self) -> bool {
self.ui_mode.use_tui()
self.opts.run_opts.ui_mode.use_tui()
}

pub fn should_start_ui(&self) -> Result<bool, Error> {
Ok(self.ui_mode.use_tui()
Ok(self.opts.run_opts.ui_mode.use_tui()
&& self.opts.run_opts.dry_run.is_none()
&& tui::terminal_big_enough()?)
}

#[allow(clippy::type_complexity)]
pub fn start_experimental_ui(
&self,
) -> Result<Option<(AppSender, JoinHandle<Result<(), tui::Error>>)>, Error> {
pub fn start_ui(&self) -> UIResult<UISender> {
// Print prelude here as this needs to happen before the UI is started
if self.should_print_prelude {
self.print_run_prelude();
}

match self.opts.run_opts.ui_mode {
UIMode::Tui => self
.start_terminal_ui()
.map(|res| res.map(|(sender, handle)| (UISender::Tui(sender), handle))),
UIMode::Stream => Ok(None),
UIMode::Web => self
.start_web_ui()
.map(|res| res.map(|(sender, handle)| (UISender::Wui(sender), handle))),
}
}
fn start_web_ui(&self) -> WuiResult {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

let handle = tokio::spawn(turborepo_ui::wui::server::start_server(rx));

Ok(Some((WebUISender { tx }, handle)))
}

#[allow(clippy::type_complexity)]
fn start_terminal_ui(&self) -> TuiResult {
if !self.should_start_ui()? {
return Ok(None);
}
Expand All @@ -223,8 +247,8 @@ impl Run {
return Ok(None);
}

let (sender, receiver) = AppSender::new();
let handle = tokio::task::spawn_blocking(move || tui::run_app(task_names, receiver));
let (sender, receiver) = TuiSender::new();
let handle = tokio::task::spawn_blocking(move || Ok(tui::run_app(task_names, receiver)?));

Ok(Some((sender, handle)))
}
Expand All @@ -236,11 +260,7 @@ impl Run {
}
}

pub async fn run(
&mut self,
experimental_ui_sender: Option<AppSender>,
is_watch: bool,
) -> Result<i32, Error> {
pub async fn run(&mut self, ui_sender: Option<UISender>, is_watch: bool) -> Result<i32, Error> {
let skip_cache_writes = self.opts.runcache_opts.skip_writes;
if let Some(subscriber) = self.signal_handler.subscribe() {
let run_cache = self.run_cache.clone();
Expand Down Expand Up @@ -427,7 +447,7 @@ impl Run {
self.processes.clone(),
&self.repo_root,
global_env,
experimental_ui_sender,
ui_sender,
is_watch,
);

Expand Down
14 changes: 8 additions & 6 deletions crates/turborepo-lib/src/run/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tokio::{
use tracing::{instrument, trace};
use turborepo_repository::package_graph::PackageName;
use turborepo_telemetry::events::command::CommandEventBuilder;
use turborepo_ui::{tui, tui::AppSender};
use turborepo_ui::sender::UISender;

use crate::{
cli::{Command, RunArgs},
Expand Down Expand Up @@ -53,8 +53,8 @@ pub struct WatchClient {
base: CommandBase,
telemetry: CommandEventBuilder,
handler: SignalHandler,
ui_sender: Option<AppSender>,
ui_handle: Option<JoinHandle<Result<(), tui::Error>>>,
ui_sender: Option<UISender>,
ui_handle: Option<JoinHandle<Result<(), turborepo_ui::Error>>>,
}

struct PersistentRunHandle {
Expand Down Expand Up @@ -99,6 +99,8 @@ pub enum Error {
SignalInterrupt,
#[error("package change error")]
PackageChange(#[from] tonic::Status),
#[error(transparent)]
UI(#[from] turborepo_ui::Error),
#[error("could not connect to UI thread")]
UISend(String),
#[error("cannot use root turbo.json at {0} with watch mode")]
Expand Down Expand Up @@ -134,7 +136,7 @@ impl WatchClient {

let watched_packages = run.get_relevant_packages();

let (sender, handle) = run.start_experimental_ui()?.unzip();
let (ui_sender, ui_handle) = run.start_ui()?.unzip();

let connector = DaemonConnector {
can_start_server: true,
Expand All @@ -150,8 +152,8 @@ impl WatchClient {
handler,
telemetry,
persistent_tasks_handle: None,
ui_sender: sender,
ui_handle: handle,
ui_sender,
ui_handle,
})
}

Expand Down
Loading

0 comments on commit b7a00d3

Please sign in to comment.