Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(183557950): Add ProjectsGrid View for Cloud Dashboard #3857

Merged
merged 47 commits into from
Dec 4, 2022

Conversation

indiv0
Copy link
Contributor

@indiv0 indiv0 commented Nov 7, 2022

Pull Request Description

This PR is a draft PR while I learn EnsoGL. The eventual goal is to implement the projects list portion of the cloud dashboard in this PR. This PR will implement part of https://www.pivotaltracker.com/n/projects/2539513/stories/183557950

Important Notes

This PR is still really rough and contains a lot of hacks & hard-coded values. The FRP usage is also likely to be suboptimal and need fixing.

Checklist

Please include the following checklist in your PR:

  • The documentation has been updated if necessary.
  • All code conforms to the
    Scala,
    Java,
    and
    Rust
    style guides.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed: Enso GUI was tested when built using BOTH
      ./run ide build and ./run ide watch.

@indiv0 indiv0 changed the base branch from develop to wip/wdanilo/min-table-scene November 7, 2022 16:35
@indiv0 indiv0 force-pushed the wip/npekin/ensogl-table-183557950 branch from decf54f to 971da45 Compare November 7, 2022 16:41
@indiv0 indiv0 changed the base branch from wip/wdanilo/min-table-scene to wip/michaelmauderer/cloud_dashboard_skeleton November 7, 2022 16:42
@indiv0 indiv0 force-pushed the wip/npekin/ensogl-table-183557950 branch from 971da45 to 879d7b5 Compare November 7, 2022 16:46
Comment on lines 52 to 62
// =============
// === Error ===
// =============

