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" />
<title>LLDAP Administration</title>
<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>
<body>

View File

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

View File

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

View File

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

View File

@ -65,7 +65,11 @@ impl Component for LogoutButton {
fn view(&self) -> 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>
{ 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!{} }
}
</>

View File

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

View File

@ -118,17 +118,32 @@ impl UserDetails {
};
html! {
<div>
<span>{"Group memberships"}</span>
<table>
<tr key="headerRow">
<th>{"Group"}</th>
{ if self.props.is_admin { html!{ <th></th> }} else { html!{} }}
</tr>
{u.groups.iter().map(make_group_row).collect::<Vec<_>>()}
<tr key="groupToAddRow">
{self.view_add_group_button(u)}
</tr>
</table>
<h3>{"Group memberships"}</h3>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr key="headerRow">
<th>{"Group"}</th>
{ if self.props.is_admin { html!{ <th></th> }} else { html!{} }}
</tr>
</thead>
<tbody>
{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>
}
}
@ -193,7 +208,11 @@ impl Component for UserDetails {
{self.view_messages(error)}
{self.view_group_memberships(u)}
<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>
}

View File

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

View File

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