superviseur_webui/
lib.rs

1use actix_cors::Cors;
2use actix_web::{
3    guard,
4    http::header::HOST,
5    web::{self, Data},
6    App, HttpRequest, HttpResponse, HttpServer, Responder, Result,
7};
8use async_graphql::{http::GraphiQLSource, Schema};
9use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
10use mime_guess::from_path;
11use rust_embed::RustEmbed;
12use std::{
13    collections::HashMap,
14    sync::{Arc, Mutex},
15};
16use tokio::sync::mpsc;
17
18use superviseur_core::core::Superviseur;
19
20use superviseur_graphql::{
21    schema::{Mutation, Query, Subscription},
22    SuperviseurSchema,
23};
24use superviseur_provider::kv::kv::Provider;
25use superviseur_types::{command::SuperviseurCommand, events::ProcessEvent, process::Process};
26
27#[derive(RustEmbed)]
28#[folder = "webui/build/"]
29struct Asset;
30
31fn handle_embedded_file(path: &str) -> HttpResponse {
32    match Asset::get(path) {
33        Some(content) => HttpResponse::Ok()
34            .content_type(from_path(path).first_or_octet_stream().as_ref())
35            .body(content.data.into_owned()),
36        None => HttpResponse::NotFound().body("404 Not Found"),
37    }
38}
39
40#[actix_web::get("/{_:.*}")]
41async fn dist(path: web::Path<String>) -> impl Responder {
42    handle_embedded_file(path.as_str())
43}
44
45#[actix_web::get("/")]
46async fn index() -> impl Responder {
47    handle_embedded_file("index.html")
48}
49
50#[actix_web::get("/projects/{_:.*}")]
51async fn index_projects() -> impl Responder {
52    handle_embedded_file("index.html")
53}
54
55#[actix_web::post("/graphql")]
56async fn index_graphql(
57    schema: web::Data<SuperviseurSchema>,
58    req: GraphQLRequest,
59) -> GraphQLResponse {
60    schema.execute(req.into_inner()).await.into()
61}
62
63#[actix_web::get("/graphiql")]
64async fn index_graphiql(req: HttpRequest) -> Result<HttpResponse> {
65    let host = req
66        .headers()
67        .get(HOST)
68        .unwrap()
69        .to_str()
70        .unwrap()
71        .split(":")
72        .next()
73        .unwrap();
74
75    const PORT: u16 = 5478;
76    let graphql_endpoint = format!("http://{}:{}/graphql", host, PORT);
77    let ws_endpoint = format!("ws://{}:{}/graphql", host, PORT);
78    Ok(HttpResponse::Ok()
79        .content_type("text/html; charset=utf-8")
80        .body(
81            GraphiQLSource::build()
82                .endpoint(&graphql_endpoint)
83                .subscription_endpoint(&ws_endpoint)
84                .finish(),
85        ))
86}
87
88async fn index_ws(
89    schema: web::Data<SuperviseurSchema>,
90    req: HttpRequest,
91    payload: web::Payload,
92) -> Result<HttpResponse> {
93    GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload)
94}
95
96pub async fn start_webui(
97    config_file_path: String,
98    cmd_tx: mpsc::UnboundedSender<SuperviseurCommand>,
99    event_tx: mpsc::UnboundedSender<ProcessEvent>,
100    superviseur: Superviseur,
101    processes: Arc<Mutex<Vec<(Process, String)>>>,
102    provider: Arc<Provider>,
103    project_map: Arc<Mutex<HashMap<String, String>>>,
104) -> std::io::Result<()> {
105    let addr = format!("0.0.0.0:{}", 5478);
106
107    let schema = Schema::build(
108        Query::default(),
109        Mutation::default(),
110        Subscription::default(),
111    )
112    .data(config_file_path)
113    .data(superviseur)
114    .data(cmd_tx)
115    .data(event_tx)
116    .data(processes)
117    .data(provider)
118    .data(project_map)
119    .finish();
120
121    HttpServer::new(move || {
122        let cors = Cors::permissive();
123        App::new()
124            .app_data(Data::new(schema.clone()))
125            .wrap(cors)
126            .service(index_graphql)
127            .service(index_graphiql)
128            .service(
129                web::resource("/graphql")
130                    .guard(guard::Get())
131                    .guard(guard::Header("upgrade", "websocket"))
132                    .to(index_ws),
133            )
134            .service(index)
135            .service(index_projects)
136            .service(dist)
137    })
138    .bind(addr)?
139    .run()
140    .await
141}