From 2f5f5bf55162155158b08daeb41113f3da0d5d6a Mon Sep 17 00:00:00 2001 From: Lucas Franceschino Date: Fri, 16 Feb 2024 22:49:05 +0100 Subject: [PATCH] feat(webapp): add a "add project" page This commit split the "Projects" component in two smaller ones, and get rid of the form to add a project. Instead, this commit introduces a separate page for creating projects. --- typhon-webapp/src/app.rs | 23 ++- typhon-webapp/src/components/header.rs | 44 +++-- typhon-webapp/src/pages/add_project.rs | 215 +++++++++++++++++++++++++ typhon-webapp/src/pages/mod.rs | 2 + typhon-webapp/src/pages/projects.rs | 156 ++++++++---------- typhon-webapp/src/prelude.rs | 2 +- typhon-webapp/src/routes.rs | 13 ++ 7 files changed, 355 insertions(+), 100 deletions(-) create mode 100644 typhon-webapp/src/pages/add_project.rs diff --git a/typhon-webapp/src/app.rs b/typhon-webapp/src/app.rs index c1812443..e0231f5c 100644 --- a/typhon-webapp/src/app.rs +++ b/typhon-webapp/src/app.rs @@ -53,6 +53,9 @@ pub fn App() -> impl IntoView { margin: 0; padding: 0; } + :deep(*) { + box-sizing: border-box; + } :deep(:root) { --font-size-huge: 20px; --font-size-big: 16px; @@ -75,6 +78,7 @@ pub fn App() -> impl IntoView { --color-red: #cf222e; --color-lightred: #d1242f; --color-green: #1a7f37; + --color-darkgreen: rgb(24, 119, 51); --color-lightgreen: #1f883d; --color-orange: rgb(219, 171, 10); @@ -136,13 +140,30 @@ pub fn App() -> impl IntoView { :deep(.is-table .rows > .row:last-child) { border-radius: 0 0 var(--radius) var(--radius); } + :deep(input[type=text]:focus), :deep(input[type=text]:focus-visible) { + outline: var(--color-bg-emphasis) auto 1px; + outline-offset: 0px; + } + :deep(input[type=text]) { + border: 1px solid var(--color-border-default); + border-radius: 3px; + font-size: inherit; + font-family: inherit; + padding: 6px 10px; + margin-top: 4px; + margin-bottom: 4px; + font-size: inherit; + font-weight: inherit; + } }; provide_context(utils::now_signal()); view! { class=_styler_class, - + + + diff --git a/typhon-webapp/src/components/header.rs b/typhon-webapp/src/components/header.rs index 2d4fdbd4..cafc983d 100644 --- a/typhon-webapp/src/components/header.rs +++ b/typhon-webapp/src/components/header.rs @@ -7,6 +7,7 @@ pub fn TyphonLogo() -> impl IntoView { a { display: inline-flex; align-items: center; + justify-content: normal; padding: 8px; user-select: none; } @@ -115,29 +116,46 @@ pub fn Nav(route: Signal>>) -> impl IntoView #[component] pub fn Header(#[prop(into)] route: Signal>>) -> impl IntoView { let style = style! { - div { + .nav-wrapper { border-bottom: 1px solid var(--color-border-default); display: flex; align-items: center; + justify-content: space-between; background: var(--color-lllightgray); } + .buttons { + justify-content: normal; + gap: 10px; + display: flex; + padding-right: 8px; + } }; let user: Signal> = use_context().unwrap(); - view! { -
+ view! { class=style, + } } diff --git a/typhon-webapp/src/pages/add_project.rs b/typhon-webapp/src/pages/add_project.rs new file mode 100644 index 00000000..c9687891 --- /dev/null +++ b/typhon-webapp/src/pages/add_project.rs @@ -0,0 +1,215 @@ +use crate::prelude::*; + +#[component] +pub(crate) fn AddProject() -> impl IntoView { + let action = request_action!( + CreateProject, + |name: String, url: String, flake: Option| requests::Request::CreateProject { + name, + decl: requests::ProjectDecl { + url, + flake: flake.is_some() + }, + }, + |result| { + // TODO: redirect when success + // if let Ok(Ok(())) = result { + // leptos_actix::redirect("/"); + // } + result + } + ); + let value: RwSignal> = action.value(); + let response = move || { + value().map(|v| match v { + Ok(Err(ResponseError::BadRequest(message))) => Err(message), + Ok(_) => Ok(()), + Err(e) => Err(format!("Fatal error: {e:?}")), + }) + }; + let style = style! { + .header { + font-size: var(--font-size-huge); + padding-bottom: 2px; + } + .header-description { + color: var(--color-fg-muted); + padding-top: 4px; + margin-bottom: 14px; + } + .sep { + border-bottom: 1px solid var(--color-border-default); + padding-bottom: 14px; + } + .page { + padding-top: 22px; + max-width: 600px; + margin: auto; + } + .fields { + display: flex; + flex-direction: column; + gap: 14px; + } + .field > label { + display: block; + padding-left: 2px; + font-size: var(--font-size-small); + font-weight: 400; + } + .field > input { + width: 100%; + } + .field.flake { + display: flex; + flex-direction: column; + gap: 16px; + } + .field.flake .option { + display: grid; + grid-template-areas: raw_str("radio icon title") raw_str("radio icon details"); + grid-template-columns: auto auto 1fr; + align-items: center; + column-gap: 5px; + } + .field.flake .option input { + grid-area: radio; + } + .field.flake :deep(svg) { + grid-area: icon; + font-size: 20px; + } + .field.flake .option .kind { + font-size: var(--font-size-small); + font-weight: 400; + grid-area: title; + padding-bottom: 2px; + } + .field.flake .option .details { + font-size: var(--font-size-small); + grid-area: details; + color: var(--color-fg-muted); + padding-top: 2px; + } + button { + background: var(--color-green-button-bg); + color: white; + width: auto!important; + transition: all 100ms; + } + button:hover { + transition: all 100ms; + background: var(--color-green); + } + button:active { + transition: all 100ms; + background: var(--color-darkgreen); + } + .page :deep(.message) { + text-align: justify; + padding-bottom: 14px; + display: flex; + align-items: center; + gap: 6px; + } + .page :deep(.error-message) { + color: var(--color-danger-emphasis); + } + .page :deep(.success-message) { + color: var(--color-success); + } + // TODO: Move to `app.rs` + .page :deep(button) { + border-radius: 6px; + border: 1px solid var(--color-border-default); + padding: 8px 10px; + cursor: pointer; + outline: inherit; + font-size: inherit; + font-family: inherit; + font-weight: 400; + } + }; + view! { class=style, +
+ +
"Add a project"
+
+ "Add continuous integration with Typhon for an existing codebase. This project will be visible to anyone that have access to this Typhon instance." +
+
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
+ + + + +
+ +
+
+ {move || { + match response() { + Some(Err(error)) => { + view! { +
+ +
{error}
+
+ } + .into_view() + } + Some(Ok(())) => { + view! { +
+ +
"The project have been created successfully."
+
+ } + .into_view() + } + None => ().into_view(), + } + }} + +
+
+
+
+ } +} diff --git a/typhon-webapp/src/pages/mod.rs b/typhon-webapp/src/pages/mod.rs index cf157ed0..7575b884 100644 --- a/typhon-webapp/src/pages/mod.rs +++ b/typhon-webapp/src/pages/mod.rs @@ -1,3 +1,4 @@ +pub mod add_project; pub mod dashboard; pub mod error; pub mod evaluation; @@ -6,6 +7,7 @@ pub mod login; pub mod project; pub mod projects; +pub(crate) use add_project::AddProject; pub(crate) use dashboard::Dashboard; pub(crate) use error::*; pub(crate) use evaluation::Evaluation; diff --git a/typhon-webapp/src/pages/projects.rs b/typhon-webapp/src/pages/projects.rs index 487ee2c1..3cdea42a 100644 --- a/typhon-webapp/src/pages/projects.rs +++ b/typhon-webapp/src/pages/projects.rs @@ -1,28 +1,10 @@ use crate::prelude::*; #[component] -pub(crate) fn Projects() -> impl IntoView { - let user: Signal> = use_context().unwrap(); - let (error, projects) = search!( - Signal::derive(|| 0), - Signal::derive(|| 100), - Signal::derive(|| requests::search::Kind::Projects), - |total, responses::search::Results::Projects(projects)| (total, projects) - ); - let projects = Signal::derive(move || projects().unwrap_or((0, Vec::new()))); - let count = Signal::derive(move || projects().0); - let projects = Signal::derive(move || projects().1); - let action = request_action!( - CreateProject, - |name: String, url: String, flake: Option| requests::Request::CreateProject { - name, - decl: requests::ProjectDecl { - url, - flake: flake.is_some() - }, - } - ); - // TODO split this view in two views, one for the table, one the the form +fn ProjectList( + count: Signal, + projects: Signal>, +) -> impl IntoView { let style = style! { .rows :deep(> .row), .header-columns { display: grid; @@ -44,88 +26,92 @@ pub(crate) fn Projects() -> impl IntoView { .summary { } - .row :deep(.empty-field) { + .rows :deep(.placeholder) { font-style: italic; opacity: 0.5; } + .rows :deep(.row.no-project-yet .placeholder) { + text-align: center; + } + .rows :deep(.row.no-project-yet) { + display: block; + } }; fn with_placeholder(text: &str) -> impl IntoView { if text.is_empty() { - view! { "" } + view! { "" } } else { view! { {text.to_string()} } } } view! { class=style, - -
-
-
{count} projects
-
-
"Identifier"
-
"Name"
-
"Description"
-
+
+
+
{count} projects
+
+
"Identifier"
+
"Name"
+
"Description"
-
- +
+ {move || { + projects() + .is_empty() + .then(|| { view! { -
- -
- {with_placeholder(&metadata.title)} -
-
- {with_placeholder(&metadata.description)} -
+
+
No any project yet!
} + }) + }} + + +
{with_placeholder(&metadata.title)}
+
+ {with_placeholder(&metadata.description)} +
+
} - /> - -
-
- - -

