Show spinner when opening/creating a project (#6321)

Closes #5505: A spinner with the Enso logo is now shown for most of the duration when opening and creating a project.

https://user-images.githubusercontent.com/607786/234019207-e34096c7-2cb3-4ae3-abd3-291fcaf43084.mp4

# Important Notes
There's still a tiny bit of lag between the end of the spinner and when the nodes pop up, but that seemed less trivial to me to work around.
This commit is contained in:
Stijn ("stain") Seghers 2023-05-15 16:08:56 +02:00 committed by GitHub
parent 23e75f5e97
commit 341f1275e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 267 additions and 326 deletions

View File

@ -5,6 +5,8 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use double_representation::name::project;
use mockall::automock;
use parser::Parser;
@ -102,9 +104,7 @@ impl StatusNotificationPublisher {
/// used internally in code.
#[derive(Copy, Clone, Debug)]
pub enum Notification {
/// User created a new project. The new project is opened in IDE.
NewProjectCreated,
/// User opened an existing project.
/// User opened a new or existing project.
ProjectOpened,
/// User closed the project.
ProjectClosed,
@ -118,10 +118,12 @@ pub enum Notification {
// === Errors ===
#[allow(missing_docs)]
/// Error raised when a project with given name or ID was not found.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project with name \"{}\" not found.", 0)]
struct ProjectNotFound(String);
#[fail(display = "Project '{}' was not found.", project)]
pub struct ProjectNotFound {
project: ProjectToOpen,
}
// === Managing API ===
@ -131,11 +133,16 @@ struct ProjectNotFound(String);
/// It is a separate trait, because those methods are not supported in some environments (see also
/// [`API::manage_projects`]).
pub trait ManagingProjectAPI {
/// Create a new unnamed project and open it in the IDE.
/// Create a new project and open it in the IDE.
///
/// `name` is an optional project name. It overrides the name of the template if given.
/// `template` is an optional project template name. Available template names are defined in
/// `lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala`.
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult>;
fn create_new_project(
&self,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult>;
/// Return a list of existing projects.
fn list_projects(&self) -> BoxFuture<FallibleResult<Vec<ProjectMetadata>>>;
@ -150,18 +157,44 @@ pub trait ManagingProjectAPI {
/// and then for the project opening.
fn open_project_by_name(&self, name: String) -> BoxFuture<FallibleResult> {
async move {
let projects = self.list_projects().await?;
let mut projects = projects.into_iter();
let project = projects.find(|project| project.name.as_ref() == name);
let uuid = project.map(|project| project.id);
if let Some(uuid) = uuid {
self.open_project(uuid).await
} else {
Err(ProjectNotFound(name).into())
let project_id = self.find_project(&ProjectToOpen::Name(name.into())).await?;
self.open_project(project_id).await
}
.boxed_local()
}
/// Open a project by name or ID. If no project with the given name exists, it will be created.
fn open_or_create_project(&self, project_to_open: ProjectToOpen) -> BoxFuture<FallibleResult> {
async move {
match self.find_project(&project_to_open).await {
Ok(project_id) => self.open_project(project_id).await,
Err(error) =>
if let ProjectToOpen::Name(name) = project_to_open {
info!("Attempting to create project with name '{name}'.");
self.create_new_project(Some(name.to_string()), None).await
} else {
Err(error)
},
}
}
.boxed_local()
}
/// Find a project by name or ID.
fn find_project<'a: 'c, 'b: 'c, 'c>(
&'a self,
project_to_open: &'b ProjectToOpen,
) -> BoxFuture<'c, FallibleResult<Uuid>> {
async move {
self.list_projects()
.await?
.into_iter()
.find(|project_metadata| project_to_open.matches(project_metadata))
.map(|metadata| metadata.id)
.ok_or_else(|| ProjectNotFound { project: project_to_open.clone() }.into())
}
.boxed_local()
}
}

View File

@ -4,12 +4,10 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use crate::controller::ide::ManagingProjectAPI;
use crate::controller::ide::Notification;
use crate::controller::ide::StatusNotificationPublisher;
use crate::controller::ide::API;
use crate::ide::initializer;
use double_representation::name::project;
use engine_protocol::project_manager;
@ -49,53 +47,16 @@ pub struct Handle {
}
impl Handle {
/// Create IDE controller. If `maybe_project_name` is `Some`, a project with provided name will
/// be opened. Otherwise controller will be used for project manager operations by Welcome
/// Screen.
pub async fn new(
project_manager: Rc<dyn project_manager::API>,
project_to_open: Option<ProjectToOpen>,
) -> FallibleResult<Self> {
let project = match project_to_open {
Some(project_to_open) =>
Some(Self::init_project_model(project_manager.clone_ref(), project_to_open).await?),
None => None,
};
Ok(Self::new_with_project_model(project_manager, project))
}
/// Create IDE controller with prepared project model. If `project` is `None`,
/// `API::current_project` returns `None` as well.
pub fn new_with_project_model(
project_manager: Rc<dyn project_manager::API>,
project: Option<model::Project>,
) -> Self {
let current_project = Rc::new(CloneCell::new(project));
let status_notifications = default();
let parser = Parser::new();
let notifications = default();
let component_browser_private_entries_visibility_flag = default();
Self {
current_project,
/// Create IDE controller.
pub fn new(project_manager: Rc<dyn project_manager::API>) -> FallibleResult<Self> {
Ok(Self {
current_project: default(),
project_manager,
status_notifications,
parser,
notifications,
component_browser_private_entries_visibility_flag,
}
}
/// Open project with provided name.
async fn init_project_model(
project_manager: Rc<dyn project_manager::API>,
project_to_open: ProjectToOpen,
) -> FallibleResult<model::Project> {
// TODO[ao]: Reuse of initializer used in previous code design. It should be soon replaced
// anyway, because we will soon resign from the "open or create" approach when opening
// IDE. See https://github.com/enso-org/ide/issues/1492 for details.
let initializer = initializer::WithProjectManager::new(project_manager, project_to_open);
let model = initializer.initialize_project_model().await?;
Ok(model)
status_notifications: default(),
parser: default(),
notifications: default(),
component_browser_private_entries_visibility_flag: default(),
})
}
}
@ -133,14 +94,16 @@ impl API for Handle {
impl ManagingProjectAPI for Handle {
#[profile(Objective)]
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult> {
fn create_new_project(
&self,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult> {
async move {
use model::project::Synchronized as Project;
let list = self.project_manager.list_projects(&None).await?;
let existing_names: HashSet<_> =
list.projects.into_iter().map(|p| p.name.into()).collect();
let name = make_project_name(&template);
let name = name.unwrap_or_else(|| make_project_name(&template));
let name = choose_unique_project_name(&existing_names, &name);
let name = ProjectName::new_unchecked(name);
let version = &enso_config::ARGS.groups.engine.options.preferred_version.value;
@ -151,12 +114,7 @@ impl ManagingProjectAPI for Handle {
.project_manager
.create_project(&name, &template.map(|t| t.into()), &version, &action)
.await?;
let new_project_id = create_result.project_id;
let project_mgr = self.project_manager.clone_ref();
let new_project = Project::new_opened(project_mgr, new_project_id);
self.current_project.set(Some(new_project.await?));
self.notifications.notify(Notification::NewProjectCreated);
Ok(())
self.open_project(create_result.project_id).await
}
.boxed_local()
}

View File

@ -679,33 +679,6 @@ impl Searcher {
Mode::NewNode { .. } => self.add_example(&example).map(Some),
_ => Err(CannotExecuteWhenEditingNode.into()),
},
Action::ProjectManagement(action) => {
match self.ide.manage_projects() {
Ok(_) => {
let ide = self.ide.clone_ref();
executor::global::spawn(async move {
// We checked that manage_projects returns Some just a moment ago, so
// unwrapping is safe.
let manage_projects = ide.manage_projects().unwrap();
let result = match action {
action::ProjectManagement::CreateNewProject =>
manage_projects.create_new_project(None),
action::ProjectManagement::OpenProject { id, .. } =>
manage_projects.open_project(*id),
};
if let Err(err) = result.await {
error!("Error when creating new project: {err}");
}
});
Ok(None)
}
Err(err) => Err(NotSupported {
action_label: Action::ProjectManagement(action).to_string(),
reason: err,
}
.into()),
}
}
}
}
@ -1001,12 +974,6 @@ impl Searcher {
let mut actions = action::ListWithSearchResultBuilder::new();
let (libraries_icon, default_icon) =
action::hardcoded::ICONS.with(|i| (i.libraries.clone_ref(), i.default.clone_ref()));
if should_add_additional_entries && self.ide.manage_projects().is_ok() {
let mut root_cat = actions.add_root_category("Projects", default_icon.clone_ref());
let category = root_cat.add_category("Projects", default_icon.clone_ref());
let create_project = action::ProjectManagement::CreateNewProject;
category.add_action(Action::ProjectManagement(create_project));
}
let mut libraries_root_cat =
actions.add_root_category("Libraries", libraries_icon.clone_ref());
if should_add_additional_entries {

View File

@ -66,14 +66,6 @@ impl Suggestion {
/// Action of adding example code.
pub type Example = Rc<model::suggestion_database::Example>;
/// A variants of project management actions. See also [`Action`].
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug, Eq, PartialEq)]
pub enum ProjectManagement {
CreateNewProject,
OpenProject { id: Immutable<Uuid>, name: ImString },
}
/// A single action on the Searcher list. See also `controller::searcher::Searcher` docs.
#[derive(Clone, CloneRef, Debug, PartialEq)]
pub enum Action {
@ -84,8 +76,6 @@ pub enum Action {
/// Add to the current module a new function with example code, and a new node in
/// current scene calling that function.
Example(Example),
/// The project management operation: creating or opening, projects.
ProjectManagement(ProjectManagement),
// In the future, other action types will be added (like module/method management, etc.).
}
@ -101,10 +91,6 @@ impl Display for Action {
Self::Suggestion(Suggestion::Hardcoded(suggestion)) =>
Display::fmt(&suggestion.name, f),
Self::Example(example) => write!(f, "Example: {}", example.name),
Self::ProjectManagement(ProjectManagement::CreateNewProject) =>
write!(f, "New Project"),
Self::ProjectManagement(ProjectManagement::OpenProject { name, .. }) =>
Display::fmt(name, f),
}
}
}

View File

@ -2,6 +2,7 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use crate::presenter::Presenter;
use analytics::AnonymousData;
@ -90,6 +91,11 @@ impl Ide {
}
}
}
/// Open a project by name or ID. If no project with the given name exists, it will be created.
pub fn open_or_create_project(&self, project: ProjectToOpen) {
self.presenter.open_or_create_project(project)
}
}
/// A reduced version of [`Ide`] structure, representing an application which failed to initialize.
@ -101,7 +107,6 @@ pub struct FailedIde {
pub view: ide_view::root::View,
}
/// The Path of the module initially opened after opening project in IDE.
pub fn initial_module_path(project: &model::Project) -> model::module::Path {
project.main_module_path()

View File

@ -3,17 +3,14 @@
use crate::prelude::*;
use crate::config;
use crate::config::ProjectToOpen;
use crate::ide::Ide;
use crate::transport::web::WebSocket;
use crate::FailedIde;
use engine_protocol::project_manager;
use engine_protocol::project_manager::ProjectName;
use enso_web::sleep;
use ensogl::application::Application;
use std::time::Duration;
use uuid::Uuid;
@ -35,19 +32,6 @@ const INITIALIZATION_RETRY_TIMES: &[Duration] =
// ==============
// === Errors ===
// ==============
/// Error raised when project with given name was not found.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project '{}' was not found.", name)]
pub struct ProjectNotFound {
name: ProjectToOpen,
}
// ===================
// === Initializer ===
// ===================
@ -94,26 +78,36 @@ impl Initializer {
// issues to user, such information should be properly passed
// in case of setup failure.
match self.initialize_ide_controller_with_retries().await {
Ok(controller) => {
let ide = Ide::new(ensogl_app, view.clone_ref(), controller);
if let Some(project) = &self.config.project_to_open {
ide.open_or_create_project(project.clone());
}
info!("IDE was successfully initialized.");
Ok(ide)
}
Err(error) => {
let message = format!("Failed to initialize application: {error}");
status_bar.add_event(ide_view::status_bar::event::Label::new(message));
Err(FailedIde { view })
}
}
}
async fn initialize_ide_controller_with_retries(&self) -> FallibleResult<controller::Ide> {
let mut retry_after = INITIALIZATION_RETRY_TIMES.iter();
loop {
match self.initialize_ide_controller().await {
Ok(controller) => {
let ide = Ide::new(ensogl_app, view.clone_ref(), controller);
info!("Setup done.");
break Ok(ide);
}
Ok(controller) => break Ok(controller),
Err(error) => {
let message = format!("Failed to initialize application: {error}");
error!("{message}");
error!("Failed to initialize controller: {error}");
match retry_after.next() {
Some(time) => {
error!("Retrying after {} seconds", time.as_secs_f32());
sleep(*time).await;
}
None => {
status_bar.add_event(ide_view::status_bar::event::Label::new(message));
break Err(FailedIde { view });
}
None => break Err(error),
}
}
}
@ -130,9 +124,8 @@ impl Initializer {
match &self.config.backend {
ProjectManager { endpoint } => {
let project_manager = self.setup_project_manager(endpoint).await?;
let project_to_open = self.config.project_to_open.clone();
let controller = controller::ide::Desktop::new(project_manager, project_to_open);
Ok(Rc::new(controller.await?))
let controller = controller::ide::Desktop::new(project_manager)?;
Ok(Rc::new(controller))
}
LanguageServer { json_endpoint, binary_endpoint, namespace, project_name } => {
let json_endpoint = json_endpoint.clone();
@ -172,81 +165,6 @@ impl Initializer {
// ==========================
// === WithProjectManager ===
// ==========================
/// Ide Initializer with project manager.
///
/// This structure do the specific initialization part when we are connected to Project Manager,
/// like list projects, find the one we want to open, open it, or create new one if it does not
/// exist.
#[allow(missing_docs)]
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct WithProjectManager {
#[derivative(Debug = "ignore")]
pub project_manager: Rc<dyn project_manager::API>,
pub project_to_open: ProjectToOpen,
}
impl WithProjectManager {
/// Constructor.
pub fn new(
project_manager: Rc<dyn project_manager::API>,
project_to_open: ProjectToOpen,
) -> Self {
Self { project_manager, project_to_open }
}
/// Create and initialize a new Project Model, for a project with name passed in constructor.
///
/// If the project with given name does not exist yet, it will be created.
pub async fn initialize_project_model(self) -> FallibleResult<model::Project> {
let project_id = self.get_project_or_create_new().await?;
let project_manager = self.project_manager;
model::project::Synchronized::new_opened(project_manager, project_id).await
}
/// Creates a new project and returns its id, so the newly connected project can be opened.
pub async fn create_project(&self, project_name: &ProjectName) -> FallibleResult<Uuid> {
use project_manager::MissingComponentAction::Install;
info!("Creating a new project named '{}'.", project_name);
let version = &enso_config::ARGS.groups.engine.options.preferred_version.value;
let version = (!version.is_empty()).as_some_from(|| version.clone());
let response = self.project_manager.create_project(project_name, &None, &version, &Install);
Ok(response.await?.project_id)
}
async fn lookup_project(&self) -> FallibleResult<Uuid> {
let response = self.project_manager.list_projects(&None).await?;
let mut projects = response.projects.iter();
projects
.find(|project_metadata| self.project_to_open.matches(project_metadata))
.map(|md| md.id)
.ok_or_else(|| ProjectNotFound { name: self.project_to_open.clone() }.into())
}
/// Look for the project with the name specified when constructing this initializer,
/// or, if it does not exist, create it. The id of found/created project is returned.
pub async fn get_project_or_create_new(&self) -> FallibleResult<Uuid> {
let project = self.lookup_project().await;
if let Ok(project_id) = project {
Ok(project_id)
} else if let ProjectToOpen::Name(name) = &self.project_to_open {
info!("Attempting to create {}", name);
self.create_project(name).await
} else {
// This can happen only if we are told to open project by id but it cannot be found.
// We cannot fallback to creating a new project in this case, as we cannot create a
// project with a given id. Thus, we simply propagate the lookup result.
project
}
}
}
// =============
// === Utils ===
// =============
@ -290,6 +208,10 @@ pub fn register_views(app: &Application) {
mod test {
use super::*;
use crate::config::ProjectToOpen;
use crate::controller::ide::ManagingProjectAPI;
use crate::engine_protocol::project_manager::ProjectName;
use json_rpc::expect_call;
use wasm_bindgen_test::wasm_bindgen_test;
@ -313,9 +235,10 @@ mod test {
expect_call!(mock_client.list_projects(count) => Ok(project_lists));
let project_manager = Rc::new(mock_client);
let ide_controller = controller::ide::Desktop::new(project_manager).unwrap();
let project_to_open = ProjectToOpen::Name(project_name);
let initializer = WithProjectManager { project_manager, project_to_open };
let project = initializer.get_project_or_create_new().await;
assert_eq!(expected_id, project.expect("Couldn't get project."))
let project_id =
ide_controller.find_project(&project_to_open).await.expect("Couldn't get project.");
assert_eq!(project_id, expected_id);
}
}

View File

@ -83,7 +83,10 @@ impl Fixture {
let project_management =
controller.manage_projects().expect("Cannot access Managing Project API");
project_management.create_new_project(None).await.expect("Failed to create new project");
project_management
.create_new_project(None, None)
.await
.expect("Failed to create new project");
}
/// After returning, the IDE is in a state with the project opened and ready to work

View File

@ -4,13 +4,16 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use crate::controller::ide::StatusNotification;
use crate::executor::global::spawn_stream_handler;
use crate::presenter;
use enso_frp as frp;
use ensogl::system::js;
use ide_view as view;
use ide_view::graph_editor::SharedHashMap;
use std::time::Duration;
// ==============
@ -29,6 +32,19 @@ pub use searcher::Searcher;
// =================
// === Constants ===
// =================
/// We don't know how long opening the project will take, but we still want to show a fake
/// progress indicator for the user. This constant represents how long the spinner will run for in
/// milliseconds.
const OPEN_PROJECT_SPINNER_TIME_MS: u64 = 5_000;
/// The interval in milliseconds at which we should increase the spinner
const OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS: u64 = 10;
// =============
// === Model ===
// =============
@ -94,15 +110,15 @@ impl Model {
#[profile(Task)]
pub fn open_project(&self, project_name: String) {
let controller = self.controller.clone_ref();
crate::executor::global::spawn(async move {
crate::executor::global::spawn(with_progress_indicator(|| async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.open_project_by_name(project_name).await {
error!("Cannot open project by name: {err}.");
}
} else {
warn!("Project opening failed: no ProjectManagingAPI available.");
warn!("Project Manager API not available, cannot open project.");
}
});
}));
}
/// Create a new project. `template` is an optional name of the project template passed to the
@ -113,9 +129,10 @@ impl Model {
if let Ok(template) =
template.map(double_representation::name::project::Template::from_text).transpose()
{
crate::executor::global::spawn(async move {
crate::executor::global::spawn(with_progress_indicator(|| async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Err(err) = managing_api.create_new_project(None, template.clone()).await
{
if let Some(template) = template {
error!("Could not create new project from template {template}: {err}.");
} else {
@ -123,13 +140,66 @@ impl Model {
}
}
} else {
warn!("Project creation failed: no ProjectManagingAPI available.");
warn!("Project Manager API not available, cannot create project.");
}
})
}))
} else if let Some(template) = template {
error!("Invalid project template name: {template}");
};
}
/// Open a project by name or ID. If no project with the given name exists, it will be created.
#[profile(Task)]
fn open_or_create_project(&self, project: ProjectToOpen) {
let controller = self.controller.clone_ref();
crate::executor::global::spawn(with_progress_indicator(|| async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(error) = managing_api.open_or_create_project(project).await {
error!("Cannot open or create project. {error}");
}
} else {
warn!("Project Manager API not available, cannot open or create project.");
}
}));
}
}
/// Show a full-screen spinner for the exact duration of the specified function.
async fn with_progress_indicator<F, T>(f: F)
where
F: FnOnce() -> T,
T: Future<Output = ()>, {
// TODO[ss]: Use a safer variant of getting the JS app. This one gets a variable from JS, casts
// it to a type, etc. Somewhere in EnsoGL we might already have some logic for getting the JS
// app and throwing an error if it's not defined.
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.show_progress_indicator(0.0);
let (finished_tx, finished_rx) = futures::channel::oneshot::channel();
let spinner_progress = futures::stream::unfold(0, |time| async move {
enso_web::sleep(Duration::from_millis(OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS)).await;
let new_time = time + OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS;
if new_time < OPEN_PROJECT_SPINNER_TIME_MS {
let progress = new_time as f32 / OPEN_PROJECT_SPINNER_TIME_MS as f32;
Some((progress, new_time))
} else {
None
}
})
.take_until(finished_rx);
executor::global::spawn(spinner_progress.for_each(|progress| async move {
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.show_progress_indicator(progress);
}));
f().await;
// This fails when the spinner progressed until the end before the function got completed
// and therefore the receiver got dropped, so we'll ignore the result.
let _ = finished_tx.send(());
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.hide_progress_indicator();
}
@ -165,6 +235,13 @@ impl Presenter {
let root_frp = &model.view.frp;
root_frp.switch_view_to_project <+ welcome_view_frp.create_project.constant(());
root_frp.switch_view_to_project <+ welcome_view_frp.open_project.constant(());
eval root_frp.selected_project ([model] (project) {
if let Some(project) = project {
model.close_project();
model.open_project(project.name.to_string());
}
});
}
Self { model, network }.init()
@ -174,7 +251,6 @@ impl Presenter {
fn init(self) -> Self {
self.setup_status_bar_notification_handler();
self.setup_controller_notification_handler();
self.model.clone_ref().setup_and_display_new_project();
executor::global::spawn(self.clone_ref().set_projects_list_on_welcome_screen());
self
}
@ -214,8 +290,7 @@ impl Presenter {
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, stream, move |notification, model| {
match notification {
controller::ide::Notification::NewProjectCreated
| controller::ide::Notification::ProjectOpened =>
controller::ide::Notification::ProjectOpened =>
model.setup_and_display_new_project(),
controller::ide::Notification::ProjectClosed => {
model.close_project();
@ -239,6 +314,11 @@ impl Presenter {
}
}
}
/// Open a project by name or ID. If no project with the given name exists, it will be created.
pub fn open_or_create_project(&self, project: ProjectToOpen) {
self.model.open_or_create_project(project)
}
}

View File

@ -9,8 +9,8 @@ use crate::presenter;
use crate::presenter::graph::ViewNodeId;
use engine_protocol::language_server::ExecutionEnvironment;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::system::js;
use ide_view as view;
use ide_view::project::SearcherParams;
use model::module::NotificationKind;
@ -19,16 +19,6 @@ use model::project::VcsStatus;
// =================
// === Constants ===
// =================
/// We don't know how long the project opening will take, but we still want to show a fake progress
/// indicator for the user. This constant represents a progress percentage that will be displayed.
const OPEN_PROJECT_SPINNER_PROGRESS: f32 = 0.8;
// =============
// === Model ===
// =============
@ -46,7 +36,7 @@ struct Model {
graph: presenter::Graph,
code: presenter::Code,
searcher: RefCell<Option<presenter::Searcher>>,
available_projects: Rc<RefCell<Vec<(ImString, Uuid)>>>,
available_projects: Rc<RefCell<Vec<ProjectMetadata>>>,
}
impl Model {
@ -268,8 +258,6 @@ impl Model {
executor::global::spawn(async move {
if let Ok(api) = controller.manage_projects() {
if let Ok(projects) = api.list_projects().await {
let projects = projects.into_iter();
let projects = projects.map(|p| (p.name.clone().into(), p.id)).collect_vec();
*projects_list.borrow_mut() = projects;
project_list_ready.emit(());
}
@ -277,36 +265,6 @@ impl Model {
})
}
/// User clicked a project in the Open Project dialog. Open it.
fn open_project(&self, id_in_list: &usize) {
let controller = self.ide_controller.clone_ref();
let projects_list = self.available_projects.clone_ref();
let view = self.view.clone_ref();
let status_bar = self.status_bar.clone_ref();
let id = *id_in_list;
executor::global::spawn(async move {
let app = js::app_or_panic();
app.show_progress_indicator(OPEN_PROJECT_SPINNER_PROGRESS);
view.hide_graph_editor();
if let Ok(api) = controller.manage_projects() {
api.close_project();
let uuid = projects_list.borrow().get(id).map(|(_name, uuid)| *uuid);
if let Some(uuid) = uuid {
if let Err(error) = api.open_project(uuid).await {
error!("Error opening project: {error}.");
status_bar.add_event(format!("Error opening project: {error}."));
}
} else {
error!("Project with id {id} not found.");
}
} else {
error!("Project Manager API not available, cannot open project.");
}
app.hide_progress_indicator();
view.show_graph_editor();
})
}
fn execution_environment_changed(
&self,
execution_environment: ide_view::execution_environment_selector::ExecutionEnvironment,
@ -370,28 +328,15 @@ impl Project {
let view = &model.view.frp;
let breadcrumbs = &model.view.graph().model.breadcrumbs;
let graph_view = &model.view.graph().frp;
let project_list = &model.view.project_list();
let project_list = &model.view.project_list().frp;
frp::extend! { network
project_list_ready <- source_();
project_list.grid.reset_entries <+ project_list_ready.map(f_!([model]{
let cols = 1;
let rows = model.available_projects.borrow().len();
(rows, cols)
}));
entry_model <- project_list.grid.model_for_entry_needed.map(f!([model]((row, col)) {
let projects = model.available_projects.borrow();
let project = projects.get(*row);
project.map(|(name, _)| (*row, *col, name.clone_ref()))
})).filter_map(|t| t.clone());
project_list.grid.model_for_entry <+ entry_model;
project_list.project_list <+ project_list_ready.map(
f_!(model.available_projects.borrow().clone())
);
open_project_list <- view.project_list_shown.on_true();
eval_ open_project_list(model.project_list_opened(project_list_ready.clone_ref()));
selected_project <- project_list.grid.entry_selected.filter_map(|e| *e);
eval selected_project(((row, _col)) model.open_project(row));
project_list.grid.select_entry <+ selected_project.constant(None);
eval_ open_project_list (model.project_list_opened(project_list_ready.clone_ref()));
eval view.searcher ([model](params) {
if let Some(params) = params {

View File

@ -133,7 +133,6 @@ impl ide_view::searcher::DocumentationProvider for Action {
Some(doc.unwrap_or_else(|| Self::doc_placeholder_for(&suggestion)))
}
Action::Example(example) => Some(example.documentation_html.clone()),
Action::ProjectManagement(_) => None,
}
}
}

View File

@ -1,11 +1,10 @@
use super::prelude::*;
use crate::config::ProjectToOpen;
use crate::ide;
use crate::controller::ide;
use crate::controller::ide::ManagingProjectAPI;
use crate::transport::test_utils::TestWithMockedTransport;
use engine_protocol::project_manager;
use engine_protocol::project_manager::ProjectName;
use json_rpc::test_util::transport::mock::MockTransport;
use serde_json::json;
use wasm_bindgen_test::wasm_bindgen_test;
@ -28,11 +27,8 @@ fn failure_to_open_project_is_reported() {
fixture.run_test(async move {
let project_manager = Rc::new(project_manager::Client::new(transport));
executor::global::spawn(project_manager.runner());
let name = ProjectName::new_unchecked(crate::constants::DEFAULT_PROJECT_NAME.to_owned());
let project_to_open = ProjectToOpen::Name(name);
let initializer =
ide::initializer::WithProjectManager::new(project_manager, project_to_open);
let result = initializer.initialize_project_model().await;
let ide_controller = ide::Desktop::new(project_manager).unwrap();
let result = ide_controller.create_new_project(None, None).await;
result.expect_err("Error should have been reported.");
});
fixture.when_stalled_send_response(json!({

View File

@ -567,7 +567,7 @@ impl View {
// === Project Dialog ===
eval_ frp.show_project_list (model.show_project_list());
project_chosen <- project_list.grid.entry_selected.constant(());
project_chosen <- project_list.frp.selected_project.constant(());
mouse_down <- scene.mouse.frp_deprecated.down.constant(());
clicked_on_bg <- mouse_down.filter(f_!(scene.mouse.target.get().is_background()));
should_be_closed <- any(frp.hide_project_list,project_chosen,clicked_on_bg);

View File

@ -4,6 +4,7 @@
use crate::prelude::*;
use ensogl::display::shape::*;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::application::frp::API;
use ensogl::application::Application;
@ -196,6 +197,23 @@ mod background {
// ===========
// === FRP ===
// ===========
ensogl::define_endpoints! {
Input {
/// This is a list of projects to choose from.
project_list (Vec<ProjectMetadata>),
}
Output {
/// This is the selected project.
selected_project (Option<ProjectMetadata>),
}
}
// ===================
// === ProjectList ===
// ===================
@ -205,18 +223,19 @@ mod background {
/// This is a list of projects in a nice frame with a title.
#[derive(Clone, CloneRef, Debug)]
pub struct ProjectList {
network: frp::Network,
display_object: display::object::Instance,
background: background::View,
caption: text::Text,
grid: grid_view::scrollable::SelectableGridView<Entry>,
#[allow(missing_docs)]
pub grid: grid_view::scrollable::SelectableGridView<Entry>,
pub frp: Frp,
}
impl ProjectList {
/// Create Project List Component.
pub fn new(app: &Application) -> Self {
let network = frp::Network::new("ProjectList");
let frp = Frp::new();
let network = &frp.network;
let display_object = display::object::Instance::new();
let background = background::View::new();
let caption = app.new_view::<text::Text>();
@ -236,7 +255,7 @@ impl ProjectList {
}
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = Style::from_theme(&network, &style_frp);
let style = Style::from_theme(network, &style_frp);
frp::extend! { network
init <- source::<()>();
@ -274,11 +293,30 @@ impl ProjectList {
grid_x <- grid_width.map(|width| -width / 2.0);
grid_y <- all_with3(&content_size, &bar_height, &paddings, |s,h,p| s.y / 2.0 - *h - *p);
_eval <- all_with(&grid_x, &grid_y, f!((x, y) grid.set_xy(Vector2(*x, *y))));
grid.reset_entries <+ frp.input.project_list.map(|projects| (projects.len(), 1));
grid_model_for_entry <= grid.model_for_entry_needed.map2(
&frp.input.project_list,
|(row, col), projects| {
let project = projects.get(*row)?;
Some((*row, *col, project.name.clone().into()))
}
);
grid.model_for_entry <+ grid_model_for_entry;
frp.source.selected_project <+ grid.entry_selected.map2(
&frp.input.project_list,
|selected_entry, projects| {
let (row, _) = (*selected_entry)?;
projects.get(row).cloned()
}
);
grid.select_entry <+ frp.output.selected_project.filter_map(|s| s.as_ref().map(|_| None));
}
style.init.emit(());
init.emit(());
Self { network, display_object, background, caption, grid }
Self { display_object, background, caption, grid, frp }
}
}

View File

@ -6,6 +6,7 @@
use ensogl::prelude::*;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::application;
use ensogl::application::Application;
@ -38,11 +39,12 @@ pub struct Model {
status_bar: crate::status_bar::View,
welcome_view: crate::welcome_screen::View,
project_view: Rc<CloneCell<Option<crate::project::View>>>,
frp: Frp,
}
impl Model {
/// Constuctor.
pub fn new(app: &Application) -> Self {
pub fn new(app: &Application, frp: &Frp) -> Self {
let app = app.clone_ref();
let display_object = display::object::Instance::new();
let state = Rc::new(CloneCell::new(State::WelcomeScreen));
@ -51,8 +53,9 @@ impl Model {
let welcome_view = app.new_view::<crate::welcome_screen::View>();
let project_view = Rc::new(CloneCell::new(None));
display_object.add_child(&welcome_view);
let frp = frp.clone_ref();
Self { app, display_object, status_bar, welcome_view, project_view, state }
Self { app, display_object, state, status_bar, welcome_view, project_view, frp }
}
/// Switch displayed view from Project View to Welcome Screen. Project View will not be
@ -82,6 +85,10 @@ impl Model {
fn init_project_view(&self) {
if self.project_view.get().is_none() {
let view = self.app.new_view::<crate::project::View>();
let project_list_frp = &view.project_list().frp;
frp::extend! { network
self.frp.source.selected_project <+ project_list_frp.selected_project;
}
self.project_view.set(Some(view));
}
}
@ -101,6 +108,8 @@ ensogl::define_endpoints! {
switch_view_to_welcome_screen(),
}
Output {
/// The selected project in the project list
selected_project (Option<ProjectMetadata>),
}
}
@ -128,8 +137,8 @@ impl Deref for View {
impl View {
/// Constuctor.
pub fn new(app: &Application) -> Self {
let model = Model::new(app);
let frp = Frp::new();
let model = Model::new(app, &frp);
let network = &frp.network;
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let offset_y = style.get_number(ensogl_hardcoded_theme::application::status_bar::offset_y);

View File

@ -1,6 +1,6 @@
# Options intended to be common for all developers.
wasm-size-limit: 15.85 MiB
wasm-size-limit: 15.87 MiB
required-versions:
# NB. The Rust version is pinned in rust-toolchain.toml.

View File

@ -33,7 +33,7 @@ impl TestOnNewProjectControllersOnly {
let initializer = enso_gui::Initializer::new(config);
let error_msg = "Couldn't open project.";
let ide = initializer.initialize_ide_controller().await.expect(error_msg);
ide.manage_projects().unwrap().create_new_project(None).await.unwrap();
ide.manage_projects().unwrap().create_new_project(None, None).await.unwrap();
let project = ide.current_project().unwrap();
Self { _ide: ide, project, _executor: executor }
}

View File

@ -426,10 +426,9 @@ export class App {
/** Show a spinner. The displayed progress is constant. */
showProgressIndicator(progress: number) {
if (this.progressIndicator) {
this.hideProgressIndicator()
if (this.progressIndicator == null) {
this.progressIndicator = new wasm.ProgressIndicator(this.config)
}
this.progressIndicator = new wasm.ProgressIndicator(this.config)
this.progressIndicator.set(progress)
}