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}