Refactoring de l'interface : migration d'un monolithe HTMx vers un client Nuxt + serveur Axum #66

Merged
florian_briand merged 10 commits from feat/65_move_out_htmx_with_axum_backend_and_nuxt_frontend into main 2024-09-24 12:53:14 +02:00
17 changed files with 555 additions and 1949 deletions
Showing only changes of commit cad2390649 - Show all commits

2157
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/app",
"crates/sesam-vitale", "crates/sesam-vitale",
"crates/desktop", "crates/desktop",
"crates/utils", "crates/utils",

View File

@ -2,12 +2,13 @@
Logiciel de Pharmacie libre et open-source. Logiciel de Pharmacie libre et open-source.
## Crates ## Modules applicatifs
- `app`: Interface du logiciel, servie par un serveur web propulsé par Axum. Utilisable en mode endpoint ou encapsulé dans le client `desktop` - `crates`: Dossier racine des modules Rust
- `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app` - `crates/desktop`: Client desktop propulsé par Tauri, exposant le `frontend`
- `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...) - `crates/sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `utils`: Bibliothèque de fonctions utilitaires - `crates/utils`: Bibliothèque de fonctions utilitaires
- `frontend`: Interface web du logiciel, propulsée par Nuxt.js
## Installation ## Installation
@ -26,22 +27,21 @@ Des exemples de fichiers de configuration sont disponibles à la racine du proje
### Pré-requis ### Pré-requis
### Frontend (Nuxt + Typescript)
Le frontend est propulsé par Nuxt.js, un framework TypeScript pour Vue.js. Pour le développement, il est nécessaire d'installer les dépendances suivantes :
- [Bun](https://bun.sh/docs/installation), un gestionnaire de paquets, équivalent à `npm` en plus performant
#### Tauri CLI #### Tauri CLI
TODO: Tauri CLI, réellement nécessaire ?
La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo : La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo :
```bash ```bash
cargo install tauri-cli --version "^2.0.0-beta" cargo install tauri-cli --version "^2.0.0-rc"
``` ```
#### Tailwindcss CLI
Le CLI Tailwindcss est nécessaire pour la génération du fichier `crates/app/assets/css/style.css`.
La documentation d'installation est disponible sur le site officiel de Tailwindcss : https://tailwindcss.com/blog/standalone-cli
La version actuellement utilisée est la [`v3.4.7`](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.7)
#### SESAM-Vitale #### SESAM-Vitale
La crate `sesam-vitale` nécessite la présence des librairies dynamiques fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `sesam-vitale`. La crate `sesam-vitale` nécessite la présence des librairies dynamiques fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `sesam-vitale`.
@ -54,29 +54,17 @@ Le logiciel dans sa globalité peut être lancé via la commande suivante :
cargo tauri dev cargo tauri dev
``` ```
/!\ Attention, le lancement du client `desktop` ne génère pas le fichier `crates/app/assets/css/style.css` automatiquement pour le moment. En cas de modification des interfaces web, il est donc nécessaire de procéder à sa génération comme indiqué dans le [README](crates/app/README.md) de la crate `app`. /!\ Attention, le lancement du client `desktop` ne génère pas le fichier `frontend/assets/css/style.css` automatiquement pour le moment. En cas de modification des interfaces web, il est donc nécessaire de procéder à sa génération comme indiqué dans le [README](frontend/README.md) du module `frontend`.
// TODO: Adapter autogénération Tailwind au nouveau Tauri + Nuxt ?
Si vous souhaitez lancer les composants séparément, les indications de lancement sont disponibles dans les README des différents crates. Si vous souhaitez lancer les composants séparément, les indications de lancement sont disponibles dans les README des différents crates.
- [app](crates/app/README.md) - [frontend](frontend/README.md)
- [sesam-vitale](crates/sesam-vitale/README.md) - [sesam-vitale](crates/sesam-vitale/README.md)
## Rechargement automatique
Pour permettre de développer plus rapidement, il existe une librairie qui recompile automatiquement nos modifications en cours : [`cargo-watch`](https://github.com/watchexec/cargo-watch) permet de relancer une commande `cargo` lorsqu'un fichier est modifié (example: `cargo run` --> `cargo watch -x run`).
Voici la commande pour l'installer dans un _package_ :
```bash
cargo add cargo-watch --dev --package app
```
Le fichier [`.ignore`](./ignore) permet d'ignorer certains fichiers pour éviter de relancer la recompilation inutilement.
⚠️ La librairie n'est pas compatible avec _Windows 7_ et les versions antérieurs de _Windows_.
## Build ## Build
Packager le client desktop Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI Tauri, qui se charge de gérer le build du `frontend` et son intégration au bundle :
```bash ```bash
cargo tauri build cargo tauri build

View File

@ -10,16 +10,11 @@ name = "desktop_lib"
crate-type = ["lib", "cdylib", "staticlib"] crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies] [dependencies]
axum = "0.7.5" tauri = { version = "2.0.0-rc", features = [] }
tauri = { version = "2.0.0-beta", features = [] } tauri-plugin-shell = "2.0.0-rc"
tower = "0.4.13" serde = { version = "1", features = ["derive"] }
tokio = "1.39.1" serde_json = "1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"
thiserror = "1.0.63"

View File

@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}

View File

@ -1,88 +1,14 @@
use axum::body::{to_bytes, Body}; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
use axum::Router; #[tauri::command]
use bytes::Bytes; fn greet(name: &str) -> String {
use http::{request, response, Request, Response}; format!("Hello, {}! You've been greeted from Rust!", name)
use std::path::PathBuf;
use std::sync::Arc;
use tauri::path::BaseDirectory;
use tauri::Manager;
use thiserror::Error;
use tokio::sync::{Mutex, MutexGuard};
use tower::{Service, ServiceExt};
#[derive(Error, Debug)]
pub enum DesktopError {
#[error("Axum error:\n{0}")]
Axum(#[from] axum::Error),
#[error("Infallible error")]
Infallible(#[from] std::convert::Infallible),
}
/// Process requests sent to Tauri (with the `axum://` protocol) and handle them with Axum
/// When an error occurs, this function is expected to panic, which should result in a 500 error
/// being sent to the client, so we let the client handle the error recovering
async fn process_tauri_request(
tauri_request: Request<Vec<u8>>,
mut router: MutexGuard<'_, Router>,
) -> Result<Response<Vec<u8>>, DesktopError> {
let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts();
let axum_request: Request<Body> = Request::from_parts(parts, body.into());
let axum_response: Response<Body> = router
.as_service()
.ready()
.await
.map_err(DesktopError::Infallible)?
.call(axum_request)
.await
.map_err(DesktopError::Infallible)?;
let (parts, body): (response::Parts, Body) = axum_response.into_parts();
let body: Bytes = to_bytes(body, usize::MAX).await?;
let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into());
Ok(tauri_response)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .plugin(tauri_plugin_shell::init())
let assets_path: PathBuf = app .invoke_handler(tauri::generate_handler![greet])
.path()
.resolve("assets", BaseDirectory::Resource)
.expect("Assets path should be resolvable");
// Adds Axum router to application state
// This makes it so we can retrieve it from any app instance (see bellow)
let router = Arc::new(Mutex::new(app::get_router(&assets_path)));
app.manage(router);
Ok(())
})
.register_asynchronous_uri_scheme_protocol("axum", move |app, request, responder| {
// Retrieve the router from the application state and clone it for the async block
let router = Arc::clone(&app.state::<Arc<Mutex<axum::Router>>>());
// Spawn a new async task to process the request
tauri::async_runtime::spawn(async move {
let router = router.lock().await;
match process_tauri_request(request, router).await {
Ok(response) => responder.respond(response),
Err(err) => {
let body = format!("Failed to process an axum:// request:\n{}", err);
responder.respond(
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CONTENT_TYPE, "text/plain")
.body::<Vec<u8>>(body.into())
.expect("BAD_REQUEST response should be valid"),
)
}
}
});
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,20 +1,24 @@
{ {
"productName": "Logiciel Pharma", "$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "Chrys4lide LGO",
"version": "0.0.1", "version": "0.0.1",
"identifier": "org.p4pillon.pharma.desktop", "identifier": "org.p4pillon.chrys4lide.lgo",
"build": { "build": {
"beforeDevCommand": { "beforeDevCommand": {
"cwd": "../app", "cwd": "../../frontend",
"script": "cargo run" "script": "bun run dev"
}, },
"devUrl": "http://localhost:3000", "devUrl": "http://localhost:1420",
"frontendDist": "axum://place.holder/" "beforeBuildCommand": {
"cwd": "../../frontend",
"script": "bun run generate"
},
"frontendDist": "../../frontend/dist"
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "Logiciel Pharma", "title": "Chrys4lide | LG0",
"width": 800, "width": 800,
"height": 600 "height": 600
} }
@ -25,9 +29,6 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"resources": {
"../app/assets/": "./assets/"
},
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
@ -37,5 +38,4 @@
"icons/icon.ico" "icons/icon.ico"
] ]
} }
}
}

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

77
frontend/README.md Normal file
View File

@ -0,0 +1,77 @@
# Nuxt 3 Minimal Starter
TODO : Faire un vrai README pour `frontend` (Nuxt 3)
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

6
frontend/app.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>

BIN
frontend/bun.lockb Executable file

Binary file not shown.

29
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,29 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
// Enables the development server to be discoverable by other devices for mobile development
devServer: { host: '0.0.0.0', port: 1420 },
devtools: { enabled: true },
// Disable SSR for Tauri
ssr: false,
vite: {
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
// Additional environment variables can be found at
// https://v2.tauri.app/reference/environment-variables/
envPrefix: ['VITE_', 'TAURI_'],
server: {
// Tauri requires a consistent port
strictPort: true,
hmr: {
// Use websocket for mobile hot reloading
protocol: 'ws',
// Make sure it's available on the network
host: '0.0.0.0',
// Use a specific port for hmr
port: 5183,
},
},
}
})

17
frontend/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"generate": "nuxi generate",
"preview": "nuxi preview",
"postinstall": "nuxi prepare"
},
"dependencies": {
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
}
Review

faudrait surement mettre les version sur vue et vue-router plutôt que latest

faudrait surement mettre les version sur vue et vue-router plutôt que latest
Review

Je n'ai rien trouvé à ce sujet dans la documentation. J'ai posé la question : https://github.com/nuxt/nuxt/discussions/29137

Je n'ai rien trouvé à ce sujet dans la documentation. J'ai posé la question : https://github.com/nuxt/nuxt/discussions/29137
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

4
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}