Skip to content

Commit 45ccd7c

Browse files
✨ Server UI in the backend (#440)
Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
1 parent 5941cda commit 45ccd7c

File tree

13 files changed

+1364
-27
lines changed

13 files changed

+1364
-27
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"server/server",
1414
"server/cli",
1515
"server/signature",
16+
"server/ui/crate",
1617
]
1718

1819
[workspace.package]
@@ -55,6 +56,7 @@ openubl-common = { path = "./server/common" }
5556
openubl-storage = { path = "./server/storage" }
5657
openubl-api = { path = "./server/api" }
5758
openubl-server = { path = "./server/server" }
59+
openubl-ui = { path = "./server/ui/crate" }
5860

5961
sea-orm = "1"
6062
sea-query = "0.32"
@@ -76,5 +78,6 @@ actix-web = "4.9"
7678
actix-web-httpauth = "0.8"
7779
actix-4-jwt-auth = "1.2"
7880
actix-multipart = "0.7"
81+
actix-web-static-files = "4"
7982

8083
[patch.crates-io]

server/server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ openubl-api = { workspace = true }
1010
openubl-common = { workspace = true }
1111
openubl-entity = { workspace = true }
1212
openubl-storage = { workspace = true }
13+
openubl-ui = { workspace = true }
1314

1415
xhandler = { workspace = true }
1516

@@ -30,4 +31,5 @@ utoipa-swagger-ui = { workspace = true, features = ["actix-web"] }
3031
actix-web-httpauth = { workspace = true }
3132
actix-4-jwt-auth = { workspace = true }
3233
actix-multipart = { workspace = true }
34+
actix-web-static-files = { workspace = true }
3335
minio = { workspace = true }

server/server/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fmt::Debug;
23
use std::process::ExitCode;
34
use std::sync::Arc;
@@ -15,10 +16,41 @@ use crate::server::credentials::{
1516
};
1617
use crate::server::document::{get_document_file, list_documents, send_document};
1718
use crate::server::health;
19+
use actix_web_static_files::{deps::static_files::Resource, ResourceFiles};
20+
use openubl_ui::{openubl_ui, UI};
1821

1922
mod dto;
2023
pub mod server;
2124