"Add a project"

-
- - -
-
- - -
-
- - -
- -
-
+ +
+ } +} + +#[component] +pub(crate) fn Projects() -> impl IntoView { + let user: Signal> = use_context().unwrap(); + + let (error, count, projects) = { + let (error, data) = search!( + Signal::derive(|| 0), + Signal::derive(|| 100), + Signal::derive(|| requests::search::Kind::Projects), + |total, responses::search::Results::Projects(projects)| (total, projects) + ); + let data = Signal::derive(move || data().unwrap_or((0, Vec::new()))); + ( + error, + Signal::derive(move || data().0), + Signal::derive(move || data().1), + ) + }; + + view! { + + } } diff --git a/typhon-webapp/src/prelude.rs b/typhon-webapp/src/prelude.rs index be7e59b5..d7a97b23 100644 --- a/typhon-webapp/src/prelude.rs +++ b/typhon-webapp/src/prelude.rs @@ -10,7 +10,7 @@ pub use uuid::Uuid; pub use typhon_types::{ data, handles, requests::{self, Request}, - responses::{self, Response}, + responses::{self, Response, ResponseError}, }; pub(crate) use crate::{ diff --git a/typhon-webapp/src/routes.rs b/typhon-webapp/src/routes.rs index 48a3e011..5c6f32db 100644 --- a/typhon-webapp/src/routes.rs +++ b/typhon-webapp/src/routes.rs @@ -13,6 +13,7 @@ pub enum Root { page: u32, }, Projects, + AddProject, Project(handles::Project), Jobset { handle: handles::Jobset, @@ -76,6 +77,7 @@ impl From for Root { Root::Login => Root::Login, Root::Dashboard { tab, page } => Root::Dashboard { tab, page }, Root::Projects => Root::Projects, + Root::AddProject => Root::AddProject, Root::Project(h) => Root::Project(h), Root::Jobset { handle, .. } => Root::Jobset { handle, page: () }, Root::Evaluation(e) => Root::Evaluation(e.into()), @@ -89,6 +91,7 @@ impl From> for Root { Root::Login => Root::Login, Root::Dashboard { tab, page } => Root::Dashboard { tab, page }, Root::Projects => Root::Projects, + Root::AddProject => Root::AddProject, Root::Project(h) => Root::Project(h), Root::Jobset { handle, .. } => Root::Jobset { handle, page: 1 }, Root::Evaluation(e) => Root::Evaluation(e.into()), @@ -102,6 +105,7 @@ impl From> for Option { Root::Login => None?, Root::Dashboard { .. } => None?, Root::Projects => None?, + Root::AddProject => None?, Root::Project(handle) => handles::Handle::Project(handle), Root::Jobset { handle, .. } => handles::Handle::Jobset(handle), Root::Evaluation(eval) => handles::Handle::Evaluation(eval.handle), @@ -214,6 +218,7 @@ impl TryFrom for Root { Ok( match &chunks.iter().map(|s| s.as_ref()).collect::>()[..] { [] => Self::Projects, + ["add-project"] => Self::AddProject, ["login"] => Self::Login, ["dashboard"] => Self::Dashboard { tab: DashboardTab::Builds, @@ -286,6 +291,7 @@ impl From for String { Root::Login => "/login".to_string(), Root::Dashboard { tab, page } => format!("/dashboard/{}?page={page}", tab), Root::Projects => "".to_string(), + Root::AddProject => "/add-project".to_string(), Root::Project(handle) => format!("/project/{}", encode(&handle.name)), Root::Jobset { handle, page } => format!( "/project/{}/jobset/{}?page={page}", @@ -318,6 +324,7 @@ use crate::components::header::*; pub fn Router() -> impl IntoView { let page = Signal::derive(|| Root::try_from(use_location())); let root_page = create_memo(move |_| page().map(Root::::from)); + let user: Signal> = use_context().unwrap(); use crate::pages::*; let main = move || match root_page() { Ok(Root::Login) => view! { }, @@ -325,6 +332,12 @@ pub fn Router() -> impl IntoView { view! { } } Ok(Root::Projects) => view! { }, + Ok(Root::AddProject) => view! { + + {user().is_some().then(|| view! { })} + {user().is_none().then(|| view! { })} + + }, Ok(Root::Project(handle)) => { view! { } }