7 Commits

Author SHA1 Message Date
968911e489 Merge pull request 'setup frontend' (#5) from feature/frontend-setup into master
All checks were successful
Test / verify-generated-code (push) Successful in 48s
Test / test-frontend (push) Successful in 21s
Test / frontend-build (push) Successful in 23s
Test / test (push) Successful in 1m10s
Test / lint (push) Successful in 1m10s
Reviewed-on: #5
2025-12-02 20:50:06 +08:00
GW_MC
c79ef265db use workflow specific cache instead of artifact
All checks were successful
Test / verify-generated-code (pull_request) Successful in 54s
Test / test-frontend (pull_request) Successful in 23s
Test / frontend-build (pull_request) Successful in 25s
Test / test (pull_request) Successful in 1m13s
Test / lint (pull_request) Successful in 1m12s
2025-12-02 20:44:18 +08:00
GW_MC
0374d63efe Add frontend build artifact handling and linting steps to CI workflow
Some checks failed
Test / verify-generated-code (pull_request) Successful in 55s
Test / test-frontend (pull_request) Successful in 20s
Test / frontend-build (pull_request) Failing after 38s
Test / test (pull_request) Has been skipped
Test / lint (pull_request) Has been skipped
2025-12-02 20:18:48 +08:00
GW_MC
06cabb0e18 Add catch-all 404 route and NotFound component to frontend routing
Some checks failed
Test / verify-generated-code (pull_request) Successful in 49s
Test / test (pull_request) Failing after 1m10s
Test / test-frontend (pull_request) Successful in 23s
Test / lint (pull_request) Failing after 1m8s
2025-12-02 19:51:49 +08:00
GW_MC
051951dc44 Added frontend testing script and job
Some checks failed
Test / verify-generated-code (pull_request) Successful in 53s
Test / test (pull_request) Failing after 1m9s
Test / test-frontend (pull_request) Successful in 1m7s
Test / lint (pull_request) Failing after 1m8s
2025-12-02 19:48:25 +08:00
GW_MC
edbcdaeff4 Implement frontend routing and API fallback handling; add dependencies for include_dir and mime_guess
Some checks failed
Test / verify-generated-code (pull_request) Successful in 7m59s
Test / test (pull_request) Failing after 1m12s
Test / lint (pull_request) Failing after 1m11s
2025-12-02 19:25:46 +08:00
GW_MC
27173c01da Added basic frontend setup 2025-12-02 19:18:14 +08:00
20 changed files with 5179 additions and 2 deletions

View File

@@ -44,6 +44,7 @@ jobs:
fi fi
test: test:
needs: frontend-build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -54,10 +55,19 @@ jobs:
- name: Setup Rust, checkout and restore caches - name: Setup Rust, checkout and restore caches
uses: ./.github/actions/setup-rust uses: ./.github/actions/setup-rust
- name: Restore frontend build cache
uses: actions/cache@v4
with:
path: apps/frontend/build
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
restore-keys: |
frontend-build-${{ runner.os }}-
- name: Run tests - name: Run tests
run: cargo test --all-features run: cargo test --all-features
lint: lint:
needs: frontend-build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -70,8 +80,84 @@ jobs:
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- name: Restore frontend build cache
uses: actions/cache@v4
with:
path: apps/frontend/build
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
restore-keys: |
frontend-build-${{ runner.os }}-
- name: Run clippy - name: Run clippy
run: cargo clippy --all-features -- -D warnings run: cargo clippy --all-features -- -D warnings
- name: Check code formatting - name: Check code formatting
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
test-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: apps/frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: |
cd apps/frontend
pnpm install
- name: Run frontend tests
run: |
cd apps/frontend
pnpm test
frontend-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: apps/frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: |
cd apps/frontend
pnpm install
- name: Build frontend
run: |
cd apps/frontend
pnpm build
- name: Cache frontend build
uses: actions/cache@v4
with:
path: apps/frontend/build
key: frontend-build-${{ runner.os }}-run-${{ github.run_id }}
restore-keys: |
frontend-build-${{ runner.os }}-

57
Cargo.lock generated
View File

@@ -1135,6 +1135,25 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@@ -1277,6 +1296,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@@ -1488,6 +1508,25 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -1690,6 +1729,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.0" version = "1.1.0"
@@ -3696,6 +3745,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"
@@ -4275,7 +4330,9 @@ dependencies = [
"chrono", "chrono",
"config", "config",
"database", "database",
"include_dir",
"migration", "migration",
"mime_guess",
"sea-orm", "sea-orm",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -7,7 +7,7 @@ edition = "2024"
database = { path = "../../public/database" } database = { path = "../../public/database" }
migration = { path = "../../public/migration" } migration = { path = "../../public/migration" }
axum = { version = "0.8.7", features = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"]} axum = { version = "0.8.7", features = ["form", "http1", "http2", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"]}
async-trait = { version = "0.1.89" } async-trait = { version = "0.1.89" }
chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] } chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] }
config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] } config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] }
@@ -18,3 +18,5 @@ tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi"
serde_json = { version = "1.0.145", features = ["std"] } serde_json = { version = "1.0.145", features = ["std"] }
serde = { version = "1.0.228", features = ["std", "derive"] } serde = { version = "1.0.228", features = ["std", "derive"] }
sea-orm = { workspace = true } sea-orm = { workspace = true }
include_dir = { version = "0.7.4" }
mime_guess = { version = "2.0.5" }

View File

@@ -1,3 +1,6 @@
mod api;
mod view;
use std::sync::Arc; use std::sync::Arc;
use axum::{Extension, Router}; use axum::{Extension, Router};
@@ -25,6 +28,10 @@ pub struct AppService {
pub fn get_root_router(state: impl Into<Arc<AppState>>) -> Router { pub fn get_root_router(state: impl Into<Arc<AppState>>) -> Router {
let mut router = Router::new(); let mut router = Router::new();
router = router
.nest("/api", api::get_api_router())
.merge(view::get_view_router());
router = middlewares::apply_root_middleware(router); router = middlewares::apply_root_middleware(router);
router = router.layer(Extension(state.into())); router = router.layer(Extension(state.into()));

View File

@@ -0,0 +1,11 @@
use axum::{Router, response::IntoResponse, routing::any};
pub fn get_api_router() -> Router {
Router::new()
// explicit fallback for unmatched API routes
.route("/{*wildcard}", any(api_fallback_handler))
}
async fn api_fallback_handler() -> impl IntoResponse {
(axum::http::StatusCode::NOT_FOUND, "API route not found").into_response()
}

View File

@@ -0,0 +1,71 @@
use axum::{
Router,
body::Bytes,
extract::Path,
http::{StatusCode, header},
response::IntoResponse,
routing::{MethodRouter, get},
};
use include_dir::{Dir, include_dir};
use mime_guess::from_path;
static DIST_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/build/client");
const INDEX_HTML_PATH: &str = "index.html";
const INDEX_FILE_NOT_FOUND_HTML: &str = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>
<p>The requested resource was not found on this server. Possibly the frontend build is missing or corrupted.</p>
</body>
</html>
"#;
pub fn get_view_router() -> Router {
Router::new()
// Serve the root index.html
.route("/", get(root_view_handler))
.route(
"/{*wildcard}",
MethodRouter::new()
.get(|Path(path): Path<String>| async move { view_handler(Some(path)).await }),
)
}
async fn root_view_handler() -> impl IntoResponse {
view_handler(None).await
}
async fn view_handler(path: Option<String>) -> impl IntoResponse {
// If path is empty, serve index.html
let incoming_path = if let Some(p) = path {
p.trim_start_matches('/').to_string()
} else {
INDEX_HTML_PATH.to_string()
};
let path = match DIST_DIR.get_file(&incoming_path) {
Some(_) => incoming_path,
None => INDEX_HTML_PATH.to_string(),
};
match DIST_DIR.get_file(&path) {
Some(file) => {
let mime = from_path(&path).first_or_octet_stream();
let body: Bytes = Bytes::copy_from_slice(file.contents());
([(header::CONTENT_TYPE, mime.as_ref())], body).into_response()
}
// This should never happen, but just in case...
None => (
StatusCode::NOT_FOUND,
[(header::CONTENT_TYPE, "text/plain")],
Bytes::from(INDEX_FILE_NOT_FOUND_HTML),
)
.into_response(),
}
}

View File

@@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

7
apps/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.DS_Store
.env
/node_modules/
# React Router
/.react-router/
/build/

15
apps/frontend/app/app.css Normal file
View File

@@ -0,0 +1,15 @@
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html,
body {
@apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}

View File

@@ -0,0 +1,59 @@
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
import type { Route } from './+types/root';
import './app.css';
import { Theme } from '@radix-ui/themes';
export const links: Route.LinksFunction = () => [];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return (
<Theme>
<Outlet />
</Theme>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}

View File

@@ -0,0 +1,7 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
// catch-all 404 route
route('*', 'routes/404.tsx'),
] satisfies RouteConfig;

View File

@@ -0,0 +1,3 @@
export default function NotFound() {
return <h1>404 - Not Found</h1>;
}

View File

@@ -0,0 +1,10 @@
import { Text } from '@radix-ui/themes';
import type { Route } from './+types/home';
export function meta({}: Route.MetaArgs) {
return [{ title: 'YANPM' }, { name: 'description', content: 'Welcome to Yet Another Nginx Proxy Manager!' }];
}
export default function Home() {
return <Text>Welcome to Yet Another Nginx Proxy Manager!</Text>;
}

View File

@@ -0,0 +1,34 @@
{
"name": "frontend",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
"test": "echo \"No tests specified\" && exit 0"
},
"dependencies": {
"@radix-ui/themes": "^3.2.1",
"@react-router/node": "^7.9.2",
"@react-router/serve": "^7.9.2",
"isbot": "^5.1.31",
"radix-ui": "^1.4.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.2"
},
"devDependencies": {
"@react-router/dev": "^7.9.2",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"dotenv": "^17.2.3",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}

4748
apps/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,5 @@
import type { Config } from '@react-router/dev/config';
export default {
ssr: false,
} satisfies Config;

View File

@@ -0,0 +1,22 @@
{
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}

View File

@@ -0,0 +1,9 @@
import { reactRouter } from '@react-router/dev/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
appType: 'spa',
});

View File

@@ -40,3 +40,23 @@ migrate *args:
generate-entity: generate-entity:
# delegate to cli # delegate to cli
just cli db:migrate_and_generate --output-path ../../public/database/src/generated/entities just cli db:migrate_and_generate --output-path ../../public/database/src/generated/entities
build-frontend:
# build frontend assets
cd apps/frontend && \
pnpm build
build-backend:
# build backend server
cd apps/api && \
cargo build --release
build: build-frontend build-backend
act *args:
if [ -n "{{args}}" ]; then \
act {{args}} --artifact-server-path /tmp/artifacts; \
else \
act; \
fi