/// Type alias for a thread-safe boxed [`Error`].
///
/// Convert your error to this type when your error can not be recovered from, or no further context
/// can be added to it.
///
/// [`Error`]: ::std::error::Error
pub type Error = Box<dyn error::Error + Send + Sync>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's refactor this to a common place, pls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 72 to 74
/// This enum shouldn't be used directly by users of this crate, and instead they should interact
/// with the Cloud API via the [`Client`] struct.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep it private then :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 103 to 84
/// HTTP client for the Cloud API.
#[derive(Clone, Debug)]
pub struct Client {
base_url: String,
token: String,
http: reqwest::Client,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the docs about fields here, pls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 119 to 129
/// * `api_gateway_id` - The unique identifier of the Amazon API Gateway that is fronting the
/// Cloud API. This is used to disambiguate between requests to development, staging,
/// production, etc. environments. This is obtained from the Terraform output after the API is
/// deployed.
/// * `aws_region` - The AWS region in which the API is deployed. This is obtained from the
/// Terraform output after the API is deployed.
/// * `token` - The authentication token to use when making requests to the API. This is
/// obtained from the browser storage after the user is logged in.
///
/// [`Client`]: crate::Client
pub fn new(api_gateway_id: &str, aws_region: &str, token: String) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can use real types here instead of &str, I would do it. According to our phone call, this can be done by importing a lib, but please check if the lib does not increase the binary size drastically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 26 to 40
// ====================

/// A response for a successful request to the [`ListProjects`] [`Route`].
///
/// [`ListProjects`]: crate::Route::ListProjects
/// [`Route`]: crate::Route
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[allow(missing_docs)]
pub struct ListProjects {
/// A list of all [`Project`]s that the user has access to. The list may be empty, if the
/// user has access to no [`Project`]s or no [`Project`]s have been created.
///
/// [`Project`]: ::enso_cloud_view::project::Project
pub projects: Vec<view::project::Project>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contain responses, so we can generate here the Routs enum automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// represented by an arrow icon.
text: column.to_string().into(),
disabled: Immutable(true),
override_width: Immutable(if col == 1 && row == 5 { Some(180.0) } else { None }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic numbers


fn get_projects(
client: enso_cloud_http::Client,
input: crate::projects_grid::api::public::Input,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass here set_projects: frp::Source<...> and then you'd need to do set_projects.emit(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I tried this I got:

    Checking debug-scene-cloud-dashboard v0.1.0 (/home/indiv0/src/enso/app/gui/view/debug_scene/cloud-dashboard)
error[E0308]: mismatched types
   --> app/gui/view/debug_scene/cloud-dashboard/src/lib.rs:554:30
    |
554 |         get_projects(client, input.set_projects);
    |         ------------         ^^^^^^^^^^^^^^^^^^ expected struct `enso_frp::SourceData`, found struct `enso_frp::AnyData`
    |         |
    |         arguments to this function are incorrect
    |
    = note: expected struct `enso_frp::stream::WeakNode<enso_frp::SourceData<std::rc::Rc<std::vec::Vec<enso_cloud_view::project::Project>>>>`
               found struct `enso_frp::stream::WeakNode<enso_frp::AnyData<std::rc::Rc<std::vec::Vec<enso_cloud_view::project::Project>>>>`
note: function defined here
   --> app/gui/view/debug_scene/cloud-dashboard/src/lib.rs:581:8
    |
581 |     fn get_projects(
    |        ^^^^^^^^^^^^
582 |         client: enso_cloud_http::Client,
583 |         set_projects: frp::Source<Rc<Vec<view::project::Project>>>,
    |         ----------------------------------------------------------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `debug-scene-cloud-dashboard` due to previous error

How do I convert AnyData-marked Source to a SourceData-marked Source?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see these lines in this file anymore, where are they now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_projects is now in the projects_grid module

Comment on lines 575 to 576
mem::forget(app);
mem::forget(panel);
mem::forget(projects_grid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm these

let panel = app.new_view::<sample_component::View>();
panel.set_size(Vector2::new(200.0, 200.0));
scene.add_child(&panel);
let _main_layer = &app.display.default_scene.layers.node_searcher;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed

@@ -211,13 +563,16 @@ pub fn main() {
theme::builtin::light::register(&app);
theme::builtin::light::enable(&app);

let world = &app.display;
let _world = &app.display;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed

@indiv0 indiv0 changed the base branch from wip/michaelmauderer/cloud_dashboard_skeleton to develop November 28, 2022 01:36
@indiv0 indiv0 force-pushed the wip/npekin/ensogl-table-183557950 branch 2 times, most recently from 302dadf to 136588d Compare November 28, 2022 01:41
@indiv0 indiv0 self-assigned this Nov 28, 2022
@indiv0 indiv0 changed the title [DRAFT] feat(183557950): Add ProjectsGrid View for Cloud Dashboard feat(183557950): Add ProjectsGrid View for Cloud Dashboard Nov 28, 2022
@indiv0
Copy link
Contributor Author

indiv0 commented Nov 28, 2022

  • The StyleWatchFrp changes are incoming

Comment on lines 539 to 547
requested_section <- header_frp.section_info_needed.map(|&(row, col)| {
let sections_size = 2 + col;
let section_start = row - (row % sections_size);
let section_end = section_start + sections_size;
let position = (section_start, col).into();
let model = header_entry_model(position);
(section_start..section_end, col, model)
});
header_frp.section_info <+ requested_section;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@farmaazon this code comes from one of the existing header view examples, but I'm having some trouble understanding what it's doing. What is the logic here supposed to be doing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, the grid view wants to know what to display as a header of the top of the viewport in the current column. Therefore, it asks about information about the section the topmost visible entry is in.

section_info_needed endpoint is exactly that: a question about the section the given entry is in.

IT also assumes that there may be more than one section, therefore it needs to know the span of rows belonging to the returned section (so it knows when to ask about another one).

So, the implementation copied from the example does not make much sense, of course, because you have only one section, so the range should be always 1..=self.rows().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, thank you :)

@indiv0 indiv0 marked this pull request as ready for review November 28, 2022 01:52
Copy link
Contributor

@farmaazon farmaazon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked just the one file with the grid view. The header requesting system shall be fixed (it should actually crash a lot if I understand the code correctly).

Also, there are many TODO or FIXME notes. Each shall be annotated with the pivotal issue, where the thing is planned to be done/fixed.

Comment on lines 246 to 251
/// The row and column coordinates of a cell in the [`ProjectsTable`].
#[derive(Clone, Copy, Debug, Default)]
pub struct Position {
row: Row,
column: Col,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may consider making it a part of the grid-view crate.

But after reading more of your code I see that you usually construct it from row and col only to decompose it again to two separate variables. So perhaps we could stay with (row: Row, col: Col) everywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wdanilo suggested to pass around the struct rather than the pair, to ensure that we're dealing with structured values rather than decomposing pairs, but I'm OK with either approach. @wdanilo what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@farmaazon I suggested that because in the code there was .0 and .1 in many places. It's a little bit hard to understand and its way easier to read code if you have .row and .column. Do you agree with this?

/// rendered.
///
/// [`Project`]: ::enso_cloud_view::project::Project
pub mod projects_table {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is so long I would put it in a separate file.

// The first row is the header row, which is displayed with a different entry model, so
// we should ignore it here.
if row == 0 {
return None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a big function with several returning points. We avoid those as they make the code more obscure. Please refactor it, perhaps making some sub-methods.

let screen_size = Vector2::from(shape);
let margin = Vector2::new(HORIZONTAL_MARGIN, VERTICAL_MARGIN);
let size = screen_size - margin;
self.grid.scroll_frp().resize(size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preferred way for calling FRP inputs is to assign them to FRP outputs. Usually I do it by making the model method returning the event payload (size in this case), and then assign the method output in frp::extend! with <+ operator (see my next comment for example).


let width = shape.width / Columns::LEN as f32;
let size = Vector2(width, ENTRY_HEIGHT);
self.grid.set_entries_size(size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar case: we could make this function return size, and have in FRP something like:
model.grid.set_entries_size <+ scene.frp.shape.map(f!((shape) model.refit_entries_to_shape(shape));

This way frp events have more informative backtraces.

Comment on lines 487 to 488
// FIXME [NP]: How do we stop the grid from calling `model_for_entry` on the
// header row? It should be handled via the later extension to the network.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible. And for the header row, you should just return headers. The "header" API is meant for the headers "pushed down" to be visible even when scrolled down.

ensogl_core::define_endpoints_2! {
Input {
set_projects(Rc<Vec<view::project::Project>>),
model_for_entry(Position, EntryModel),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this output is here? Is it used?

Comment on lines 480 to 568
// FIXME [NP]: Should we be assuming that the `row, col` are always in bounds? If we do so, we
// can avoid having to return `Option` here, but we run the risk of panicking in the event of an
// out of bounds `row, col`. Though this should only happen on implementation error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean, receive with section_info_needed? It should not emit any position outside the last size set by reset_entries or resize. But still, one could just put the wrong size on it.

And as I understand, we still have a rule of avoiding panicking at all cost, so you may assume that the position is valid, but still not panic ;) let's return some dummy entry, for example labeled "INVALID" instead.

Comment on lines 539 to 547
requested_section <- header_frp.section_info_needed.map(|&(row, col)| {
let sections_size = 2 + col;
let section_start = row - (row % sections_size);
let section_end = section_start + sections_size;
let position = (section_start, col).into();
let model = header_entry_model(position);
(section_start..section_end, col, model)
});
header_frp.section_info <+ requested_section;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, the grid view wants to know what to display as a header of the top of the viewport in the current column. Therefore, it asks about information about the section the topmost visible entry is in.

section_info_needed endpoint is exactly that: a question about the section the given entry is in.

IT also assumes that there may be more than one section, therefore it needs to know the span of rows belonging to the returned section (so it knows when to ask about another one).

So, the implementation copied from the example does not make much sense, of course, because you have only one section, so the range should be always 1..=self.rows().

Comment on lines 81 to 92
/// The width of a single entry in the [`ProjectsTable`], in pixels.
const ENTRY_WIDTH: f32 = 130.0;
/// The height of a single entry in the [`ProjectsTable`], in pixels.
const ENTRY_HEIGHT: f32 = 28.0;
/// A placeholder value for the initial height of the viewport. This value is a placeholder
/// because the value must be provided when creating the viewport, but is not used since the
/// viewport is resized to fit the [`ProjectsTable`] when the graphics context is initialized.
const VIEWPORT_HEIGHT: f32 = 300.0;
/// A placeholder value for the initial width of the viewport. This value is a placeholder
/// because the value must be provided when creating the viewport, but is not used since the
/// viewport is resized to fit the [`ProjectsTable`] when the graphics context is initialized.
const VIEWPORT_WIDTH: f32 = 400.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ENTRY_WIDTH ENTRY_HEIGHT and GRID_HEIGHT_RATIO should be taken from the styles.
And isn't the VIEWPORT_WIDTH calculated basing on the scene shape anyway?

aws_region: AwsRegion,
token: AccessToken,
) -> Self {
let base_url = format!("https://{api_gateway_id}.execute-api.{aws_region}.amazonaws.com");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho its better to have base_url as a parameter. We will move from apig generated api addresses in few days (the main will be api.cloud.enso.org so personal ones can be api.npekin.cloud.enso.org). This way you can set url as a env variable

@mwu-tow mwu-tow merged commit bd455ff into develop Dec 4, 2022
@mwu-tow mwu-tow deleted the wip/npekin/ensogl-table-183557950 branch December 4, 2022 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants