Managed to load server configuration from WebUI
This commit is contained in:
		
							
								
								
									
										16
									
								
								remote_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								remote_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -19,6 +19,21 @@ dependencies = [
 | 
				
			|||||||
 "tracing",
 | 
					 "tracing",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "actix-cors"
 | 
				
			||||||
 | 
					version = "0.7.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "actix-utils",
 | 
				
			||||||
 | 
					 "actix-web",
 | 
				
			||||||
 | 
					 "derive_more",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "once_cell",
 | 
				
			||||||
 | 
					 "smallvec",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "actix-http"
 | 
					name = "actix-http"
 | 
				
			||||||
version = "3.6.0"
 | 
					version = "3.6.0"
 | 
				
			||||||
@@ -1691,6 +1706,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
 | 
				
			|||||||
name = "remote_backend"
 | 
					name = "remote_backend"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "actix-cors",
 | 
				
			||||||
 "actix-identity",
 | 
					 "actix-identity",
 | 
				
			||||||
 "actix-remote-ip",
 | 
					 "actix-remote-ip",
 | 
				
			||||||
 "actix-session",
 | 
					 "actix-session",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ actix-web = "4.5.1"
 | 
				
			|||||||
actix-remote-ip = "0.1.0"
 | 
					actix-remote-ip = "0.1.0"
 | 
				
			||||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
 | 
					actix-session = { version = "0.9.0", features = ["cookie-session"] }
 | 
				
			||||||
actix-identity = "0.7.1"
 | 
					actix-identity = "0.7.1"
 | 
				
			||||||
 | 
					actix-cors = "0.7.0"
 | 
				
			||||||
lazy_static = "1.4.0"
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
anyhow = "1.0.82"
 | 
					anyhow = "1.0.82"
 | 
				
			||||||
reqwest = { version = "0.12.4", features = ["json"] }
 | 
					reqwest = { version = "0.12.4", features = ["json"] }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ pub struct AppConfig {
 | 
				
			|||||||
    pub listen_address: String,
 | 
					    pub listen_address: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Website main origin
 | 
					    /// Website main origin
 | 
				
			||||||
    #[clap(short, long, env, default_value = "http://localhost:3000")]
 | 
					    #[clap(short, long, env, default_value = "http://localhost:5173")]
 | 
				
			||||||
    pub website_origin: String,
 | 
					    pub website_origin: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Proxy IP, might end with a star "*"
 | 
					    /// Proxy IP, might end with a star "*"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter};
 | 
				
			|||||||
use std::io::ErrorKind;
 | 
					use std::io::ErrorKind;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod auth_controller;
 | 
					pub mod auth_controller;
 | 
				
			||||||
 | 
					pub mod server_controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Custom error to ease controller writing
 | 
					/// Custom error to ease controller writing
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								remote_backend/src/controllers/server_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								remote_backend/src/controllers/server_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
 | 
					use crate::extractors::auth_extractor::AuthExtractor;
 | 
				
			||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Serialize)]
 | 
				
			||||||
 | 
					struct ServerConfig {
 | 
				
			||||||
 | 
					    authenticated: bool,
 | 
				
			||||||
 | 
					    disable_auth: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn config(auth: AuthExtractor) -> HttpResult {
 | 
				
			||||||
 | 
					    Ok(HttpResponse::Ok().json(ServerConfig {
 | 
				
			||||||
 | 
					        authenticated: auth.is_authenticated(),
 | 
				
			||||||
 | 
					        disable_auth: AppConfig::get().unsecure_disable_login,
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					use actix_cors::Cors;
 | 
				
			||||||
use actix_identity::config::LogoutBehaviour;
 | 
					use actix_identity::config::LogoutBehaviour;
 | 
				
			||||||
use actix_identity::IdentityMiddleware;
 | 
					use actix_identity::IdentityMiddleware;
 | 
				
			||||||
use actix_remote_ip::RemoteIPConfig;
 | 
					use actix_remote_ip::RemoteIPConfig;
 | 
				
			||||||
@@ -9,9 +10,9 @@ use actix_web::web::Data;
 | 
				
			|||||||
use actix_web::{web, App, HttpServer};
 | 
					use actix_web::{web, App, HttpServer};
 | 
				
			||||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
					use light_openid::basic_state_manager::BasicStateManager;
 | 
				
			||||||
use remote_backend::app_config::AppConfig;
 | 
					use remote_backend::app_config::AppConfig;
 | 
				
			||||||
use remote_backend::controllers::auth_controller;
 | 
					use remote_backend::constants;
 | 
				
			||||||
 | 
					use remote_backend::controllers::{auth_controller, server_controller};
 | 
				
			||||||
use remote_backend::middlewares::auth_middleware::AuthChecker;
 | 
					use remote_backend::middlewares::auth_middleware::AuthChecker;
 | 
				
			||||||
use remote_backend::{constants, virtweb_client};
 | 
					 | 
				
			||||||
use std::time::Duration;
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[actix_web::main]
 | 
					#[actix_web::main]
 | 
				
			||||||
@@ -40,15 +41,28 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
            .login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
 | 
					            .login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
 | 
				
			||||||
            .build();
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cors = Cors::default()
 | 
				
			||||||
 | 
					            .allowed_origin(&AppConfig::get().website_origin)
 | 
				
			||||||
 | 
					            .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"])
 | 
				
			||||||
 | 
					            .allowed_header("X-Auth-Token")
 | 
				
			||||||
 | 
					            .allow_any_header()
 | 
				
			||||||
 | 
					            .supports_credentials()
 | 
				
			||||||
 | 
					            .max_age(3600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        App::new()
 | 
					        App::new()
 | 
				
			||||||
            .wrap(Logger::default())
 | 
					            .wrap(Logger::default())
 | 
				
			||||||
            .wrap(AuthChecker)
 | 
					            .wrap(AuthChecker)
 | 
				
			||||||
            .wrap(identity_middleware)
 | 
					            .wrap(identity_middleware)
 | 
				
			||||||
            .wrap(session_mw)
 | 
					            .wrap(session_mw)
 | 
				
			||||||
 | 
					            .wrap(cors)
 | 
				
			||||||
            .app_data(state_manager.clone())
 | 
					            .app_data(state_manager.clone())
 | 
				
			||||||
            .app_data(Data::new(RemoteIPConfig {
 | 
					            .app_data(Data::new(RemoteIPConfig {
 | 
				
			||||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
					                proxy: AppConfig::get().proxy_ip.clone(),
 | 
				
			||||||
            }))
 | 
					            }))
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/api/server/config",
 | 
				
			||||||
 | 
					                web::get().to(server_controller::config),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
                "/api/auth/start_oidc",
 | 
					                "/api/auth/start_oidc",
 | 
				
			||||||
                web::get().to(auth_controller::start_oidc),
 | 
					                web::get().to(auth_controller::start_oidc),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								remote_frontend/.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								remote_frontend/.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					VITE_APP_BACKEND=http://localhost:8002/api
 | 
				
			||||||
							
								
								
									
										1797
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1797
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -10,6 +10,7 @@
 | 
				
			|||||||
    "preview": "vite preview"
 | 
					    "preview": "vite preview"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@fluentui/react-components": "^9.49.0",
 | 
				
			||||||
    "react": "^18.2.0",
 | 
					    "react": "^18.2.0",
 | 
				
			||||||
    "react-dom": "^18.2.0"
 | 
					    "react-dom": "^18.2.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1,14 @@
 | 
				
			|||||||
export function App(): React.ReactElement {}
 | 
					import { ServerApi } from "./api/ServerApi";
 | 
				
			||||||
 | 
					import { AsyncWidget } from "./widgets/AsyncWidget";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function App() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <AsyncWidget
 | 
				
			||||||
 | 
					      loadKey={1}
 | 
				
			||||||
 | 
					      errMsg="Failed to load server configuration!"
 | 
				
			||||||
 | 
					      load={ServerApi.LoadConfig}
 | 
				
			||||||
 | 
					      loadingMessage="Loading server configuration..."
 | 
				
			||||||
 | 
					      build={() => <>todo</>}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										174
									
								
								remote_frontend/src/api/ApiClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								remote_frontend/src/api/ApiClient.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
				
			|||||||
 | 
					interface RequestParams {
 | 
				
			||||||
 | 
					  uri: string;
 | 
				
			||||||
 | 
					  method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
 | 
				
			||||||
 | 
					  allowFail?: boolean;
 | 
				
			||||||
 | 
					  jsonData?: any;
 | 
				
			||||||
 | 
					  formData?: FormData;
 | 
				
			||||||
 | 
					  upProgress?: (progress: number) => void;
 | 
				
			||||||
 | 
					  downProgress?: (e: { progress: number; total: number }) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface APIResponse {
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  status: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ApiError extends Error {
 | 
				
			||||||
 | 
					  constructor(message: string, public code: number, public data: any) {
 | 
				
			||||||
 | 
					    super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class APIClient {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get backend URL
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static backendURL(): string {
 | 
				
			||||||
 | 
					    const URL = import.meta.env.VITE_APP_BACKEND ?? "";
 | 
				
			||||||
 | 
					    if (URL.length === 0) throw new Error("Backend URL undefined!");
 | 
				
			||||||
 | 
					    return URL;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Check out whether the backend is accessed through
 | 
				
			||||||
 | 
					   * HTTPS or not
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static IsBackendSecure(): boolean {
 | 
				
			||||||
 | 
					    return this.backendURL().startsWith("https");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Perform a request on the backend
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async exec(args: RequestParams): Promise<APIResponse> {
 | 
				
			||||||
 | 
					    let body: string | undefined | FormData = undefined;
 | 
				
			||||||
 | 
					    let headers: any = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // JSON request
 | 
				
			||||||
 | 
					    if (args.jsonData) {
 | 
				
			||||||
 | 
					      headers["Content-Type"] = "application/json";
 | 
				
			||||||
 | 
					      body = JSON.stringify(args.jsonData);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Form data request
 | 
				
			||||||
 | 
					    else if (args.formData) {
 | 
				
			||||||
 | 
					      body = args.formData;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const url = this.backendURL() + args.uri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data;
 | 
				
			||||||
 | 
					    let status: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make the request with XMLHttpRequest
 | 
				
			||||||
 | 
					    if (args.upProgress) {
 | 
				
			||||||
 | 
					      const res: XMLHttpRequest = await new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					        const xhr = new XMLHttpRequest();
 | 
				
			||||||
 | 
					        xhr.upload.addEventListener("progress", (e) =>
 | 
				
			||||||
 | 
					          args.upProgress!(e.loaded / e.total)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("load", () => resolve(xhr));
 | 
				
			||||||
 | 
					        xhr.addEventListener("error", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload failed"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("abort", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload aborted"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.addEventListener("timeout", () =>
 | 
				
			||||||
 | 
					          reject(new Error("File upload timeout"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        xhr.open(args.method, url, true);
 | 
				
			||||||
 | 
					        xhr.withCredentials = true;
 | 
				
			||||||
 | 
					        for (const key in headers) {
 | 
				
			||||||
 | 
					          if (headers.hasOwnProperty(key))
 | 
				
			||||||
 | 
					            xhr.setRequestHeader(key, headers[key]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        xhr.send(body);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      status = res.status;
 | 
				
			||||||
 | 
					      if (res.responseType === "json") data = JSON.parse(res.responseText);
 | 
				
			||||||
 | 
					      else data = res.response;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make the request with fetch
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      const res = await fetch(url, {
 | 
				
			||||||
 | 
					        method: args.method,
 | 
				
			||||||
 | 
					        body: body,
 | 
				
			||||||
 | 
					        headers: headers,
 | 
				
			||||||
 | 
					        credentials: "include",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Process response
 | 
				
			||||||
 | 
					      // JSON response
 | 
				
			||||||
 | 
					      if (res.headers.get("content-type") === "application/json")
 | 
				
			||||||
 | 
					        data = await res.json();
 | 
				
			||||||
 | 
					      // Text / XML response
 | 
				
			||||||
 | 
					      else if (
 | 
				
			||||||
 | 
					        ["application/xml", "text/plain"].includes(
 | 
				
			||||||
 | 
					          res.headers.get("content-type") ?? ""
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					        data = await res.text();
 | 
				
			||||||
 | 
					      // Binary file, tracking download progress
 | 
				
			||||||
 | 
					      else if (res.body !== null && args.downProgress) {
 | 
				
			||||||
 | 
					        // Track download progress
 | 
				
			||||||
 | 
					        const contentEncoding = res.headers.get("content-encoding");
 | 
				
			||||||
 | 
					        const contentLength = contentEncoding
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : res.headers.get("content-length");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const total = parseInt(contentLength ?? "0", 10);
 | 
				
			||||||
 | 
					        let loaded = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resInt = new Response(
 | 
				
			||||||
 | 
					          new ReadableStream({
 | 
				
			||||||
 | 
					            start(controller) {
 | 
				
			||||||
 | 
					              const reader = res.body!.getReader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              const read = async () => {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                  const ret = await reader.read();
 | 
				
			||||||
 | 
					                  if (ret.done) {
 | 
				
			||||||
 | 
					                    controller.close();
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  loaded += ret.value.byteLength;
 | 
				
			||||||
 | 
					                  args.downProgress!({ progress: loaded, total });
 | 
				
			||||||
 | 
					                  controller.enqueue(ret.value);
 | 
				
			||||||
 | 
					                  read();
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                  console.error(e);
 | 
				
			||||||
 | 
					                  controller.error(e);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              read();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = await resInt.blob();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Do not track progress (binary file)
 | 
				
			||||||
 | 
					      else data = await res.blob();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      status = res.status;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle expired tokens
 | 
				
			||||||
 | 
					    if (status === 412) {
 | 
				
			||||||
 | 
					      window.location.href = "/";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!args.allowFail && (status < 200 || status > 299))
 | 
				
			||||||
 | 
					      throw new ApiError("Request failed!", status, data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      data: data,
 | 
				
			||||||
 | 
					      status: status,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								remote_frontend/src/api/ServerApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								remote_frontend/src/api/ServerApi.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import { APIClient } from "./ApiClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ServerConfig {
 | 
				
			||||||
 | 
					  authenticated: boolean;
 | 
				
			||||||
 | 
					  disable_auth: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let config: ServerConfig | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ServerApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get server configuration
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async LoadConfig(): Promise<void> {
 | 
				
			||||||
 | 
					    config = (
 | 
				
			||||||
 | 
					      await APIClient.exec({
 | 
				
			||||||
 | 
					        uri: "/server/config",
 | 
				
			||||||
 | 
					        method: "GET",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    ).data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get cached configuration
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static get Config(): ServerConfig {
 | 
				
			||||||
 | 
					    if (config === null) throw new Error("Missing configuration!");
 | 
				
			||||||
 | 
					    return config;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,14 @@
 | 
				
			|||||||
body {
 | 
					body {
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  place-items: center;
 | 
					 | 
				
			||||||
  min-width: 320px;
 | 
					  min-width: 320px;
 | 
				
			||||||
  min-height: 100vh;
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#root {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,9 +2,18 @@ import React from "react";
 | 
				
			|||||||
import ReactDOM from "react-dom/client";
 | 
					import ReactDOM from "react-dom/client";
 | 
				
			||||||
import "./index.css";
 | 
					import "./index.css";
 | 
				
			||||||
import { App } from "./App";
 | 
					import { App } from "./App";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FluentProvider,
 | 
				
			||||||
 | 
					  teamsHighContrastTheme,
 | 
				
			||||||
 | 
					} from "@fluentui/react-components";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
					ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
				
			||||||
  <React.StrictMode>
 | 
					  <React.StrictMode>
 | 
				
			||||||
    <App />
 | 
					    <FluentProvider
 | 
				
			||||||
 | 
					      theme={teamsHighContrastTheme}
 | 
				
			||||||
 | 
					      style={{ display: "flex", flex: 1 }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <App />
 | 
				
			||||||
 | 
					    </FluentProvider>
 | 
				
			||||||
  </React.StrictMode>
 | 
					  </React.StrictMode>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										77
									
								
								remote_frontend/src/widgets/AsyncWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								remote_frontend/src/widgets/AsyncWidget.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					import { Button, Spinner } from "@fluentui/react-components";
 | 
				
			||||||
 | 
					import React, { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum State {
 | 
				
			||||||
 | 
					  Loading,
 | 
				
			||||||
 | 
					  Ready,
 | 
				
			||||||
 | 
					  Error,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AsyncWidget(p: {
 | 
				
			||||||
 | 
					  loadKey: any;
 | 
				
			||||||
 | 
					  load: () => Promise<void>;
 | 
				
			||||||
 | 
					  errMsg: string;
 | 
				
			||||||
 | 
					  build: () => React.ReactElement;
 | 
				
			||||||
 | 
					  ready?: boolean;
 | 
				
			||||||
 | 
					  buildLoading?: () => React.ReactElement;
 | 
				
			||||||
 | 
					  buildError?: (e: string) => React.ReactElement;
 | 
				
			||||||
 | 
					  errAdditionalElement?: () => React.ReactElement;
 | 
				
			||||||
 | 
					  loadingMessage?: string;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const [state, setState] = useState(State.Loading);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const counter = useRef<any | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const load = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      setState(State.Loading);
 | 
				
			||||||
 | 
					      await p.load();
 | 
				
			||||||
 | 
					      setState(State.Ready);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					      setState(State.Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (counter.current === p.loadKey) return;
 | 
				
			||||||
 | 
					    counter.current = p.loadKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    load();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state === State.Error)
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      p.buildError?.(p.errMsg) ?? (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            display: "flex",
 | 
				
			||||||
 | 
					            justifyContent: "center",
 | 
				
			||||||
 | 
					            alignItems: "center",
 | 
				
			||||||
 | 
					            height: "100%",
 | 
				
			||||||
 | 
					            flex: "1",
 | 
				
			||||||
 | 
					            flexDirection: "column",
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div style={{ margin: "50px" }}>{p.errMsg}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Button onClick={load}>Try again</Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {p.errAdditionalElement && p.errAdditionalElement()}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state === State.Loading || p.ready === false)
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      p.buildLoading?.() ?? (
 | 
				
			||||||
 | 
					        <Spinner
 | 
				
			||||||
 | 
					          labelPosition="below"
 | 
				
			||||||
 | 
					          label={p.loadingMessage}
 | 
				
			||||||
 | 
					          style={{ margin: "auto" }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return p.build();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user