Skip to content

Commit

Permalink
Provide a wrapper Rust crate around the static-built UI. (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob McWhirter authored May 9, 2024
1 parent b17ed55 commit 453a502
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/ci-actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ jobs:
run: npm run build
- name: Test
run: npm run test -- --coverage --watchAll=false
- name: Crate Format
working-directory: ./crate
run: cargo fmt --check
- name: Crate Check
working-directory: ./crate
run: cargo check
- name: Crate Clippy
working-directory: ./crate
run: cargo clippy --all-targets --all-features -- -D warnings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ npm-debug.log*

# Intellij IDEA
.idea/

crate/target
crate/Cargo.lock
14 changes: 14 additions & 0 deletions crate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "trustify-ui"
version = "0.1.0"
edition = "2021"

[dependencies]
static-files = "0.2.1"
serde_json = "1.0.117"
tera = "1.19.1"
base64 = "0.22.1"
serde = { version = "1.0.201", features = ["derive"] }

[build-dependencies]
static-files = "0.2.1"
69 changes: 69 additions & 0 deletions crate/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use static_files::resource_dir;
use std::path::Path;
use std::process::{Command, ExitStatus};
use std::{fs, io};

static UI_DIR: &str = "../";
static UI_DIR_SRC: &str = "../src";
static UI_DIST_DIR: &str = "../client/dist";
static STATIC_DIR: &str = "target/generated";

#[cfg(windows)]
static NPM_CMD: &str = "npm.cmd";
#[cfg(not(windows))]
static NPM_CMD: &str = "npm";

fn main() {
println!("Build Trustify - build.rs!");

println!("cargo:rerun-if-changed={}", UI_DIR_SRC);

let build_ui_status = install_ui_deps()
.and_then(|_| build_ui())
.and_then(|_| copy_dir_all(UI_DIST_DIR, STATIC_DIR));

match build_ui_status {
Ok(_) => println!("UI built successfully"),
Err(_) => panic!("Error while building UI"),
}

resource_dir("./target/generated").build().unwrap();
}

fn install_ui_deps() -> io::Result<ExitStatus> {
if !Path::new("../node_modules").exists() {
println!("Installing node dependencies...");
Command::new(NPM_CMD)
.args(["clean-install", "--ignore-scripts"])
.current_dir(UI_DIR)
.status()
} else {
Ok(ExitStatus::default())
}
}

fn build_ui() -> io::Result<ExitStatus> {
if !Path::new(STATIC_DIR).exists() || Path::new(STATIC_DIR).read_dir()?.next().is_none() {
println!("Building UI...");
Command::new(NPM_CMD)
.args(["run", "build", "--dev"])
.current_dir(UI_DIR)
.status()
} else {
Ok(ExitStatus::default())
}
}

fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
108 changes: 108 additions & 0 deletions crate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
include!(concat!(env!("OUT_DIR"), "/generated.rs"));

use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use serde::Serialize;
use serde_json::Value;
use static_files::resource::new_resource;
use static_files::Resource;
use std::collections::HashMap;
use std::str::from_utf8;
use std::sync::OnceLock;

#[derive(Serialize, Clone, Default)]
pub struct UI {
#[serde(rename(serialize = "VERSION"))]
pub version: String,

#[serde(rename(serialize = "AUTH_REQUIRED"))]
pub auth_required: String,

#[serde(rename(serialize = "OIDC_SERVER_URL"))]
pub oidc_server_url: String,

#[serde(rename(serialize = "OIDC_CLIENT_ID"))]
pub oidc_client_id: String,

#[serde(rename(serialize = "OIDC_SCOPE"))]
pub oidc_scope: String,

#[serde(rename(serialize = "ANALYTICS_ENABLED"))]
pub analytics_enabled: String,

#[serde(rename(serialize = "ANALYTICS_WRITE_KEY"))]
pub analytics_write_key: String,
}

pub fn trustify_ui_resources() -> HashMap<&'static str, Resource> {
let mut resources = generate();
if let Some(index) = resources.get("index.html.ejs") {
resources.insert(
"index.html",
new_resource(index.data, index.modified, "text/html"),
);
}

resources
}

pub fn generate_index_html(
ui: &UI,
template_file: String,
branding_file_content: String,
) -> tera::Result<String> {
let template = template_file
.replace("<%=", "{{")
.replace("%>", "}}")
.replace(
"?? branding.application.title",
"| default(value=branding.application.title)",
)
.replace(
"?? branding.application.title",
"| default(value=branding.application.title)",
);

let env_json = serde_json::to_string(&ui)?;
let env_base64 = BASE64_STANDARD.encode(env_json.as_bytes());

let branding: Value = serde_json::from_str(&branding_file_content)?;

let mut context = tera::Context::new();
context.insert("_env", &env_base64);
context.insert("branding", &branding);

tera::Tera::one_off(&template, &context, true)
}

pub fn trustify_ui(ui: &UI) -> HashMap<&'static str, Resource> {
let mut resources = generate();

let template_file = resources.get("index.html.ejs");
let branding_file_content = resources.get("branding/strings.json");

let index_html = INDEX_HTML.get_or_init(|| {
if let (Some(template_file), Some(branding_file_content)) =
(template_file, branding_file_content)
{
let template_file =
from_utf8(template_file.data).expect("cannot interpret template as UTF-8");
let branding_file_content =
from_utf8(branding_file_content.data).expect("cannot interpret branding as UTF-8");
generate_index_html(
ui,
template_file.to_string(),
branding_file_content.to_string(),
)
.expect("cannot generate index.html")
} else {
"Something went wrong".to_string()
}
});

resources.insert("", new_resource(index_html.as_bytes(), 0, "text/html"));

resources
}

static INDEX_HTML: OnceLock<String> = OnceLock::new();

0 comments on commit 453a502

Please sign in to comment.