From 4ea1880dec47b4df1e6ae1b37ee3bb6fbf23842c Mon Sep 17 00:00:00 2001 From: Nikita Pekin Date: Wed, 18 Jan 2023 10:46:48 +0300 Subject: [PATCH] chore(183909391): Remove existing Cloud dashboard code (#4047) * chore(183909391): Remove Cloud dashboard * update changelog --- CHANGELOG.md | 4 + Cargo.lock | 63 -- app/gui/view/debug_scene/Cargo.toml | 1 - .../debug_scene/cloud-dashboard/Cargo.toml | 29 - .../cloud-dashboard/cloud-http/Cargo.toml | 15 - .../cloud-dashboard/cloud-http/src/lib.rs | 248 ------ .../cloud-http/src/response.rs | 101 --- .../cloud-dashboard/cloud-view/Cargo.toml | 12 - .../cloud-dashboard/cloud-view/src/id.rs | 302 -------- .../cloud-dashboard/cloud-view/src/lib.rs | 108 --- .../cloud-dashboard/cloud-view/src/project.rs | 98 --- .../debug_scene/cloud-dashboard/src/lib.rs | 77 -- .../cloud-dashboard/src/projects_table.rs | 708 ------------------ app/gui/view/debug_scene/src/lib.rs | 1 - 14 files changed, 4 insertions(+), 1763 deletions(-) delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/Cargo.toml delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-http/Cargo.toml delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/lib.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/response.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-view/Cargo.toml delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/id.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/lib.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/project.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/src/lib.rs delete mode 100644 app/gui/view/debug_scene/cloud-dashboard/src/projects_table.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea9454bc1..038bf6a1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,11 +114,15 @@ steps towards migrating the Cloud Dashboard from the existing React (web-only) implementation towards a shared structure that can be used in both the Desktop and Web versions of the IDE. +- [Removed Cloud Dashboard][4047]. The Cloud Dashboard was being rewritten in + EnsoGL but after internal discussion we've decided to rewrite it in React, + with a shared implementation between the Desktop and Web versions of the IDE. - [Added a new component: Dropdown][3985]. A list of selectable labeled entries, suitable for single and multi-select scenarios. [3857]: https://github.com/enso-org/enso/pull/3857 [3985]: https://github.com/enso-org/enso/pull/3985 +[4047]: https://github.com/enso-org/enso/pull/4047 #### Enso Standard Library diff --git a/Cargo.lock b/Cargo.lock index 2d6545d0ec..c3e18c8f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,12 +754,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base-encode" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17bd29f7c70f32e9387f4d4acfa5ea7b7749ef784fb78cf382df97069337b8c" - [[package]] name = "base64" version = "0.9.3" @@ -1562,28 +1556,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" -[[package]] -name = "debug-scene-cloud-dashboard" -version = "0.1.0" -dependencies = [ - "enso-frp", - "enso-profiler", - "enso_cloud_http", - "enso_cloud_view", - "ensogl", - "ensogl-core", - "ensogl-grid-view", - "ensogl-hardcoded-theme", - "ensogl-selector", - "ensogl-text", - "ensogl-text-msdf", - "ide-view-component-list-panel", - "ide-view-graph-editor", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", -] - [[package]] name = "debug-scene-component-list-panel-view" version = "0.1.0" @@ -2059,7 +2031,6 @@ dependencies = [ name = "enso-debug-scene" version = "0.1.0" dependencies = [ - "debug-scene-cloud-dashboard", "debug-scene-component-list-panel-view", "debug-scene-documentation", "debug-scene-icons", @@ -2553,29 +2524,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "enso_cloud_http" -version = "0.1.0" -dependencies = [ - "enso-prelude", - "enso_cloud_view", - "headers", - "http", - "reqwest", - "serde", - "serde_json", -] - -[[package]] -name = "enso_cloud_view" -version = "0.1.0" -dependencies = [ - "enso-prelude", - "serde", - "serde_json", - "svix-ksuid", -] - [[package]] name = "ensogl" version = "0.1.0" @@ -6742,17 +6690,6 @@ dependencies = [ "syn", ] -[[package]] -name = "svix-ksuid" -version = "0.6.0" -source = "git+https://github.com/svix/rust-ksuid#16468002c4da26932ca1004fcae190f543565b78" -dependencies = [ - "base-encode", - "byteorder", - "getrandom 0.2.7", - "time 0.3.14", -] - [[package]] name = "symlink" version = "0.1.0" diff --git a/app/gui/view/debug_scene/Cargo.toml b/app/gui/view/debug_scene/Cargo.toml index 5603d0c1f1..5b2cae492e 100644 --- a/app/gui/view/debug_scene/Cargo.toml +++ b/app/gui/view/debug_scene/Cargo.toml @@ -14,4 +14,3 @@ debug-scene-icons = { path = "icons" } debug-scene-interface = { path = "interface" } debug-scene-text-grid-visualization = { path = "text-grid-visualization" } debug-scene-visualization = { path = "visualization" } -debug-scene-cloud-dashboard = { path = "cloud-dashboard" } diff --git a/app/gui/view/debug_scene/cloud-dashboard/Cargo.toml b/app/gui/view/debug_scene/cloud-dashboard/Cargo.toml deleted file mode 100644 index dce33b3757..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "debug-scene-cloud-dashboard" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -enso_cloud_http = { path = "./cloud-http" } -enso_cloud_view = { path = "./cloud-view" } -ide-view-graph-editor = { path = "../../../../../app/gui/view/graph-editor" } -enso-frp = { path = "../../../../../lib/rust/frp" } -enso-profiler = { path = "../../../../../lib/rust/profiler" } -ensogl-core = { path = "../../../../../lib/rust/ensogl/core" } -ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" } -ensogl-grid-view = { path = "../../../../../lib/rust/ensogl/component/grid-view" } -ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" } -ensogl-text = { path = "../../../../../lib/rust/ensogl/component/text" } -ensogl-text-msdf = { path = "../../../../../lib/rust/ensogl/component/text/src/font/msdf" } -ide-view-component-list-panel = { path = "../../component-browser/component-list-panel" } -wasm-bindgen = { workspace = true } -js-sys = { version = "0.3" } -ensogl = { path = "../../../../../lib/rust/ensogl" } -wasm-bindgen-futures = { version = "0.4.8", default-features = false } - -# Stop wasm-pack from running wasm-opt, because we run it from our build scripts in order to customize options. -[package.metadata.wasm-pack.profile.release] -wasm-opt = false diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/Cargo.toml b/app/gui/view/debug_scene/cloud-dashboard/cloud-http/Cargo.toml deleted file mode 100644 index ea5af48364..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "enso_cloud_http" -version = "0.1.0" -edition = "2021" - -[dependencies] -enso-prelude = { path = "../../../../../../lib/rust/prelude" } -enso_cloud_view = { path = "../cloud-view", default-features = false } -http = { version = "0.2.8", default-features = false } -reqwest = { version = "0.11.12", default-features = false, features = ["json"] } -serde = { version = "1.0.144", default-features = false, features = ["std"] } -headers = { version = "0.3.8", default-features = false } - -[dev-dependencies] -serde_json = "1.0.85" diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/lib.rs b/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/lib.rs deleted file mode 100644 index 81d9b1a5d2..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/lib.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Crate containing types for interacting with the Cloud dashboard. -//! -//! - [`Client`] is used to make requests to the Cloud dashboard API. -//! - Responses returned by the Cloud dashboard API are deserialized from JSON into strongly-typed -//! structs, defined in the [`response`] module. - -// === Standard Linter Configuration === -#![deny(non_ascii_idents)] -#![warn(unsafe_code)] -#![allow(clippy::bool_to_int_with_if)] -#![allow(clippy::let_and_return)] -// === Non-Standard Linter Configuration === -#![deny(keyword_idents)] -#![deny(macro_use_extern_crate)] -#![deny(missing_abi)] -#![deny(pointer_structural_match)] -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(unconditional_recursion)] -#![warn(missing_docs)] -#![warn(absolute_paths_not_starting_with_crate)] -#![warn(elided_lifetimes_in_paths)] -#![warn(explicit_outlives_requirements)] -#![warn(missing_copy_implementations)] -#![warn(missing_debug_implementations)] -#![warn(noop_method_call)] -#![warn(single_use_lifetimes)] -#![warn(trivial_casts)] -#![warn(trivial_numeric_casts)] -#![warn(unused_extern_crates)] -#![warn(unused_import_braces)] -#![warn(unused_lifetimes)] -#![warn(unused_qualifications)] -#![warn(variant_size_differences)] -#![warn(unreachable_pub)] - -use enso_cloud_view::prelude::*; -use enso_prelude::*; - -use headers::authorization; -use response::Route; - - -// ============== -// === Export === -// ============== - -pub mod response; - - - -// ============== -// === Client === -// ============== - -/// Client used to make HTTP requests to the Cloud API. -/// -/// This struct provides convenience functions like [`Route::list_projects`] which let you send HTTP -/// requests to the Cloud API without building HTTP requests manually or dealing with details like -/// adding HTTP headers for authorization, etc. The convenience functions return strongly-typed -/// responses that are automatically deserialized from JSON. -#[derive(Clone, Debug)] -pub struct Client { - /// The URL that Cloud API requests are sent to, minus the relative path to an API endpoint. - /// - /// [`Route`]s appended to this URL as a relative path to form the full URL of a request. Each - /// API endpoint has an associated [`Route`]. - /// - /// This URL is constructed differently for different environments. For example, in production - /// this URL is usually our public domain (e.g., `https://cloud.enso.org`), in staging it is an - /// AWS API Gateway URL (e.g., `https://12345678a.execute-api.us-west-1.amazonaws.com`), and in - /// development it is a local URL (e.g., `http://localhost:8080`). - /// - /// If you are developing against a deployed environment, the AWS API Gateway URL can be - /// looking in the Terraform output logs after a successful deployment. - base_url: reqwest::Url, - /// The JSON Web Token (JWT) used to authenticate requests to our API. - token: AccessToken, - /// Underlying HTTP client used to make requests. - /// - /// This struct wraps the HTTP client and hides the details of how requestts are made, so that - /// we can provide a simpler API to users of this crate. For example, use the - /// [`Client::list_projects`] method to make requests to the [`Route::ListProjects`] endpoint. - http: reqwest::Client, -} - - -// === Main `impl` === - -impl Client { - /// Creates a new instance of the Cloud API [`Client`]. - /// - /// For more info about how the arguments are used, see the documentation of the [`Client`] - /// struct and its fields, or the type documentation for the arguments. - pub fn new(base_url: reqwest::Url, token: AccessToken) -> Result { - let http = reqwest::Client::new(); - Ok(Self { base_url, token, http }) - } - - /// Returns the list of [`Project`]s the user has access to. - /// - /// [`Project`]: ::enso_cloud_view::project::Project - pub async fn list_projects(&self) -> Result { - let route = Route::ListProjects; - let response = self.try_request(route).await?; - let projects = response.json().await?; - Ok(projects) - } -} - - -// === Internal `impl` === - -impl Client { - /// Converts the [`Route`] into an HTTP [`Request`], executes it, and returns the [`Response`]. - /// - /// # Errors - /// - /// Returns an [`Error`] if: - /// - the [`Request`] could not be built, - /// - the [`Request`] could not be executed (e.g., due to a network error), - /// - the [`Response`] did not have a successful (i.e., 2xx) HTTP status code, - /// - /// [`Request`]: ::reqwest::Request - /// [`Response`]: ::reqwest::Response - async fn try_request(&self, route: Route) -> Result { - let method = route.method(); - let relative_path = route.to_string(); - let mut url = self.base_url.clone(); - url.set_path(&relative_path); - - let request = self.http.request(method, url); - let request = request.bearer_auth(&self.token); - let request = request.build()?; - - let mut response = self.http.execute(request).await?; - if !response.status().is_success() { - response = handle_error_response(response).await?; - } - - Ok(response) - } -} - -/// Creates a base URL for the Cloud Dashboard (as described in the documentation of the [`Client`] -/// struct) for an instance of the Cloud Dashboard running on AWS API Gateway. -/// -/// Use this function to connect to the Cloud Dashboard API when running a staging or testing -/// deployment. -pub fn base_url_for_api_gateway( - api_gateway_id: ApiGatewayId, - aws_region: AwsRegion, -) -> Result { - let url = format!("https://{api_gateway_id}.execute-api.{aws_region}.amazonaws.com").parse()?; - Ok(url) -} - -/// Converts an unsuccessful HTTP [`Response`] into an [`Error`], or returns the [`Response`]. -/// -/// This function exists to make user-facing errors for HTTP requests more informative by including -/// the HTTP response body in the error message, where possible. -/// -/// [`Response`]: ::reqwest::Response -async fn handle_error_response(response: reqwest::Response) -> Result { - if let Some(e) = response.error_for_status_ref().err() { - match response.text().await { - Ok(body) => { - let e = format!("Error \"{e:?}\" with error message body: {body}"); - Err(e)? - } - Err(body_error) => { - let e = format!("Failed to get error response body: \"{e:?}\"; {body_error}"); - Err(e)? - } - } - } else { - Ok(response) - } -} - - - -// ==================== -// === ApiGatewayId === -// ==================== - -/// Identifier of which Cloud API deployment a request is intended to go to (e.g., a development, -/// staging, production, or other deployment). -/// -/// This value is the AWS API Gateway ID of the deployment. The API Gateway ID is a unique -/// identifier generated by AWS when the API is deployed for the first time. It is always available -/// in the output logs of a successful deployment via our Terraform scripts. It can also be found by -/// manually navigating to the API Gateway in the AWS web UI. -#[derive(Clone, Debug, Display)] -pub struct ApiGatewayId(pub String); - - - -// ================= -// === AwsRegion === -// ================= - -/// AWS region in which the Cloud API is deployed. -/// -/// This corresponds to the region of the AWS API Gateway identified by the [`ApiGatewayId`] used -/// when making requests to the Cloud API via the [`Client`]. This value can be found in our -/// Terraform output logs after a successful deployment of the Cloud API. -#[derive(Clone, Copy, Debug, Display)] -#[allow(missing_docs)] -pub enum AwsRegion { - #[display(fmt = "eu-west-1")] - EuWest1, -} - - - -// =================== -// === AccessToken === -// =================== - -/// The JSON Web Token (JWT) used to authenticate requests to our API. -/// -/// Requests without a token are rejected as unauthorized. A token determines which Cloud user is -/// making a request, what permissions they have, and what resources they can access. -/// -/// The token can be obtained from the browser storage after the user logs in. Do so via the APIs -/// provided by the [`aws-amplify`] JavaScript library. -/// -/// [`aws-amplify`]: https://docs.amplify.aws/lib/auth/getting-started/q/platform/js/ -#[derive(Clone, Debug, Display)] -#[display(fmt = "{}", "bearer.token()")] -pub struct AccessToken { - bearer: headers::Authorization, -} - -impl AccessToken { - /// Creates a new [`AccessToken`] from the given [`str`] slice. - /// - /// # Errors - /// - /// Returns an error if the value of the [`str`] slice is not a properly-formatted JSON Web - /// Token (JWT). Note that we do not return an error if the token is expired, etc. We only check - /// that it is properly formatted. - pub fn new(token: &str) -> Result { - let bearer = headers::Authorization::bearer(token)?; - let token = Self { bearer }; - Ok(token) - } -} diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/response.rs b/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/response.rs deleted file mode 100644 index 92404a9376..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-http/src/response.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Module containing response types for the Cloud HTTP API. -//! -//! These types are used both on the server and the client side. On the server side, our Lambdas -//! return these types in response to requests. On the client side, we deserialize HTTP response -//! bodies to these types. By using identical models on both the server and client side, we avoid -//! mismatches between API specification and implementation. This does, however, mean that we have -//! to upgrade server and client side deployments in lockstep. - -use enso_prelude::*; - - - -// ============================================ -// === `declare_routes_and_responses` Macro === -// ============================================ - -/// This is a macro that is used to define the [`Route`]s to our Cloud API endpoints and the -/// responses returned by the Cloud API for requests to those endpoints. -/// -/// The macro exists to keep the names of the [`Route`]s and their response structs in sync, rather -/// than for any code generation purposes. This is because each [`Route`] has a unique response type -/// and structure, so we can't use the macro to reduce code duplication. -macro_rules! declare_routes_and_responses { - ( - $list_projects:ident - ) => { - // ============= - // === Route === - // ============= - - /// A combination of the HTTP method and the relative URL path to an endpoint of our Cloud - /// API. - /// - /// Instead of constructing HTTP requests manually using this enum, use the convenience - /// methods on [`Client`] to send requests to the Cloud API instead -- it's simpler. - /// - /// Each variant in this enum corresponds to an available endpoint in our Cloud API. The - /// [`Display`] impl of this enum provides the relative path to the endpoint. When combined - /// with the base URL of our Cloud API (see [`Client`] for details), this becomes the full - /// URL that requests should be sent to. The [`Route::method`] method provides the HTTP - /// method that the request should be sent as. - #[derive(Clone, Copy, Debug, Display)] - #[allow(missing_docs)] - pub(crate) enum Route { - /// Endpoint for listing the user's projects. The response type for this endpoint is - /// [`response::project::$list_projects`]. - #[display(fmt = "/projects")] - $list_projects, - } - - - // === Main `impl` === - - impl Route { - /// Returns the [`http::Method`] used to interact with this route. - /// - /// [`http::Method`]: ::http::Method - pub(crate) fn method(&self) -> http::Method { - match self { - Self::$list_projects => http::Method::GET, - } - } - } - - - - // =============== - // === Project === - // =============== - - /// Module containing response types for [`Project`]-related [`Route`]s. - /// - /// [`Project`]: ::enso_cloud_view::project::Project - pub mod project { - use enso_cloud_view as view; - - - - // ==================== - // === ListProjects === - // ==================== - - /// A response for a successful request to the [`ListProjects`] [`Route`]. - /// - /// [`ListProjects`]: crate::Route::$list_projects - /// [`Route`]: crate::Route - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] - #[allow(missing_docs)] - pub struct $list_projects { - /// 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, - } - } - }; -} - -declare_routes_and_responses!(ListProjects); diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/Cargo.toml b/app/gui/view/debug_scene/cloud-dashboard/cloud-view/Cargo.toml deleted file mode 100644 index c79a60d139..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "enso_cloud_view" -version = "0.1.0" -edition = "2021" - -[dependencies] -enso-prelude = { path = "../../../../../../lib/rust/prelude" } -serde = { version = "1.0.144", features = ["derive"] } -svix-ksuid = { git = "https://github.com/svix/rust-ksuid" } - -[dev-dependencies] -serde_json = "1.0.85" diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/id.rs b/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/id.rs deleted file mode 100644 index db25cbd355..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/id.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Module containing an implementation of a unique identifier type. -//! -//! See [`Id`] for details on identifiers in general, and [`IdVariant`] for instructions on how to -//! implement new identifier variants (e.g., [`ProjectId`], [`OrganizationId`]). - -use enso_prelude::*; - -use serde::de; - - - -// ================= -// === Constants === -// ================= - -/// When manually implementing [`Deserialize`] for a struct, [`serde`] requires that we provide a -/// user-facing name for the struct being deserialized, for error messages. -/// -/// This constant provides the user-facing name of the [`Id`] struct for deserialization error -/// messages. -/// -/// [`Deserialize`]: ::serde::de::Deserialize -const ID_STRUCT_NAME: &str = stringify!(Id); -/// Separator used in string representations of [`Id`]s. See [`Id`] for details. -pub const ID_PREFIX_SEPARATOR: &str = "-"; - - - -// ====================== -// === OrganizationId === -// ====================== - -/// Unique [`Id`] for an organization. -pub type OrganizationId = Id; - - - -// ================= -// === ProjectId === -// ================= - -/// Unique [`Id`] for a [`Project`]. -/// -/// [`Project`]: crate::project::Project -pub type ProjectId = Id; - - - -// ================= -// === IdVariant === -// ================= - -/// Our [`Id`] struct provides a convenient implementation of a unique identifier, but it is -/// generic. We want our identifiers to be type-safe (i.e., so we don't accidentally use a -/// [`ProjectId`] where we intended to use an [`OrganizationId`]), so our [`Id`] struct has a marker -/// field to keep track of the type of the [`Id`]. Any struct implementing this [`IdVariant`] trait -/// can thus be used as a marker, by passing it as a type parameter to [`Id::`] (e.g., -/// [`Id::`] becomes a [`Project`] identifier). -/// -/// By implementing this trait on a struct, you are creating a new variant of identifier marker. An -/// [`Id`] parameterized with this marker will then have all the useful methods and traits -/// implemented by the [`Id`] struct available to it. For example, your [`Id`] variant will have -/// [`Display`], [`Serialize`], etc. -/// -/// To keep things simple, we recommend that you implement this trait on an empty newtype struct ( -/// see [`ProjectIdVariant`] for an example). Then provide a type alias for an [`Id::`] -/// parameterized with said struct (see [`ProjectId`] for an example). Then use only the type alias. -/// -/// [`Serialize`]: ::serde::Serialize -/// [`Project`]: crate::project::Project -pub trait IdVariant { - /// Prefix applied to serialized representations of the [`Id`]. - /// - /// For example, the [`ProjectId`] type has a prefix of `"proj"`, so the serialized - /// representation of a [`ProjectId`] will look like `"proj-27xJM00p8jWoL2qByTo6tQfciWC"`. - const PREFIX: &'static str; -} - - -// === Trait `impl`s === - -// Implement IdVariant for Id itself so that the associated items of the marker type are -// available through Id itself (i.e., so you can do `ProjectId::PREFIX` rather than doing -// `ProjectIdVariant::PREFIX`). -impl IdVariant for Id -where T: IdVariant -{ - const PREFIX: &'static str = T::PREFIX; -} - - - -// ============================= -// === OrganizationIdVariant === -// ============================= - -/// IdVariant struct that indicates an identifier is for an organization. -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] -pub struct OrganizationIdVariant(PhantomData<()>); - - -// === Trait `impl`s === - -impl IdVariant for OrganizationIdVariant { - const PREFIX: &'static str = "org"; -} - - - -// ======================== -// === ProjectIdVariant === -// ======================== - -/// Marker struct that indicates an identifier is for a [`Project`]. -/// -/// [`Project`]: crate::project::Project -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] -pub struct ProjectIdVariant(PhantomData<()>); - - -// === Trait `impl`s === - -impl IdVariant for ProjectIdVariant { - const PREFIX: &'static str = "proj"; -} - - - -// ========== -// === Id === -// ========== - -/// An identifer that is used to uniquely identify entities in the Cloud backend (e.g., -/// [`Project`]s). -/// -/// Internally, this is a [K-Sortable Unique IDentifier] (KSUID). KSUIDs are a variant of unique -/// identifier that have some nice properties that are useful to us. For example, KSUIDs are -/// naturally ordered by generation time because they incorporate a timestamp as part of the -/// identifier. This is desirable because it means that entities are stored in our database sorted -/// by their creation time, which means we don't have to sort our entities after fetching them. -/// -/// Example: -/// ``` -/// # use enso_prelude::*; -/// let id = enso_cloud_view::id::ProjectId::from_str("proj-27xJM00p8jWoL2qByTo6tQfciWC").unwrap(); -/// ``` -/// -/// An [`Id`] string contains: the type of the entity this identifier is for (e.g. -/// [`ProjectId::PREFIX`] for [`Project`], [`OrganizationId::PREFIX`] for organization, etc.), -/// followed by the [`ID_PREFIX_SEPARATOR`], then the base62-encoded KSUID. -/// -/// Using KSUIDs for identifiers gives several useful properties: -/// 1. Naturally ordered by generation time; -/// 2. Collision-free, coordination-free, dependency-free; -/// 3. Highly portable representations. -/// -/// [K-Sortable Unique IDentifier]: https://github.com/segmentio/ksuid#what-is-a-ksuid -/// [`Project`]: crate::project::Project -#[derive(Clone, Copy, Derivative, Display)] -#[display(bound = "T: IdVariant")] -#[display(fmt = "{}{}{}", "T::PREFIX", "ID_PREFIX_SEPARATOR", "ksuid")] -#[derivative(PartialEq, PartialOrd, Eq, Ord)] -pub struct Id { - /// The inner [`svix_ksuid::Ksuid`] value for this identifier. - pub ksuid: svix_ksuid::Ksuid, - /// Denotes the target entity type for this identifier; for internal use. - #[derivative(PartialEq = "ignore", PartialOrd = "ignore", Ord = "ignore")] - pub marker: PhantomData, -} - - -// === Trait `impl`s` === - -impl serde::Serialize for Id -where Id: Display -{ - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - serializer.serialize_newtype_struct(ID_STRUCT_NAME, &self.to_string()) - } -} - -impl FromStr for Id -where T: IdVariant -{ - type Err = crate::Error; - - fn from_str(s: &str) -> Result { - let ksuid_str = match s.split_twice(T::PREFIX, ID_PREFIX_SEPARATOR) { - Some(("", "", s)) => s, - _ => Err(format!("\"{s}\" is not an {ID_STRUCT_NAME}."))?, - }; - let ksuid = svix_ksuid::Ksuid::from_str(ksuid_str) - .map_err(|_| format!("\"{ksuid_str}\" is not a Ksuid."))?; - let marker = PhantomData; - Ok(Self { ksuid, marker }) - } -} - -/// This implementation is provided manually because [`Id`] is a dash-separated string, which -/// [`serde`] can't derive [`Deserialize`] for. By manually implementing [`Deserialize`], we can -/// delegate deserialization to our [`FromStr`] implementation which can parse an [`Id`] from a -/// [`String`] by splitting it into its [`PREFIX`] and [`Ksuid`] components. -/// -/// [`Deserialize`]: ::serde::de::Deserialize -/// [`PREFIX`]: crate::id::IdVariant::PREFIX -/// [`Ksuid`]: ::svix_ksuid::Ksuid -impl<'a, T> serde::Deserialize<'a> for Id -where Id: FromStr -{ - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'a> { - struct IdVisitor(PhantomData); - - impl<'a, T> de::Visitor<'a> for IdVisitor - where Id: FromStr - { - type Value = Id; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("identifier") - } - - fn visit_newtype_struct(self, deserializer: D) -> Result - where D: serde::Deserializer<'a> { - deserializer.deserialize_any(IdVisitor(PhantomData)) - } - - fn visit_str(self, v: &str) -> Result - where E: de::Error { - v.parse().map_err(|_| { - let unexpected = de::Unexpected::Str(v); - de::Error::invalid_value(unexpected, &"identifier string") - }) - } - } - - deserializer.deserialize_any(IdVisitor(PhantomData)) - } -} - -impl Debug for Id -where T: IdVariant -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple(ID_STRUCT_NAME).field(&format_args!("{}", self)).finish() - } -} - - - -// ============= -// === Tests === -// ============= - -#[cfg(test)] -mod test { - use enso_prelude::*; - - use crate::id; - - #[test] - fn test_id_from_str() { - fn check(id_str: &str, expected_str: &str) { - let result = id::OrganizationId::from_str(id_str); - let result = result.map(|v| v.to_string()); - let result = result.map_err(|e| e.to_string()); - let debug_str = format!("{:?}", result); - assert_eq!(debug_str, expected_str); - } - - check("not_a_valid_id", r#"Err("\"not_a_valid_id\" is not an Id.")"#); - check("org-", r#"Err("\"\" is not a Ksuid.")"#); - check("org-27xJM00p8jWoL2qByTo6tQfciWC", r#"Ok("org-27xJM00p8jWoL2qByTo6tQfciWC")"#); - } - - #[test] - fn test_serialize_id() { - fn check(id_str: &str, expected_str: &str) { - let id = id::OrganizationId::from_str(id_str).unwrap(); - let result = serde_json::to_string(&id); - let debug_str = format!("{:?}", result); - assert_eq!(debug_str, expected_str); - } - - check("org-27xJM00p8jWoL2qByTo6tQfciWC", r#"Ok("\"org-27xJM00p8jWoL2qByTo6tQfciWC\"")"#); - } - - #[test] - fn test_deserialize_id() { - fn check(id_str: &str, expected_str: &str) { - let result = serde_json::from_str::(id_str); - let result = result.map(|v| v.to_string()); - let debug_str = format!("{:?}", result); - assert_eq!(debug_str, expected_str); - } - - check(r#""org-27xJM00p8jWoL2qByTo6tQfciWC""#, r#"Ok("org-27xJM00p8jWoL2qByTo6tQfciWC")"#); - } -} diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/lib.rs b/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/lib.rs deleted file mode 100644 index 8c1da383f9..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Crate containing types defining the API used by the Cloud dashboard. -//! -//! The types defined in this crate are slimmed down versions of the types defined in the private -//! `enso_cloud_lambdas` crate. The reason this crate exists, rather than both the Cloud frontend -//! and Cloud backend using those types is because they contain implementation details that should -//! not be user-visible for security reasons, or aren't necessary for the Cloud frontend to operate. -//! Thus, this crate contains representations of the same types that exist in `enso_cloud_lambdas`, -//! but with only the minimum information necessary. The full types in `enso_cloud_lambdas` are then -//! built on top of these definitions. -//! -//! The types in this crate are used by the: -//! - **Cloud frontend** when *requests* are send to the Cloud backend, -//! - **Cloud backend** when *requests* are deserialized prior to being handled, -//! - **Cloud backend** when *responses* are serialized prior to being returned to the Cloud -//! frontend, -//! - **Cloud frontend** when *responses* are deserialized. -//! -//! Defining the types in this shared crate keeps our API *definition* and *usage* consistent in -//! both the frontend and the backend. - -// === Standard Linter Configuration === -#![deny(non_ascii_idents)] -#![warn(unsafe_code)] -#![allow(clippy::bool_to_int_with_if)] -#![allow(clippy::let_and_return)] -// === Non-Standard Linter Configuration === -#![deny(keyword_idents)] -#![deny(macro_use_extern_crate)] -#![deny(missing_abi)] -#![deny(pointer_structural_match)] -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(unconditional_recursion)] -#![warn(missing_docs)] -#![warn(absolute_paths_not_starting_with_crate)] -#![warn(elided_lifetimes_in_paths)] -#![warn(explicit_outlives_requirements)] -#![warn(missing_copy_implementations)] -#![warn(missing_debug_implementations)] -#![warn(noop_method_call)] -#![warn(single_use_lifetimes)] -#![warn(trivial_casts)] -#![warn(trivial_numeric_casts)] -#![warn(unused_extern_crates)] -#![warn(unused_import_braces)] -#![warn(unused_lifetimes)] -#![warn(unused_qualifications)] -#![warn(variant_size_differences)] -#![warn(unreachable_pub)] - -use std::error; - - -// ============== -// === Export === -// ============== - -pub mod id; -pub mod project; - - - -// =============== -// === Prelude === -// =============== - -/// A module to let consumers of this crate import common Cloud-related types. -/// -/// For example, the [`Error`] type is used in almost all Cloud-related crates so it is desirable -/// to always have it in scope. Glob-importing this module makes this easy to do. -pub mod prelude { - pub use crate::Error; -} - - - -// ============= -// === 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; - - - -// ============================================== -// === `separate_pascal_case_str_with_spaces` === -// ============================================== - -/// Takes a [`&str`] containing a Pascal-case identifier (e.g. `"UsagePlan"`) and -/// returns a [`String`] with the individual words of the identifier separated by a -/// single whitespace (e.g., `"Usage Plan"`). -pub fn separate_pascal_case_str_with_spaces(s: &str) -> String { - let mut new = String::with_capacity(s.len()); - let mut first_char = true; - for c in s.chars() { - if c.is_uppercase() && !first_char { - new.push(' '); - } - new.push(c); - first_char = false; - } - new -} diff --git a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/project.rs b/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/project.rs deleted file mode 100644 index 20931e89f3..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/cloud-view/src/project.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Model definitions for [`Project`] and related types. - -use enso_prelude::*; - -use crate::id; - - - -// ================ -// === StateTag === -// ================ - -/// Enum representing what state a Cloud backend [`Project`] is in. -/// -/// A [`Project`]'s state has a lot of implementation details that are irrelevant to the Cloud -/// frontend. So this enum contains only the tag used to discriminate between different variants. -/// This is returned in API responses from the backend where the content of the state is irrelevant -/// or should not be public, and we're just interested in *which* state the [`Project`] is in. -#[derive(Debug, serde::Deserialize, Eq, serde::Serialize, Clone, Copy, PartialEq)] -#[serde(tag = "type")] -#[allow(missing_docs)] -pub enum StateTag { - New, - Created, - OpenInProgress, - Opened, - Closed, -} - - -// === Trait `impl`s === - -impl Display for StateTag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let debug_str = format!("{:?}", self); - let display_str = crate::separate_pascal_case_str_with_spaces(&debug_str); - write!(f, "{display_str}") - } -} - - - -// =========== -// === Ami === -// =========== - -/// Struct that describes Amazon Machine Image identifier. -#[derive(Clone, Debug, serde::Serialize, Into, Display, PartialEq, Eq)] -pub struct Ami(pub String); - - -// === Trait `impl`s === - -impl FromStr for Ami { - type Err = crate::Error; - - fn from_str(ami: &str) -> Result { - /// A valid ami ID starts with `ami-` prefix. - const AMI_PREFIX: &str = "ami-"; - - if !ami.starts_with(AMI_PREFIX) { - return Err(format!("Bad Ami format: {}", ami))?; - }; - Ok(Self(ami.to_string())) - } -} - -impl<'a> serde::Deserialize<'a> for Ami { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'a> { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(serde::de::Error::custom) - } -} - - - -// =============== -// === Project === -// =============== - -/// A representation of a project in the Cloud IDE. -/// -/// This is returned as part of responses to cloud API endpoints. It simplified to avoid leaking -/// implementation details via the HTTP API. An example of the implementation details we wish to -/// avoid leaking is the ID of the internal EC2 instance that the project is running on. Users don't -/// need to know these details, as we proxy requests to the correct instance through our API -/// gateway. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -#[allow(missing_docs)] -pub struct Project { - pub organization_id: id::OrganizationId, - pub project_id: id::ProjectId, - pub name: String, - pub state: StateTag, - pub ami: Option, -} diff --git a/app/gui/view/debug_scene/cloud-dashboard/src/lib.rs b/app/gui/view/debug_scene/cloud-dashboard/src/lib.rs deleted file mode 100644 index 701d1f54b6..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Cloud dashboard scene showing a table of projects. - -#![recursion_limit = "512"] -// === Features === -#![allow(incomplete_features)] -#![feature(negative_impls)] -#![feature(associated_type_defaults)] -#![feature(cell_update)] -#![feature(const_type_id)] -#![feature(drain_filter)] -#![feature(entry_insert)] -#![feature(fn_traits)] -#![feature(marker_trait_attr)] -#![feature(specialization)] -#![feature(trait_alias)] -#![feature(type_alias_impl_trait)] -#![feature(unboxed_closures)] -#![feature(trace_macros)] -#![feature(const_trait_impl)] -#![feature(slice_as_chunks)] -#![feature(variant_count)] -// === Standard Linter Configuration === -#![deny(non_ascii_idents)] -#![warn(unsafe_code)] -#![allow(clippy::bool_to_int_with_if)] -#![allow(clippy::let_and_return)] -// === Non-Standard Linter Configuration === -#![allow(clippy::option_map_unit_fn)] -#![allow(clippy::precedence)] -#![allow(dead_code)] -#![deny(unconditional_recursion)] -#![warn(missing_copy_implementations)] -#![warn(missing_debug_implementations)] -#![warn(missing_docs)] -#![warn(trivial_casts)] -#![warn(trivial_numeric_casts)] -#![warn(unused_import_braces)] -#![warn(unused_qualifications)] - -use ensogl::prelude::*; -use wasm_bindgen::prelude::*; - -use ensogl_core::application::Application; -use ensogl_core::display::object::ObjectOps; -use ensogl_hardcoded_theme as theme; - - -// ============== -// === Export === -// ============== - -pub mod projects_table; - - - -// =================== -// === Entry Point === -// =================== - -/// The example entry point. -#[entry_point] -#[allow(dead_code)] -pub fn main() { - ensogl_text_msdf::run_once_initialized(|| { - let app = Application::new("root"); - theme::builtin::light::register(&app); - theme::builtin::light::enable(&app); - - let scene = &app.display.default_scene; - - let projects_table = app.new_view::(); - let projects_table = projects_table.init().expect("Failed to initialize projects table."); - scene.add_child(&projects_table); - - std::mem::forget(projects_table); - }) -} diff --git a/app/gui/view/debug_scene/cloud-dashboard/src/projects_table.rs b/app/gui/view/debug_scene/cloud-dashboard/src/projects_table.rs deleted file mode 100644 index f892b28368..0000000000 --- a/app/gui/view/debug_scene/cloud-dashboard/src/projects_table.rs +++ /dev/null @@ -1,708 +0,0 @@ -//! Module containing the [`ProjectsTable`], which is used to display a list of [`Project`]s in the -//! Cloud dashboard. -//! -//! The list of [`Project`]s, as rendered, contains information about the state of the [`Project`], -//! the data associated with the [`Project`], the permissions assigned to it, etc. Buttons for -//! interacting with the [`Project`]s (e.g., starting and stopping the [`Project`]s) are also -//! rendered. -//! -//! [`Project`]: ::enso_cloud_view::project::Project - -use enso_cloud_view::prelude::*; -use ensogl::prelude::*; - -use enso_cloud_view as view; -use ensogl::application; -use ensogl::data::color; -use ensogl_core::application::Application; -use ensogl_core::display; -use ensogl_core::frp; -use ensogl_grid_view::simple::EntryModel; -use ensogl_grid_view::Col; -use ensogl_grid_view::Row; - - - -// ================= -// === Constants === -// ================= - -/// The width of a single entry in the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const ENTRY_WIDTH: f32 = 130.0; -/// The height of a single entry in the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const ENTRY_HEIGHT: f32 = 28.0; -/// The grid is intended to take up 20% of the viewport height, expressed as a fraction; -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const GRID_HEIGHT_RATIO: f32 = 0.2; -/// The top margin of the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const TOP_MARGIN: f32 = 10f32; -/// The bottom margin of the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const BOTTOM_MARGIN: f32 = 10f32; -/// The left margin of the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const LEFT_MARGIN: f32 = 10f32; -/// The right margin of the [`ProjectsTable`], in pixels. -// FIXME [NP]: https://www.pivotaltracker.com/story/show/183909467 -// Move the style constants to StyleWatchFrp variables. -const RIGHT_MARGIN: f32 = 10f32; -/// The combined horizontal margin of the [`ProjectsTable`], in pixels. -const HORIZONTAL_MARGIN: f32 = LEFT_MARGIN + RIGHT_MARGIN; -/// The combined vertical margin of the [`ProjectsTable`], in pixels. -const VERTICAL_MARGIN: f32 = TOP_MARGIN + BOTTOM_MARGIN; - -/// In the future, we want to display the last modification time of a [`Project`]. For now, the API -/// does not provide that information so we use a placeholder value. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909494 -// Remove the unused columns from the `"Projects" Table`. -const LAST_MODIFIED: &str = "2022-10-08 13:30"; -/// In the future, we want to display icons for users/groups with access to the [`Project`] and -/// their corresponding permissions (e.g., read/write/execute). -/// -/// [`Project`]: ::enso_cloud_view::project::Project -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909494 -// Remove the unused columns from the `"Projects" Table`. -const SHARED_WITH: &str = "Baron von Münchhausen (Read/Write)"; -/// In the future, we want to display icons for the [`Project`]'s labels. Labels may be user-defined -/// or system-defined (e.g., labels indicating high resource usage or outdated version). -/// -/// [`Project`]: ::enso_cloud_view::project::Project -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909420 -// `"Home Screen" User` can see a `"running" Label` for any currently running `Project`s. -const LABELS: &str = "(!) outdated version"; -/// In the future, we want to display icons for datasets associated with the [`Project`], as well as -/// what permissions are set on the dataset, from the user's perspective. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909494 -// Remove the unused columns from the `"Projects" Table`. -const DATA_ACCESS: &str = "./user_data"; -/// In the future, we want to display which usage plan the [`Project`] is configured for (e.g., -/// "Interactive" or cron-style, etc.). -/// -/// [`Project`]: ::enso_cloud_view::project::Project -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909494 -// Remove the unused columns from the `"Projects" Table`. -const USAGE_PLAN: &str = "Interactive"; - -/// ID of the Amazon API Gateway serving the Cloud API. -const API_GATEWAY_ID: &str = "7aqkn3tnbc"; -/// The AWS region in which the Amazon API Gateway referenced by [`API_GATEWAY_ID`] is deployed. -/// -/// [`API_GATEWAY_ID`]: crate::projects_table::API_GATEWAY_ID -const AWS_REGION: enso_cloud_http::AwsRegion = enso_cloud_http::AwsRegion::EuWest1; -/// Access token used to authenticate requests to the Cloud API. -// TODO [NP]: https://www.pivotaltracker.com/story/show/183909294 -// `"Home Screen" User` is authenticated using the browser-stored `Access Token`. -const TOKEN: &str = "eyJraWQiOiJiVjd1ZExrWTkxU2lVUWRpWVhDSVByRitoSTRYVHlOYTQ2TXhJRDlmY3EwPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI0YjBhMzExNy1hNmI5LTRkYmYtOGEyMC0wZGZmNWE4NjA2ZGYiLCJjb2duaXRvOmdyb3VwcyI6WyJldS13ZXN0LTFfOUt5Y3UyU2JEX0dvb2dsZSJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtd2VzdC0xLmFtYXpvbmF3cy5jb21cL2V1LXdlc3QtMV85S3ljdTJTYkQiLCJ2ZXJzaW9uIjoyLCJjbGllbnRfaWQiOiI0ajliZnM4ZTc0MTVlcmY4MmwxMjl2MHFoZSIsIm9yaWdpbl9qdGkiOiI4MDYxMDkxMS01OGVlLTRjYzctYjU0Ny1lNmZjNzY2OTMwNmYiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6Im9wZW5pZCBlbWFpbCIsImF1dGhfdGltZSI6MTY2ODUyNzkwNiwiZXhwIjoxNjY5NjczOTAyLCJpYXQiOjE2Njk2NzAzMDIsImp0aSI6IjUyYzE3NzkzLTA2YmYtNDkxYi1iYmExLWZjMTI0MTUxZTQ1NSIsInVzZXJuYW1lIjoiR29vZ2xlXzEwNDA4MDA2MTY5NTg0NDYwMDk2OCJ9.KsFezaQrtDOJiy-edAJEtWH0hXE5SfBQGczazgvXUGMY4xluKZMle_Q0x2myFxu_1-eb8ND8M-2nOU9Kz09hKLrxqiJI6BRZrAlNKk0B6c2mtJM-OXS5Nyvs83xTfjHJPnBOPD6qadDZPx82FZkiI99HCiSEn1s1lyMy9-GPAHcIFa-PMDtrf0mzAbJUnCCfJPjxz49003FKTThOLCBVvjrgiTCCYIzvF96ERIVL2bMJ08bQEIwsbHxFvONgHh1yjGFjYDT0JC6OXZraQ3wFQFOnCwL33sijtn8_p9b3f22UttbaOFg03V-I7tSx5YUTiVDJHWEKYqlmm_fHUFWlVw"; - - - -// ================================== -// === define_columns_enum! macro === -// ================================== - -/// A helper macro for defining the [`Columns`] enum. -/// -/// The variants of this enum are defined only at the location where the enum is invoked. This -/// ensures that all the functions implemented on this enum always cover all the variants of the -/// enum, and are defined programmatically rather than manually. This helps avoid drift and bugs due -/// to programmer error. -macro_rules! define_columns_enum { - ($($column_name:ident),*) => { - // =============== - // === Columns === - // =============== - - /// Names of the columns that we want to display in the grid view. The [`Display`] - /// implementation for this enum represents the user-facing values shown in the grid - /// headers. - /// - /// These columns correspond roughly, but not exactly, to the fields of the [`Project`] - /// struct. The differences are mainly in the fact that we display some properties of the - /// [`Project`] as icons (e.g., the [`state`]). - /// - /// [`Project`]: ::enso_cloud_view::project::Project - /// [`state`]: ::enso_cloud_view::project::Project.state - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - #[allow(missing_docs)] - pub enum Columns { - $($column_name,)+ - } - - - // === Main `impl` === - - impl Columns { - /// Number of columns that we want to display in the [`ProjectsTable`] view. This - /// corresponds to the number of variants in this enum, since each variant is a column - /// of data that we want to display. - const LEN: usize = mem::variant_count::(); - - /// Returns the [`Columns`] variant corresponding to the given discriminant value. - /// - /// If the discriminant is out of bounds, returns `None`. - /// - /// Example: - /// ```rust - /// # use debug_scene_cloud_dashboard::projects_table::Columns; - /// assert_eq!(Columns::from_discriminant(0), Some(Columns::Projects)); - /// assert_eq!(Columns::from_discriminant(1), Some(Columns::LastModified)); - /// ``` - pub fn from_discriminant(discriminant: usize) -> Option { - // It is non-trivial to write a macro that can use `match` for this purpose, because - // a macro can't return both the pattern and the expression parts of a `match` arm - // (i.e., `x => y`). So instead we use an iterator over the values `0..` combined - // with `if` statements. This is functionally equivalent and is optimized by the - // compiler to the same instructions. - let mut i = 0..; - $(if discriminant == i.next().unwrap() { Some(Self::$column_name) } else)* - { unreachable!() } - } - } - }; -} - -define_columns_enum!(Projects, LastModified, SharedWith, Labels, DataAccess, UsagePlan); - - -// === Trait `impl`s === - -impl Display for Columns { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let debug_str = format!("{:?}", self); - let display_str = view::separate_pascal_case_str_with_spaces(&debug_str); - write!(f, "{display_str}") - } -} - - - -// ===================== -// === ProjectsTable === -// ===================== - -/// A table of [`Project`]s displayed in the Cloud dashboard. -/// -/// The table contains information about the [`Project`]s (e.g. their names, their state, etc.) as -/// well as components for interacting with the [`Project`]s (e.g. buttons to start/stop) the -/// projects, etc. -/// -/// Under the hood, we use a scrollable grid since the user may have more [`Project`]s than can fit -/// on the screen. We use a grid with headers since the user needs to know which [`Project`] -/// property each column represents. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -pub type ProjectsTable = ensogl_grid_view::simple::SimpleScrollableSelectableGridViewWithHeaders; - - - -// ================ -// === Position === -// ================ - -/// The row and column coordinates of a cell in the [`ProjectsTable`]. -#[derive(Clone, Copy, Debug, Default)] -pub struct Position { - row: Row, - column: Col, -} - - -// === Trait `impl`s === - -impl From<(Row, Col)> for Position { - fn from((row, column): (Row, Col)) -> Self { - Self { row, column } - } -} - -impl From for (Row, Col) { - fn from(Position { row, column }: Position) -> Self { - (row, column) - } -} - - - -// =========== -// === FRP === -// =========== - -ensogl_core::define_endpoints_2! { - Input { - set_projects(Rc>), - } -} - - - -// ============= -// === Model === -// ============= - -#[derive(Clone, CloneRef, Debug)] -struct Model { - display_object: display::object::Instance, - projects_table: ProjectsTable, - projects: ide_view_graph_editor::SharedVec, -} - - -// === Internal `impl` === - -impl Model { - fn new(app: &Application) -> Self { - let display_object = display::object::Instance::new(); - let projects_table = ProjectsTable::new(app); - let projects = ide_view_graph_editor::SharedVec::new(); - display_object.add_child(&projects_table); - Self { display_object, projects_table, projects } - } - - fn model_for_entry(&self, position: Position) -> Option<(Position, EntryModel)> { - let Position { row, .. } = position; - // If the row is the first one in the table, we're trying to render a header so we use that - // model instead. - if row == 0 { - let entry_model = self.header_entry_model(position); - Some((position, entry_model)) - } else { - let idx = self.project_index_for_entry(position)?; - let column = column_for_entry(position)?; - let project = &self.projects.raw.borrow()[idx]; - let entry_model = project_entry_model(project, column); - Some((position, entry_model)) - } - } - - /// Returns the index of the [`Project`] in the list of [`Project`]s that corresponds to the - /// provided [`Position`] of the [`ProjectsTable`]. - /// - /// Aside from the header row, each row of the [`ProjectsTable`] contains data on a [`Project`] - /// from the backing [`Model.projects`] list. But the index of the row doesn't correspond to the - /// index in the table, since the header row doesn't contain data on a [`Project`]. So this - /// function performs a conversion between the two indices. - /// - /// [`Project`]: ::enso_cloud_view::project::Project - fn project_index_for_entry(&self, position: Position) -> Option { - let Position { row, .. } = position; - // The rows of the grid are zero-indexed, but the first row is the header row, so we need to - // subtract 1 to get the index of the project we want to display. - let idx = row.checked_sub(1)?; - if idx >= self.projects.len() { - warn!( - "Attempted to display entry at index {idx}, but we only have data up to index {}.", - self.projects.len() - ); - return None; - } - Some(idx) - } - - fn resize_grid_to_shape(&self, shape: &ensogl::display::scene::Shape) -> Vector2 { - let screen_size = Vector2::from(shape); - let margin = Vector2::new(HORIZONTAL_MARGIN, VERTICAL_MARGIN); - let size = screen_size - margin; - size - } - - fn reposition_grid_to_shape(&self, shape: &ensogl::display::scene::Shape) { - let screen_size = Vector2::from(shape); - let screen_size_halved = screen_size / 2.0; - let viewport_min_y = -screen_size_halved.y + BOTTOM_MARGIN; - let viewport_min_x = -screen_size_halved.x + LEFT_MARGIN; - let grid_height = screen_size.y * GRID_HEIGHT_RATIO; - let grid_min_x = viewport_min_x; - let grid_max_y = viewport_min_y + grid_height; - let position = Vector2::new(grid_min_x, grid_max_y); - self.projects_table.set_xy(position); - } - - fn refit_entries_to_shape(&self, shape: &ensogl::display::scene::Shape) -> Vector2 { - let width = shape.width / Columns::LEN as f32; - let size = Vector2(width, ENTRY_HEIGHT); - size - } - - /// Returns the information about what section the requested visible entry is in. - /// - /// 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` 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). - /// - /// Since we use separate tables to display separate types of data in the dashboard, this table - /// only ever has one section. So it should always return the range `0..=self.rows()`. - fn position_to_requested_section(&self, position: Position) -> (Range, Col, EntryModel) { - /// The first row in the section the requested visible entry is in. - /// - /// This is always `0` because we only have one section, so the first row is the first - /// visible one in the table. - const SECTION_START: usize = 0; - /// The last row in the section the requested visible entry is in is an inclusive value, so - /// we need to increment `self.rows()` by 1 to get it, since `self.rows()` isn't including - /// the header row. - const HEADER_OFFSET: usize = 1; - - let Position { column, .. } = position; - let position = (SECTION_START, column).into(); - let model = self.header_entry_model(position); - let section_end = self.projects.len() + HEADER_OFFSET; - let section_range = SECTION_START..section_end; - (section_range, column, model) - } - - fn header_entry_model(&self, position: Position) -> EntryModel { - let Position { row, .. } = position; - assert!(row == 0, "Header row was {row}, but it is expected to be first row in the table."); - let column = column_for_entry(position); - let entry_model = column.map(|column| EntryModel { - text: column.to_string().into(), - disabled: Immutable(true), - override_width: Immutable(None), - }); - let entry_model = entry_model.unwrap_or_else(invalid_entry_model); - entry_model - } -} - - -// === Setter `impl` === - -impl Model { - fn set_projects(&self, projects: Rc>) -> (Row, Col) { - *self.projects.raw.borrow_mut() = projects.to_vec(); - - let rows = self.rows(); - let cols = Columns::LEN; - (rows, cols) - } -} - -/// Returns an [`EntryModel`] representing a [`Project`] in the [`ProjectsTable`]. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -fn project_entry_model(project: &view::project::Project, column: Columns) -> EntryModel { - // Map the requested column to the corresponding field of the `Project` struct. - let model = match column { - Columns::Projects => { - // TODO [NP]: https://www.pivotaltracker.com/story/show/183909391 - // `"Home Screen" User` can see an `Icon` representing the `Project`'s state. - let state = &project.state; - let name = &project.name; - let state = match state { - view::project::StateTag::New => "New".to_string(), - view::project::StateTag::Created => "Created".to_string(), - view::project::StateTag::OpenInProgress => "OpenInProgress".to_string(), - view::project::StateTag::Opened => "Opened".to_string(), - view::project::StateTag::Closed => "Closed".to_string(), - }; - format!("({state}) {name}") - } - Columns::LastModified => LAST_MODIFIED.to_string(), - Columns::SharedWith => SHARED_WITH.to_string(), - // TODO [NP]: https://www.pivotaltracker.com/story/show/183909420 - // `"Home Screen" User` can see a `"running" Label` for any currently running - // `Project`s. - Columns::Labels => LABELS.to_string(), - Columns::DataAccess => DATA_ACCESS.to_string(), - Columns::UsagePlan => USAGE_PLAN.to_string(), - }; - - EntryModel { - text: model.into(), - disabled: Immutable(false), - override_width: Immutable(None), - } -} - -/// Returns an [`EntryModel`] representing an "invalid" entry, which is used to fill in the table in -/// the event that we request a [`Position`] that is out of bounds. -fn invalid_entry_model() -> EntryModel { - EntryModel { - text: "Invalid entry".into(), - disabled: Immutable(false), - override_width: Immutable(None), - } -} - - -// === Getter `impl`s === - -impl Model { - /// Returns the number of [`ProjectsTable`] rows needed to display all the data in this - /// [`Model`]. - /// - /// The number of rows is equal to the number of [`Project`]s in the [`Model`] plus one, because - /// the first row is the header row. - /// - /// [`Project`]: ::enso_cloud_view::project::Project - fn rows(&self) -> usize { - self.projects.len() + 1 - } -} - - - -// ============ -// === View === -// ============ - -/// The view implementation for the Cloud dashboard. -/// -/// This is the combination of the [`ProjectsTable`] controlling the rendering of the table of -/// [`Project`]s, the [`Model`] containing the data to be displayed, and the [`Frp`] network used to -/// update the [`Model`] with network-fetched [`Project`]s data and re-render the [`ProjectsTable`] -/// accordingly. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -#[derive(Clone, Debug, Deref)] -pub struct View { - #[deref] - frp: Frp, - model: Model, - app: Application, -} - - -// === Internal `impl` === - -impl View { - fn new(app: Application) -> Self { - let frp = Frp::new(); - let model = Model::new(&app); - Self { frp, model, app } - } - - pub(crate) fn init(self) -> Result { - let frp = &self.frp; - let model = &self.model; - let app = &self.app; - let root = &model.display_object; - let input = &frp.public.input; - - self.init_projects_table_model_data_loading(); - self.init_projects_table_grid_resizing(app); - self.init_projects_table_entries_models(); - self.init_projects_table_grid(); - self.init_projects_table_header(); - self.init_event_tracing(app); - - app.display.add_child(root); - - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909432 - // Rather than pass this error up, display the error in this view. - populate_table_with_data(input.clone_ref())?; - - Ok(self) - } - - fn init_projects_table_model_data_loading(&self) { - let frp = &self.frp; - let network = &frp.network; - let model = &self.model; - let input = &frp.public.input; - let projects_table = &model.projects_table; - - frp::extend! { network - grid_size <- input.set_projects.map(f!((projects) model.set_projects(projects.clone_ref()))); - projects_table.resize_grid <+ grid_size; - projects_table.reset_entries <+ grid_size; - } - } - - fn init_projects_table_grid_resizing(&self, app: &Application) { - let network = &self.frp.network; - let model = &self.model; - let scene = &app.display.default_scene; - let projects_table = &model.projects_table; - let scroll_frp = projects_table.scroll_frp(); - - frp::extend! { network - scroll_frp.resize <+ scene.frp.shape.map(f!((shape) model.resize_grid_to_shape(shape))); - eval scene.frp.shape ((shape) model.reposition_grid_to_shape(shape)); - projects_table.set_entries_size <+ scene.frp.shape.map(f!((shape) model.refit_entries_to_shape(shape))); - } - } - - fn init_projects_table_entries_models(&self) { - let network = &self.frp.network; - let model = &self.model; - let projects_table = &model.projects_table; - - frp::extend! { network - // We want to work with our `Position` struct rather than a coordinate pair, so convert. - needed_entries <- projects_table.model_for_entry_needed.map(|position| Position::from(*position)); - projects_table.model_for_entry <+ - needed_entries.filter_map(f!((position) model.model_for_entry(*position).map(|(position, entry_model)| { - let (row, col) = position.into(); - (row, col, entry_model) - }))); - } - } - - fn init_projects_table_grid(&self) { - let model = &self.model; - let projects_table = &model.projects_table; - - let entry_size = Vector2(ENTRY_WIDTH, ENTRY_HEIGHT); - projects_table.set_entries_size(entry_size); - let params = ensogl_grid_view::simple::EntryParams { - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909458 - // Move the EntryParams values to StyleWatchFrp values. - bg_color: color::Lcha::transparent(), - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909458 - // Move the EntryParams values to StyleWatchFrp values. - bg_margin: 1.0, - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909458 - // Move the EntryParams values to StyleWatchFrp values. - // TODO [NP]: https://www.pivotaltracker.com/story/show/183909450 - // `"Home Screen" User` can see `Project` row is highlighted when hovering - // over the row. - hover_color: color::Lcha::from(color::Rgba( - 62f32 / u8::MAX as f32, - 81f32 / u8::MAX as f32, - 95f32 / u8::MAX as f32, - 0.05, - )), - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909458 - // Move the EntryParams values to StyleWatchFrp values. - selection_color: color::Lcha::from(color::Rgba(1.0, 0.0, 0.0, 1.0)), - ..default() - }; - projects_table.set_entries_params(params); - let row = model.rows(); - let col = Columns::LEN; - projects_table.reset_entries(row, col); - projects_table.focus(); - } - - fn init_projects_table_header(&self) { - let model = &self.model; - let network = self.frp.network(); - let header_frp = model.projects_table.header_frp(); - - frp::extend! { network - requested_section <- header_frp.section_info_needed.map(f!((position) model.position_to_requested_section((*position).into()))); - header_frp.section_info <+ requested_section; - } - } - - fn init_event_tracing(&self, app: &Application) { - let frp = &self.frp; - let network = &frp.network; - let model = &self.model; - let projects_table = &model.projects_table; - let input = &frp.public.input; - let scene = &app.display.default_scene; - - frp::extend! { network - trace input.set_projects; - trace projects_table.model_for_entry; - trace scene.frp.shape; - } - } -} - -fn populate_table_with_data(input: api::public::Input) -> Result<(), Error> { - let api_gateway_id = enso_cloud_http::ApiGatewayId(API_GATEWAY_ID.to_string()); - let token = enso_cloud_http::AccessToken::new(TOKEN)?; - let base_url = enso_cloud_http::base_url_for_api_gateway(api_gateway_id, AWS_REGION)?; - let client = enso_cloud_http::Client::new(base_url, token)?; - get_projects(client, input); - Ok(()) -} - -/// Returns the [`Columns`] variant for the entry at the given [`Position`], letting us select over -/// what field of data from a [`Project`] we want to display. -/// -/// [`Project`]: ::enso_cloud_view::project::Project -fn column_for_entry(position: Position) -> Option { - let Position { column, .. } = position; - let column = match Columns::from_discriminant(column) { - Some(column) => column, - None => { - warn!("Attempted to display entry at column {column}, but we the table only has {} columns.", Columns::LEN); - return None; - } - }; - Some(column) -} - -fn get_projects(client: enso_cloud_http::Client, input: crate::projects_table::api::public::Input) { - // FIXME [NP]: https://www.pivotaltracker.com/story/show/183909482 - // Replace `wasm_bindgen_futures` with the futures runtime used throughout the - // remainder of the project. - wasm_bindgen_futures::spawn_local(async move { - let response = client.list_projects().await.unwrap(); - let projects = response.projects; - let projects = Rc::new(projects); - input.set_projects(projects); - }); -} - - -// === Trait `impl`s === - -impl display::Object for View { - fn display_object(&self) -> &display::object::Instance { - &self.model.display_object - } -} - -impl FrpNetworkProvider for View { - fn network(&self) -> &frp::Network { - &self.frp.network - } -} - -impl application::View for View { - fn label() -> &'static str { - "grid::View" - } - - fn new(app: &Application) -> Self { - let app = app.clone_ref(); - Self::new(app) - } - - fn app(&self) -> &Application { - &self.app - } -} - - - -// ============= -// === Tests === -// ============= - -#[cfg(test)] -mod tests { - use crate::projects_table; - - #[test] - fn test_columns_display() { - assert_eq!(projects_table::Columns::Projects.to_string(), "Projects"); - assert_eq!(projects_table::Columns::LastModified.to_string(), "Last Modified"); - assert_eq!(projects_table::Columns::SharedWith.to_string(), "Shared With"); - assert_eq!(projects_table::Columns::Labels.to_string(), "Labels"); - assert_eq!(projects_table::Columns::DataAccess.to_string(), "Data Access"); - assert_eq!(projects_table::Columns::UsagePlan.to_string(), "Usage Plan"); - } -} diff --git a/app/gui/view/debug_scene/src/lib.rs b/app/gui/view/debug_scene/src/lib.rs index 6cb670aadd..52dac66928 100644 --- a/app/gui/view/debug_scene/src/lib.rs +++ b/app/gui/view/debug_scene/src/lib.rs @@ -22,7 +22,6 @@ // === Export === // ============== -pub use debug_scene_cloud_dashboard as cloud_dashboard; pub use debug_scene_component_list_panel_view as new_component_list_panel_view; pub use debug_scene_documentation as documentation; pub use debug_scene_icons as icons;