25+
pub struct UiResources {
26+
resources: HashMap<&'static str, Resource>,
27+
}
28+
29+
impl UiResources {
30+
pub fn new(ui: &UI) -> anyhow::Result<Self> {
31+
Ok(Self {
32+
resources: openubl_ui(ui)?,
33+
})
34+
}
35+
36+
pub fn resources(&self) -> HashMap<&'static str, Resource> {
37+
self.resources
38+
.iter()
39+
.map(|(k, v)| {
40+
// unfortunately, we can't just clone, but we can do it ourselves
41+
(
42+
*k,
43+
Resource {
44+
data: v.data,
45+
modified: v.modified,
46+
mime_type: v.mime_type,
47+
},
48+
)
49+
})
50+
.collect()
51+
}
52+
}
53+
2254
/// Run the API server
2355
#[derive(clap::Args, Debug)]
2456
pub struct ServerRun {
@@ -39,6 +71,12 @@ impl ServerRun {
3971
pub async fn run(self) -> anyhow::Result<ExitCode> {
4072
env_logger::init();
4173

74+
// UI
75+
let ui = UI {
76+
version: "".to_string(),
77+
};
78+
let ui = Arc::new(UiResources::new(&ui)?);
79+
4280
// Database
4381
let system = match self.bootstrap {
4482
true => InnerSystem::bootstrap(&self.database).await?,
@@ -56,6 +94,7 @@ impl ServerRun {
5694
.wrap(Logger::default())
5795
.app_data(TempFileConfig::default())
5896
.configure(configure)
97+
.service(ResourceFiles::new("/", ui.resources()).resolve_not_found_to(""))
5998
})
6099
.bind(self.bind_addr)?
61100
.run()

server/ui/crate/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "openubl-ui"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
7+
[dependencies]
8+
anyhow = { workspace = true }
9+
base64 = { workspace = true }
10+
serde = { workspace = true, features = ["derive"] }
11+
serde_json = { workspace = true }
12+
static-files = { workspace = true }
13+
tera = { workspace = true }
14+
15+
[build-dependencies]
16+
static-files = { workspace = true }

server/ui/crate/build.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::path::Path;
2+
use std::process::{Command, ExitStatus};
3+
use std::{fs, io};
4+
5+
use static_files::resource_dir;
6+
7+
static UI_DIR: &str = "../";
8+
static UI_DIST_DIR: &str = "../dist";
9+
static STATIC_DIR: &str = "target/generated";
10+
11+
#[cfg(windows)]
12+
static NPM_CMD: &str = "npm.cmd";
13+
#[cfg(not(windows))]
14+
static NPM_CMD: &str = "npm";
15+
16+
fn main() {
17+
println!("Build UI - build.rs!");
18+
19+
build_ui().expect("Error while building UI");
20+
21+
copy_dir_all(UI_DIST_DIR, STATIC_DIR).expect("Failed to copy UI files");
22+
resource_dir("./target/generated").build().unwrap();
23+
}
24+
25+
fn install_ui_deps() -> io::Result<ExitStatus> {
26+
if !Path::new("../node_modules").exists() {
27+
println!("Installing node dependencies...");
28+
Command::new(NPM_CMD)
29+
.args(["ci"])
30+
.current_dir(UI_DIR)
31+
.status()
32+
} else {
33+
Ok(ExitStatus::default())
34+
}
35+
}
36+
37+
fn build_ui() -> io::Result<ExitStatus> {
38+
if !Path::new(UI_DIST_DIR).exists() || Path::new(UI_DIST_DIR).read_dir()?.next().is_none() {
39+
install_ui_deps()?;
40+
41+
println!("Building UI...");
42+
Command::new(NPM_CMD)
43+
.args(["run", "build"])
44+
.current_dir(UI_DIR)
45+
.status()
46+
} else {
47+
println!("Using previously built UI files");
48+
Ok(ExitStatus::default())
49+
}
50+
}
51+
52+
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
53+
fs::create_dir_all(&dst)?;
54+
for entry in fs::read_dir(src)? {
55+
let entry = entry?;
56+
let ty = entry.file_type()?;
57+
if ty.is_dir() {
58+
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
59+
} else {
60+
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
61+
}
62+
}
63+
Ok(())
64+
}

server/ui/crate/src/lib.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
2+
3+
use anyhow::{anyhow, bail, Context};
4+
use base64::prelude::BASE64_STANDARD;
5+
use base64::Engine;
6+
use serde::Serialize;
7+
use static_files::resource::new_resource;
8+
use static_files::Resource;
9+
use std::collections::HashMap;
10+
use std::str::from_utf8;
11+
use std::sync::OnceLock;
12+
13+
#[derive(Serialize, Clone, Default)]
14+
pub struct UI {
15+
#[serde(rename(serialize = "VERSION"))]
16+
pub version: String,
17+
}
18+
19+
pub fn openubl_ui_resources() -> HashMap<&'static str, Resource> {
20+
let mut resources = generate();
21+
if let Some(index) = resources.get("index.html.ejs") {
22+
resources.insert(
23+
"index.html",
24+
new_resource(index.data, index.modified, "text/html"),
25+
);
26+
}
27+
28+
resources
29+
}
30+
31+
pub fn generate_index_html(ui: &UI, template_file: String) -> tera::Result<String> {
32+
let template = template_file.replace("<%=", "{{").replace("%>", "}}");
33+
34+
let env_json = serde_json::to_string(&ui)?;
35+
let env_base64 = BASE64_STANDARD.encode(env_json.as_bytes());
36+
37+
let mut context = tera::Context::new();
38+
context.insert("_env", &env_base64);
39+
40+
tera::Tera::one_off(&template, &context, true)
41+
}
42+
43+
pub fn openubl_ui(ui: &UI) -> anyhow::Result<HashMap<&'static str, Resource>> {
44+
let mut resources = generate();
45+
46+
let template_file = resources.get("index.html.ejs");
47+
48+
let result = INDEX_HTML.get_or_init(|| {
49+
if let Some(template_file) = template_file {
50+
let modified = template_file.modified;
51+
let template_file =
52+
from_utf8(template_file.data).context("cannot interpret template as UTF-8")?;
53+
Ok((
54+
generate_index_html(ui, template_file.to_string())
55+
.expect("cannot generate index.html"),
56+
modified,
57+
))
58+
} else {
59+
bail!("Missing template");
60+
}
61+
});
62+
63+
let (index_html, modified) = match result {
64+
Ok((index_html, modified)) => (index_html.as_bytes(), *modified),
65+
Err(err) => return Err(anyhow!(err)),
66+
};
67+
68+
resources.insert("", new_resource(index_html, modified, "text/html"));
69+
70+
Ok(resources)
71+
}
72+
73+
static INDEX_HTML: OnceLock<anyhow::Result<(String, u64)>> = OnceLock::new();

server/ui/index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
6+
<meta name="theme-color" content="#000000"/>
7+
<meta name="version" content="2.0.0"/>
8+
<link rel="manifest" href="/manifest.json"/>
9+
<link rel="icon" href="/favicon.ico"/>
10+
<base href="/"/>
11+
<script>
12+
window._env = "<%= _env %>";
13+
</script>
14+
</head>
15+
<body>
16+
<noscript>You need to enable JavaScript to run this app.</noscript>
17+
<div id="root" style="height: 100%"></div>
18+
</body>
19+
</html>

0 commit comments

Comments
 (0)