feat: dashboard homepage. List existing deployments with add site btn
This commit is contained in:
parent
cbcd7bad7b
commit
cdeabb06aa
6 changed files with 225 additions and 150 deletions
|
@ -16,14 +16,23 @@
|
||||||
*/
|
*/
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
use actix_web::http::header::ContentType;
|
use actix_web::http::header::ContentType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
|
use super::get_auth_middleware;
|
||||||
use crate::ctx::api::v1::auth::Login as LoginPayload;
|
use crate::ctx::api::v1::auth::Login as LoginPayload;
|
||||||
|
use crate::db::Site;
|
||||||
|
use crate::errors::ServiceResult;
|
||||||
use crate::pages::errors::*;
|
use crate::pages::errors::*;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::AppCtx;
|
use crate::AppCtx;
|
||||||
|
|
||||||
|
use crate::pages::errors::*;
|
||||||
|
|
||||||
|
use super::TemplateSiteEvent;
|
||||||
|
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
|
|
||||||
pub const DASH_HOME: TemplateFile = TemplateFile::new("dash_home", "pages/dash/index.html");
|
pub const DASH_HOME: TemplateFile = TemplateFile::new("dash_home", "pages/dash/index.html");
|
||||||
|
@ -32,6 +41,12 @@ pub struct Home {
|
||||||
ctx: RefCell<Context>,
|
ctx: RefCell<Context>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct TemplateSite {
|
||||||
|
site: Site,
|
||||||
|
last_update: Option<TemplateSiteEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
impl CtxError for Home {
|
impl CtxError for Home {
|
||||||
fn with_error(&self, e: &ReadableError) -> String {
|
fn with_error(&self, e: &ReadableError) -> String {
|
||||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||||
|
@ -40,10 +55,10 @@ impl CtxError for Home {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Home {
|
impl Home {
|
||||||
pub fn new(settings: &Settings, payload: Option<&LoginPayload>) -> Self {
|
pub fn new(settings: &Settings, sites: Option<&[TemplateSite]>) -> Self {
|
||||||
let ctx = RefCell::new(context(settings));
|
let ctx = RefCell::new(context(settings));
|
||||||
if let Some(payload) = payload {
|
if let Some(sites) = sites {
|
||||||
ctx.borrow_mut().insert(PAYLOAD_KEY, payload);
|
ctx.borrow_mut().insert(PAYLOAD_KEY, sites);
|
||||||
}
|
}
|
||||||
Self { ctx }
|
Self { ctx }
|
||||||
}
|
}
|
||||||
|
@ -53,19 +68,34 @@ impl Home {
|
||||||
.render(DASH_HOME.name, &self.ctx.borrow())
|
.render(DASH_HOME.name, &self.ctx.borrow())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page(s: &Settings) -> String {
|
|
||||||
let p = Self::new(s, None);
|
|
||||||
p.render()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web_codegen_const_routes::get(path = "PAGES.dash.home")]
|
async fn get_site_data(ctx: &AppCtx, id: &Identity) -> ServiceResult<Vec<TemplateSite>> {
|
||||||
#[tracing::instrument(name = "Dashboard homepage", skip(ctx))]
|
let db_sites = ctx.db.list_all_sites(&id.identity().unwrap()).await?;
|
||||||
pub async fn get_home(ctx: AppCtx) -> impl Responder {
|
let mut sites = Vec::with_capacity(db_sites.len());
|
||||||
let home = Home::page(&ctx.settings);
|
for site in db_sites {
|
||||||
|
// TODO: impl method on DB to get latest "update" event
|
||||||
|
let mut events = ctx.db.list_all_site_events(&site.hostname).await?;
|
||||||
|
let last_update = if let Some(event) = events.pop() {
|
||||||
|
Some(event.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
sites.push(TemplateSite { site, last_update });
|
||||||
|
}
|
||||||
|
Ok(sites)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::get(path = "PAGES.dash.home", wrap = "get_auth_middleware()")]
|
||||||
|
#[tracing::instrument(name = "Dashboard homepage", skip(ctx, id))]
|
||||||
|
pub async fn get_home(ctx: AppCtx, id: Identity) -> PageResult<impl Responder, Home> {
|
||||||
|
let sites = get_site_data(&ctx, &id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| PageError::new(Home::new(&ctx.settings, None), e))?;
|
||||||
|
let home = Home::new(&ctx.settings, Some(&sites)).render();
|
||||||
let html = ContentType::html();
|
let html = ContentType::html();
|
||||||
HttpResponse::Ok().content_type(html).body(home)
|
Ok(HttpResponse::Ok().content_type(html).body(home))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
|
|
@ -15,15 +15,43 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use super::get_auth_middleware;
|
||||||
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
|
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
|
||||||
|
|
||||||
mod home;
|
use crate::db::Event;
|
||||||
|
use crate::db::LibrePagesEvent;
|
||||||
|
|
||||||
|
pub mod home;
|
||||||
|
pub mod sites;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct TemplateSiteEvent {
|
||||||
|
pub event_type: Event,
|
||||||
|
pub time: i64,
|
||||||
|
pub site: String,
|
||||||
|
pub id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LibrePagesEvent> for TemplateSiteEvent {
|
||||||
|
fn from(e: LibrePagesEvent) -> Self {
|
||||||
|
Self {
|
||||||
|
event_type: e.event_type,
|
||||||
|
time: e.time.unix_timestamp(),
|
||||||
|
site: e.site,
|
||||||
|
id: e.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_templates(t: &mut tera::Tera) {
|
pub fn register_templates(t: &mut tera::Tera) {
|
||||||
home::DASH_HOME.register(t).expect(home::DASH_HOME.name);
|
home::DASH_HOME.register(t).expect(home::DASH_HOME.name);
|
||||||
|
sites::register_templates(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
home::services(cfg);
|
home::services(cfg);
|
||||||
|
sites::services(cfg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,8 +146,8 @@ pub async fn home(ctx: AppCtx, id: &Identity) -> HttpResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
auth::services(cfg);
|
|
||||||
dash::services(cfg);
|
dash::services(cfg);
|
||||||
|
auth::services(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -169,6 +169,7 @@ mod tests {
|
||||||
auth::login::LOGIN,
|
auth::login::LOGIN,
|
||||||
auth::register::REGISTER,
|
auth::register::REGISTER,
|
||||||
errors::ERROR_TEMPLATE,
|
errors::ERROR_TEMPLATE,
|
||||||
|
super::dash::home::DASH_HOME,
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
@import "defaults.scss";
|
@import "defaults.scss";
|
||||||
@import "pages/auth/sass/main.scss";
|
@import "pages/auth/sass/main.scss";
|
||||||
@import "pages/auth/sass/form/main.scss";
|
@import "pages/auth/sass/form/main.scss";
|
||||||
|
@import "pages/dash/main.scss";
|
||||||
@import "components/sass/footer/main.scss";
|
@import "components/sass/footer/main.scss";
|
||||||
@import "components/nav/sass/main.scss";
|
@import "components/nav/sass/main.scss";
|
||||||
|
|
||||||
|
.default-body {
|
||||||
|
display: flex;
|
||||||
|
@include fullscreen;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,43 @@
|
||||||
|
{% extends 'base' %}
|
||||||
|
{% block title %} Add Site{% endblock title %}
|
||||||
|
{% block nav %} {% include "auth_nav" %} {% endblock nav %}
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<main class="sites__main">
|
||||||
|
<div class="sites__collection">
|
||||||
|
<div class="sites__actions">
|
||||||
|
<a class="sites__actions__new-site" href="{{ page.dash.site.add }}">
|
||||||
|
<button>Add new site</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% for site in payload %}
|
||||||
|
<a href="/sites/mcaptcha.org" class="site__container">
|
||||||
|
<div class="site__info--head">
|
||||||
|
<img
|
||||||
|
class="site__container--preview"
|
||||||
|
src="https://mcaptcha.org/favicon.ico"
|
||||||
|
/>
|
||||||
|
<div class="site__info--column">
|
||||||
|
<p href="https://mcaptcha.org">
|
||||||
|
<b>{{ site.info.hostname }}</b>
|
||||||
|
</p>
|
||||||
|
<p>Deploys from {{ site.info.source_url }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if site.last_update %}
|
||||||
|
<div class="site__info--tail">
|
||||||
|
<p>Last update {{ site.last_update.time }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock main %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -8,153 +48,40 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="auth__body">
|
<body class="auth__body">
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
{% include "auth_nav" %}
|
||||||
<p>LibrePages</p>
|
|
||||||
<span class="nav__spacer"></span>
|
|
||||||
<ul class="nav__links">
|
|
||||||
<li class="nav__item">Help</li>
|
|
||||||
<li class="nav__item">Settings</li>
|
|
||||||
<li class="nav__item">Logout</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="sites__collection">
|
<div class="sites__collection">
|
||||||
<div class="sites__actions">
|
<div class="sites__actions">
|
||||||
<a class="sites__actions__new-site" href="/add/new/site">
|
<a class="sites__actions__new-site" href="{{ page.dash.site.add }}">
|
||||||
<button>Add new site</button>
|
<button>Add new site</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="/sites/mcaptcha.org" class="site__container">
|
{% for site in payload %}
|
||||||
<div class="site__info--head">
|
<a href="/sites/mcaptcha.org" class="site__container">
|
||||||
<img
|
<div class="site__info--head">
|
||||||
class="site__container--preview"
|
<img
|
||||||
src="https://mcaptcha.org/favicon.ico"
|
class="site__container--preview"
|
||||||
/>
|
src="https://mcaptcha.org/favicon.ico"
|
||||||
<div class="site__info--column">
|
/>
|
||||||
<p href="https://mcaptcha.org"><b>mcaptcha.org</b></p>
|
<div class="site__info--column">
|
||||||
<p>Deploys from {{ source_url }}</p>
|
<p href="https://mcaptcha.org"><b>{{ site.info.hostname }}</b></p>
|
||||||
|
<p>Deploys from {{ site.info.source_url }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% if site.last_update %}
|
||||||
<div class="site__info--tail">
|
<div class="site__info--tail">
|
||||||
<p>Last update {{ last_update }}</p>
|
<p>Last update {{ site.last_update.time }}</p>
|
||||||
</div></a
|
</div>
|
||||||
>
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% include "footer" %}
|
{% include "footer" %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<style>
|
|
||||||
header {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav__spacer {
|
|
||||||
flex: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav__links {
|
|
||||||
display: flex;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav__item {
|
|
||||||
margin: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 100%;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sites__collection {
|
|
||||||
margin: auto;
|
|
||||||
width: 70%;
|
|
||||||
|
|
||||||
border: 1px solid #e8ebed;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sites__actions {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
align-items: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0px 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sites__actions__new-site {
|
|
||||||
min-height: 36px;
|
|
||||||
background: green;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sites__actions__new-site > button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
color: white;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__container {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 10px 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 20px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__container:hover {
|
|
||||||
background: #f7f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__info--head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__info--column {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__info--column > p,
|
|
||||||
.site__info--column > a {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.site__container:visited,
|
|
||||||
.site__container {
|
|
||||||
color: black;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site__container--preview {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</html>
|
</html>
|
||||||
|
-->
|
||||||
|
|
81
templates/pages/dash/main.scss
Normal file
81
templates/pages/dash/main.scss
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
.sites__main {
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites__collection {
|
||||||
|
margin: auto;
|
||||||
|
width: 70%;
|
||||||
|
|
||||||
|
border: 1px solid #e8ebed;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites__actions {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0px 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites__actions__new-site {
|
||||||
|
min-height: 36px;
|
||||||
|
background: green;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites__actions__new-site > button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__container:hover {
|
||||||
|
background: #f7f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__info--head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__info--column {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__info--column > p,
|
||||||
|
.site__info--column > a {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.site__container:visited,
|
||||||
|
.site__container {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__container--preview {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue