mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-02 15:15:05 +02:00
[htmx] Apps page (#2868)
* Add htmx Apps page with halfworking navbar Co-authored-by: Firstyear <william@blackhats.net.au>
This commit is contained in:
parent
7db0142ec4
commit
33ca757bed
server/core
src/https
static/external
bootstrap.bundle.min.jsbootstrap.bundle.min.js.mapbootstrap.min.cssbootstrap.min.css.maphtmx.1.9.12.jshtmx.min.1.9.12.js
templates
|
@ -1,12 +1,14 @@
|
|||
//! Where we hide the error handling widgets
|
||||
//!
|
||||
|
||||
use axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
use axum::http::{HeaderValue, StatusCode};
|
||||
use axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use kanidm_proto::internal::OperationError;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use kanidm_proto::internal::OperationError;
|
||||
|
||||
|
||||
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum WebError {
|
||||
|
|
|
@ -137,7 +137,7 @@ pub fn get_js_files(role: ServerRole) -> Result<JavaScriptFiles, ()> {
|
|||
let filelist = if cfg!(feature = "ui_htmx") {
|
||||
vec![
|
||||
("external/bootstrap.bundle.min.js", None, false, false),
|
||||
("external/htmx.min.1.9.2.js", None, false, false),
|
||||
("external/htmx.min.1.9.12.js", None, false, false),
|
||||
("external/confetti.js", None, false, false),
|
||||
("external/pkhtml.js", None, false, false),
|
||||
("external/base64.js", None, false, false),
|
||||
|
|
|
@ -8,6 +8,13 @@ use axum::{Extension, Router};
|
|||
use super::middleware::KOpId;
|
||||
use super::ServerState;
|
||||
|
||||
// when you want to put big text at the top of the page
|
||||
pub const CSS_PAGE_HEADER: &str = "d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-0 pb-0 mb-3 border-bottom";
|
||||
|
||||
pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4";
|
||||
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand navbar-dark";
|
||||
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav me-auto mb-2 mb-md-0";
|
||||
|
||||
pub(crate) fn spa_router_user_ui() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/", get(ui_handler_user_ui))
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
use askama::Template;
|
||||
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::State,
|
||||
http::uri::Uri,
|
||||
response::{IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
|
||||
use axum_htmx::{HxPushUrl, HxReswap, HxRetarget, SwapOption};
|
||||
use axum_htmx::extractors::HxRequest;
|
||||
|
||||
use axum_htmx::{HxPushUrl, HxReswap, HxRetarget, SwapOption};
|
||||
use kanidm_proto::internal::AppLink;
|
||||
|
||||
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
||||
|
||||
use super::{
|
||||
HtmlTemplate,
|
||||
// UnrecoverableErrorView,
|
||||
};
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use super::HtmlTemplate;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "apps.html")]
|
||||
|
@ -27,36 +23,45 @@ struct AppsView {
|
|||
#[derive(Template)]
|
||||
#[template(path = "apps_partial.html")]
|
||||
struct AppsPartialView {
|
||||
// todo - actually list the applications the user can access here.
|
||||
apps: Vec<AppLink>,
|
||||
}
|
||||
|
||||
pub(crate) async fn view_apps_get(
|
||||
State(_state): State<ServerState>,
|
||||
Extension(_kopid): Extension<KOpId>,
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
) -> Response {
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
) -> axum::response::Result<Response> {
|
||||
// Because this is the route where the login page can land, we need to actually alter
|
||||
// our response as a result. If the user comes here directly we need to render the full
|
||||
// page, otherwise we need to render the partial.
|
||||
|
||||
let apps_partial = AppsPartialView {};
|
||||
let app_links = state
|
||||
.qe_r_ref
|
||||
.handle_list_applinks(client_auth_info, kopid.eventid)
|
||||
.await
|
||||
.map_err(|old| HtmxError::new(&kopid, old) )?;
|
||||
|
||||
if hx_request {
|
||||
(
|
||||
let apps_view = AppsView {
|
||||
apps_partial: AppsPartialView {
|
||||
apps: app_links,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(if hx_request {
|
||||
(
|
||||
// On the redirect during a login we don't push urls. We set these headers
|
||||
// so that the url is updated, and we swap the correct element.
|
||||
HxPushUrl(Uri::from_static("/ui/apps")),
|
||||
// Tell htmx that we want to update the body instead. There is no need
|
||||
// set the swap value as it defaults to innerHTML. This is because we came here
|
||||
// from an htmx request so we only need to render the inner portion.
|
||||
HxRetarget("#main".to_string()),
|
||||
HxRetarget("body".to_string()),
|
||||
// We send our own main, replace the existing one.
|
||||
HxReswap(SwapOption::OuterHtml),
|
||||
HtmlTemplate(apps_partial),
|
||||
)
|
||||
.into_response()
|
||||
HtmlTemplate(apps_view),
|
||||
).into_response()
|
||||
} else {
|
||||
HtmlTemplate(AppsView { apps_partial }).into_response()
|
||||
}
|
||||
HtmlTemplate(apps_view).into_response()
|
||||
})
|
||||
}
|
||||
|
|
62
server/core/src/https/views/errors.rs
Normal file
62
server/core/src/https/views/errors.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use kanidm_proto::internal::OperationError;
|
||||
|
||||
use crate::https::middleware::KOpId;
|
||||
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "recoverable_error_partial.html")]
|
||||
// struct ErrorPartialView {
|
||||
// error_message: String,
|
||||
// operation_id: Uuid,
|
||||
// recovery_path: String,
|
||||
// recovery_boosted: bool,
|
||||
// }
|
||||
|
||||
|
||||
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub(crate) enum HtmxError {
|
||||
/// Something went wrong when doing things.
|
||||
OperationError(Uuid, OperationError),
|
||||
// InternalServerError(Uuid, String),
|
||||
}
|
||||
|
||||
impl HtmxError {
|
||||
pub(crate) fn new(kopid: &KOpId, operr: OperationError) -> Self {
|
||||
HtmxError::OperationError(kopid.eventid, operr)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for HtmxError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
// HtmxError::InternalServerError(_kopid, inner) => {
|
||||
// (StatusCode::INTERNAL_SERVER_ERROR, inner).into_response()
|
||||
// }
|
||||
HtmxError::OperationError(_kopid, inner) => {
|
||||
let body = serde_json::to_string(&inner).unwrap_or(inner.to_string());
|
||||
let response = match &inner {
|
||||
OperationError::NotAuthenticated | OperationError::SessionExpired => {
|
||||
Redirect::to("/ui").into_response()
|
||||
}
|
||||
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
||||
(StatusCode::FORBIDDEN, body).into_response()
|
||||
}
|
||||
OperationError::NoMatchingEntries => (StatusCode::NOT_FOUND, body).into_response(),
|
||||
OperationError::PasswordQuality(_)
|
||||
| OperationError::EmptyRequest
|
||||
| OperationError::SchemaViolation(_)
|
||||
| OperationError::CU0003WebauthnUserNotVerified => {
|
||||
(StatusCode::BAD_REQUEST, body).into_response()
|
||||
}
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, body).into_response(),
|
||||
};
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ use crate::https::{
|
|||
|
||||
mod apps;
|
||||
mod login;
|
||||
mod errors;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "unrecoverable_error.html")]
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3925
server/core/static/external/htmx.1.9.12.js
vendored
Normal file
3925
server/core/static/external/htmx.1.9.12.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
server/core/static/external/htmx.min.1.9.12.js
vendored
Normal file
1
server/core/static/external/htmx.min.1.9.12.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,10 +1,9 @@
|
|||
(% extends "base_htmx.html" %)
|
||||
|
||||
(% block title %)Error(% endblock %)
|
||||
(% extends "base_htmx_with_nav.html" %)
|
||||
(% block title %)Apps(% endblock %)
|
||||
|
||||
(% block head %)
|
||||
(% endblock %)
|
||||
|
||||
(% block body %)
|
||||
(% block main %)
|
||||
(( apps_partial|safe ))
|
||||
(% endblock %)
|
||||
(% endblock %)
|
|
@ -1,3 +1,28 @@
|
|||
<main class="flex-shrink-0 form-signin">
|
||||
<h2>Apps Go Here</h2>
|
||||
<main class="p-3 x-auto">
|
||||
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
|
||||
<h2>Applications list</h2>
|
||||
</div>
|
||||
(% if apps.is_empty() %)
|
||||
<h5>No linked applications available</h5>
|
||||
(% else %)
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||
(% for app in apps %)
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
(% match app %)
|
||||
(% when AppLink::Oauth2 with { name, display_name, redirect_url, has_image } %)
|
||||
<a href="(( redirect_url ))" class="link-dark stretched-link mt-2">
|
||||
(% if has_image %)
|
||||
<img src="/ui/images/oauth2/(( name ))" class="oauth2-img" alt="((display_name)) icon" id="(( name ))">
|
||||
(% else %)
|
||||
<img src="/pkg/img/icon-oauth2.svg" class="oauth2-img" alt="missing-icon icon" id="(( name ))">
|
||||
(% endif %)
|
||||
</a>
|
||||
<label for="(( name ))">(( display_name ))</label>
|
||||
(% endmatch %)
|
||||
</div>
|
||||
</div>
|
||||
(% endfor %)
|
||||
</div>
|
||||
(% endif %)
|
||||
</main>
|
||||
|
|
|
@ -16,14 +16,15 @@
|
|||
<link rel="apple-touch-icon" sizes="512x512"
|
||||
href="/pkg/img/logo-square.svg" />
|
||||
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
|
||||
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"></script>
|
||||
<script src="/pkg/external/htmx.min.1.9.2.js" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"></script>
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"/>
|
||||
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script>
|
||||
<script src="/pkg/external/htmx.min.1.9.12.js" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"></script>
|
||||
<link rel="stylesheet" href="/pkg/style.css" />
|
||||
|
||||
(% block head %)(% endblock %)
|
||||
</head>
|
||||
<body hx-boost="true" class="flex-column d-flex h-100">
|
||||
(% block nav %)(% endblock %)
|
||||
(% block body %)(% endblock %)
|
||||
<footer class="footer mt-auto py-3 bg-light text-end">
|
||||
<div class="container">
|
||||
|
|
7
server/core/templates/base_htmx_with_nav.html
Normal file
7
server/core/templates/base_htmx_with_nav.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
(% extends "base_htmx.html" %)
|
||||
|
||||
(% block body %)
|
||||
(% include "navbar.html" %)
|
||||
(% block main %)(% endblock %)
|
||||
(% include "signout_modal.html" %)
|
||||
(% endblock %)
|
24
server/core/templates/navbar.html
Normal file
24
server/core/templates/navbar.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))">
|
||||
<div class="container-fluid">
|
||||
<a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))" href="/ui/apps">Kanidm</a>
|
||||
|
||||
<!-- this shows a button on mobile devices to open the menu-->
|
||||
<button class="navbar-toggler bg-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<img src="/pkg/img/logo-square.svg" alt="Toggle navigation" class="navbar-toggler-img" />
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="/ui/apps"><span data-feather="file"></span>Apps</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="/ui/profile"><span data-feather="file"></span>Profile</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#signoutModal">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
19
server/core/templates/recoverable_error_partial.html
Normal file
19
server/core/templates/recoverable_error_partial.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class="modal" tabindex="-1" role="dialog" id="errorModal" data-backdrop="static" data-keyboard="false" data-show="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><img src="/pkg/img/kani-warning.svg" alt="Kani holding warning sign" /> An error occurred</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>Error Code: (( error_message ))</p>
|
||||
<p>Operation ID: (( operation_id ))</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="(( recovery_path ))" hx-boost="(( recovery_boosted ))">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-bs-dismiss="modal">Continue</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
24
server/core/templates/signout_modal.html
Normal file
24
server/core/templates/signout_modal.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<div class="modal" tabindex="-1" role="dialog" id="signoutModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirm Sign out</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
Are you sure you'd like to log out?
|
||||
<br/>
|
||||
<img src="/pkg/img/kani-waving.svg" alt="Kani waving goodbye" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="/ui/logout" hx-boost="false">
|
||||
<button type="button" class="btn btn-success"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#signoutModal">Sign out</button>
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue