app: Add Bootstrap classes.

This commit is contained in:
Valentin Tolmer 2021-09-19 19:44:53 +02:00 committed by nitnelave
parent 00efdb42af
commit a952968e9f
11 changed files with 307 additions and 104 deletions

View File

@ -5,7 +5,18 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>LLDAP Administration</title> <title>LLDAP Administration</title>
<script src="/pkg/bundle.js" defer></script> <script src="/pkg/bundle.js" defer></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="preload stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous" as="style" /> <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
rel="preload stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
crossorigin="anonymous"
as="style" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
as="style" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>

View File

@ -195,8 +195,9 @@ impl Component for AddUserToGroupComponent {
</Select> </Select>
</td> </td>
<td> <td>
<button onclick=self.link.callback( <button
|_| Msg::SubmitAddGroup)> class="btn btn-success"
onclick=self.link.callback(|_| Msg::SubmitAddGroup)>
{"Add"} {"Add"}
</button> </button>
</td> </td>

View File

@ -93,7 +93,7 @@ impl Component for App {
let link = self.link.clone(); let link = self.link.clone();
let is_admin = self.is_admin(); let is_admin = self.is_admin();
html! { html! {
<div> <div class="container">
<h1>{ "LLDAP" }</h1> <h1>{ "LLDAP" }</h1>
<Router<AppRoute> <Router<AppRoute>
render = Router::render(move |switch: AppRoute| { render = Router::render(move |switch: AppRoute| {
@ -111,7 +111,7 @@ impl Component for App {
<div> <div>
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) /> <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
<UserTable /> <UserTable />
<NavButton route=AppRoute::CreateUser>{"Create a user"}</NavButton> <NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
</div> </div>
}, },
AppRoute::UserDetails(username) => html! { AppRoute::UserDetails(username) => html! {

View File

@ -53,9 +53,9 @@ impl CreateUserForm {
email: get_element("email") email: get_element("email")
.filter(not_empty) .filter(not_empty)
.ok_or_else(|| anyhow!("Missing email"))?, .ok_or_else(|| anyhow!("Missing email"))?,
displayName: get_element("displayname").filter(not_empty), displayName: get_element("display-name").filter(not_empty),
firstName: get_element("firstname").filter(not_empty), firstName: get_element("first-name").filter(not_empty),
lastName: get_element("lastname").filter(not_empty), lastName: get_element("last-name").filter(not_empty),
}, },
}; };
self._task = Some(HostService::graphql_query::<CreateUser>( self._task = Some(HostService::graphql_query::<CreateUser>(
@ -175,39 +175,107 @@ impl Component for CreateUserForm {
fn view(&self) -> Html { fn view(&self) -> Html {
html! { html! {
<form ref=self.node_ref.clone() onsubmit=self.link.callback(|e: FocusEvent| { e.prevent_default(); Msg::SubmitForm })> <>
<div> <form
<label for="username">{"User name:"}</label> class="form"
<input type="text" id="username" required=true /> ref=self.node_ref.clone()
onsubmit=self.link.callback(|e: FocusEvent| { e.prevent_default(); Msg::SubmitForm })>
<div class="form-group row">
<label for="username"
class="form-label col-sm-2 col-form-label">
{"User name*:"}
</label>
<div class="col-sm-10">
<input
type="text"
id="username"
class="form-control"
autocomplete="username"
required=true />
</div> </div>
<div> </div>
<label for="email">{"Email:"}</label> <div class="form-group row">
<input type="email" id="email" required=true /> <label for="email"
class="form-label col-sm-2 col-form-label">
{"Email*:"}
</label>
<div class="col-sm-10">
<input
type="email"
id="email"
class="form-control"
autocomplete="email"
required=true />
</div> </div>
<div> </div>
<label for="displayname">{"Display name:"}</label> <div class="form-group row">
<input type="text" id="displayname" /> <label for="display-name"
class="form-label col-sm-2 col-form-label">
{"Display name*:"}
</label>
<div class="col-sm-10">
<input
type="text"
autocomplete="name"
class="form-control"
id="display-name" />
</div>
</div>
<div class="form-group row">
<label for="first-name"
class="form-label col-sm-2 col-form-label">
{"First name:"}
</label>
<div class="col-sm-10">
<input
type="text"
autocomplete="given-name"
class="form-control"
id="first-name" />
</div> </div>
<div> </div>
<label for="firstname">{"First name:"}</label> <div class="form-group row">
<input type="text" id="firstname" /> <label for="last-name"
class="form-label col-sm-2 col-form-label">
{"Last name:"}
</label>
<div class="col-sm-10">
<input
type="text"
autocomplete="family-name"
class="form-control"
id="last-name" />
</div> </div>
<div> </div>
<label for="lastname">{"Last name:"}</label> <div class="form-group row">
<input type="text" id="lastname" /> <label for="password"
</div> class="form-label col-sm-2 col-form-label">
<div> {"Password:"}
<label for="password">{"Password:"}</label> </label>
<input type="password" id="password" autocomplete="new-password" minlength="8" /> <div class="col-sm-10">
</div> <input
<button type="submit">{"Submit"}</button> type="password"
<div> id="password"
{ if let Some(e) = &self.error { class="form-control"
html! { e.to_string() } autocomplete="new-password"
} else { html! {} } minlength="8" />
}
</div> </div>
</div>
<div class="form-group row">
<button
class="btn btn-primary col-sm-1 col-form-label"
type="submit">{"Submit"}</button>
</div>
</form> </form>
{ if let Some(e) = &self.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
</>
} }
} }
} }

View File

@ -142,17 +142,45 @@ impl Component for LoginForm {
fn view(&self) -> Html { fn view(&self) -> Html {
html! { html! {
<form ref=self.node_ref.clone() onsubmit=self.link.callback(|e: FocusEvent| { e.prevent_default(); Msg::Submit })> <form
<div> ref=self.node_ref.clone()
<label for="username">{"User name:"}</label> class="form center-block col-sm-4 col-offset-4"
<input type="text" id="username" required=true /> onsubmit=self.link.callback(|e: FocusEvent| { e.prevent_default(); Msg::Submit })>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-person-fill"/>
</span>
</div>
<input
type="text"
class="form-control"
id="username"
placeholder="Username"
required=true />
</div> </div>
<div> <div class="input-group">
<label for="password">{"Password:"}</label> <div class="input-group-prepend">
<input type="password" id="password" required=true autocomplete="current-password" /> <span class="input-group-text">
<i class="bi-lock-fill"/>
</span>
</div>
<input
type="password"
class="form-control"
id="password"
required=true
placeholder="Email"
autocomplete="current-password" />
</div> </div>
<button type="submit">{"Login"}</button> <div class="form-group">
<div> <button
type="submit"
class="btn btn-primary">
{"Login"}
</button>
</div>
<div class="form-group">
{ if let Some(e) = &self.error { { if let Some(e) = &self.error {
html! { e.to_string() } html! { e.to_string() }
} else { html! {} } } else { html! {} }

View File

@ -65,7 +65,11 @@ impl Component for LogoutButton {
fn view(&self) -> Html { fn view(&self) -> Html {
html! { html! {
<button onclick=self.link.callback(|_| Msg::LogoutRequested)>{"Logout"}</button> <button
class="btn btn-primary"
onclick=self.link.callback(|_| Msg::LogoutRequested)>
{"Logout"}
</button>
} }
} }
} }

View File

@ -102,7 +102,13 @@ impl Component for RemoveUserFromGroupComponent {
<> <>
<td>{&group.display_name}</td> <td>{&group.display_name}</td>
{ if self.props.is_admin { html! { { if self.props.is_admin { html! {
<td><button onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>{"-"}</button></td> <td>
<button
class="btn btn-danger"
onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>
<i class="bi-x-circle-fill" aria-label="Remove user from group" />
</button>
</td>
}} else { html!{} } }} else { html!{} }
} }
</> </>

View File

@ -7,10 +7,10 @@ use yew_router::{
pub enum AppRoute { pub enum AppRoute {
#[to = "/login"] #[to = "/login"]
Login, Login,
#[to = "/users"]
ListUsers,
#[to = "/users/create"] #[to = "/users/create"]
CreateUser, CreateUser,
#[to = "/users"]
ListUsers,
#[to = "/user/{user_id}/password"] #[to = "/user/{user_id}/password"]
ChangePassword(String), ChangePassword(String),
#[to = "/user/{user_id}"] #[to = "/user/{user_id}"]

View File

@ -118,17 +118,32 @@ impl UserDetails {
}; };
html! { html! {
<div> <div>
<span>{"Group memberships"}</span> <h3>{"Group memberships"}</h3>
<table> <div class="table-responsive">
<tr key="headerRow"> <table class="table table-striped">
<th>{"Group"}</th> <thead>
{ if self.props.is_admin { html!{ <th></th> }} else { html!{} }} <tr key="headerRow">
</tr> <th>{"Group"}</th>
{u.groups.iter().map(make_group_row).collect::<Vec<_>>()} { if self.props.is_admin { html!{ <th></th> }} else { html!{} }}
<tr key="groupToAddRow"> </tr>
{self.view_add_group_button(u)} </thead>
</tr> <tbody>
</table> {if u.groups.is_empty() {
html! {
<tr key="EmptyRow">
<td>{"Not member of any group"}</td>
</tr>
}
} else {
html! {<>{u.groups.iter().map(make_group_row).collect::<Vec<_>>()}</>}
}}
<hr/>
<tr key="groupToAddRow">
{self.view_add_group_button(u)}
</tr>
</tbody>
</table>
</div>
</div> </div>
} }
} }
@ -193,7 +208,11 @@ impl Component for UserDetails {
{self.view_messages(error)} {self.view_messages(error)}
{self.view_group_memberships(u)} {self.view_group_memberships(u)}
<div> <div>
<NavButton route=AppRoute::ChangePassword(u.id.clone())>{"Change password"}</NavButton> <NavButton
route=AppRoute::ChangePassword(u.id.clone())
classes="btn btn-primary">
{"Change password"}
</NavButton>
</div> </div>
</div> </div>
} }

View File

@ -1,5 +1,5 @@
use crate::{components::user_details::User, infra::api::HostService}; use crate::{components::user_details::User, infra::api::HostService};
use anyhow::{Error, Result}; use anyhow::{bail, Error, Result};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use validator_derive::Validate; use validator_derive::Validate;
use yew::{ use yew::{
@ -101,45 +101,104 @@ impl Component for UserDetailsForm {
type Field = yew_form::Field<UserModel>; type Field = yew_form::Field<UserModel>;
html! { html! {
<> <>
<form> <form class="form">
<div class="form-group"> <div class="form-group row">
<span>{"User ID: "}</span> <label for="userId"
<span>{&self.props.user.id}</span> class="form-label col-sm-2 col-form-label">
</div> {"User ID: "}
<div class="form-group"> </label>
<label for="email">{"Email: "}</label> <div class="col-sm-10">
<Field form=&self.form field_name="email" oninput=self.link.callback(|_| Msg::Update) /> <span id="userId" class="form-constrol-static">{&self.props.user.id}</span>
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group row">
<label for="display_name">{"Display Name: "}</label> <label for="email"
<Field form=&self.form field_name="display_name" oninput=self.link.callback(|_| Msg::Update) /> class="form-label col-sm-2 col-form-label">
<div class="invalid-feedback"> {"Email*: "}
{&self.form.field_message("display_name")} </label>
<div class="col-sm-10">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="email"
autocomplete="email"
oninput=self.link.callback(|_| Msg::Update) />
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group row">
<label for="first_name">{"First Name: "}</label> <label for="display_name"
<Field form=&self.form field_name="first_name" oninput=self.link.callback(|_| Msg::Update) /> class="form-label col-sm-2 col-form-label">
<div class="invalid-feedback"> {"Display Name*: "}
{&self.form.field_message("first_name")} </label>
<div class="col-sm-10">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="display_name"
autocomplete="name"
oninput=self.link.callback(|_| Msg::Update) />
<div class="invalid-feedback">
{&self.form.field_message("display_name")}
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group row">
<label for="last_name">{"Last Name: "}</label> <label for="first_name"
<Field form=&self.form field_name="last_name" oninput=self.link.callback(|_| Msg::Update) /> class="form-label col-sm-2 col-form-label">
<div class="invalid-feedback"> {"First Name: "}
{&self.form.field_message("last_name")} </label>
<div class="col-sm-10">
<Field
class="form-control"
form=&self.form
field_name="first_name"
autocomplete="given-name"
oninput=self.link.callback(|_| Msg::Update) />
<div class="invalid-feedback">
{&self.form.field_message("first_name")}
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group row">
<span>{"Creation date: "}</span> <label for="last_name"
<span>{&self.props.user.creation_date.with_timezone(&chrono::Local)}</span> class="form-label col-sm-2 col-form-label">
{"Last Name: "}
</label>
<div class="col-sm-10">
<Field
class="form-control"
form=&self.form
field_name="last_name"
autocomplete="family-name"
oninput=self.link.callback(|_| Msg::Update) />
<div class="invalid-feedback">
{&self.form.field_message("last_name")}
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group row">
<button type="button" onclick=self.link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})>{"Update"}</button> <label for="creationDate"
class="form-label col-sm-2 col-form-label">
{"Creation date: "}
</label>
<div class="col-sm-10">
<span id="creationDate" class="form-constrol-static">{&self.props.user.creation_date.with_timezone(&chrono::Local)}</span>
</div>
</div>
<div class="form-group row">
<button
type="button"
class="btn btn-primary col-sm-1 col-form-label"
onclick=self.link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})>
<b>{"Update"}</b>
</button>
</div> </div>
</form> </form>
<div hidden=!self.just_updated> <div hidden=!self.just_updated>
@ -160,6 +219,9 @@ impl UserDetailsForm {
} }
fn submit_user_update_form(&mut self) -> Result<bool> { fn submit_user_update_form(&mut self) -> Result<bool> {
if !self.form.validate() {
bail!("Invalid inputs");
}
let base_user = &self.props.user; let base_user = &self.props.user;
let mut user_input = update_user::UpdateUserInput { let mut user_input = update_user::UpdateUserInput {
id: self.props.user.id.clone(), id: self.props.user.id.clone(),

View File

@ -4,7 +4,6 @@ use crate::{
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use yew::format::Json;
use yew::prelude::*; use yew::prelude::*;
use yew::services::{fetch::FetchTask, ConsoleService}; use yew::services::{fetch::FetchTask, ConsoleService};
@ -65,7 +64,6 @@ impl Component for UserTable {
match msg { match msg {
Msg::ListUsersResponse(Ok(users)) => { Msg::ListUsersResponse(Ok(users)) => {
self.users = Some(Ok(users.users.into_iter().collect())); self.users = Some(Ok(users.users.into_iter().collect()));
ConsoleService::log(format!("Response: {:?}", Json(&self.users)).as_str());
true true
} }
Msg::ListUsersResponse(Err(e)) => { Msg::ListUsersResponse(Err(e)) => {
@ -94,17 +92,23 @@ impl Component for UserTable {
}; };
let make_table = |users: &Vec<User>| { let make_table = |users: &Vec<User>| {
html! { html! {
<table> <div class="table-responsive">
<tr> <table class="table table-striped">
<th>{"User ID"}</th> <thead>
<th>{"Email"}</th> <tr>
<th>{"Display name"}</th> <th>{"User ID"}</th>
<th>{"First name"}</th> <th>{"Email"}</th>
<th>{"Last name"}</th> <th>{"Display name"}</th>
<th>{"Creation date"}</th> <th>{"First name"}</th>
</tr> <th>{"Last name"}</th>
{users.iter().map(make_user_row).collect::<Vec<_>>()} <th>{"Creation date"}</th>
</table> </tr>
</thead>
<tbody>
{users.iter().map(make_user_row).collect::<Vec<_>>()}
</tbody>
</table>
</div>
} }
}; };
match &self.users { match &self.users {