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",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[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]]
 | 
			
		||||
name = "actix-http"
 | 
			
		||||
version = "3.6.0"
 | 
			
		||||
@@ -1691,6 +1706,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
 | 
			
		||||
name = "remote_backend"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix-cors",
 | 
			
		||||
 "actix-identity",
 | 
			
		||||
 "actix-remote-ip",
 | 
			
		||||
 "actix-session",
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ actix-web = "4.5.1"
 | 
			
		||||
actix-remote-ip = "0.1.0"
 | 
			
		||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
 | 
			
		||||
actix-identity = "0.7.1"
 | 
			
		||||
actix-cors = "0.7.0"
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
anyhow = "1.0.82"
 | 
			
		||||
reqwest = { version = "0.12.4", features = ["json"] }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ pub struct AppConfig {
 | 
			
		||||
    pub listen_address: String,
 | 
			
		||||
 | 
			
		||||
    /// 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,
 | 
			
		||||
 | 
			
		||||
    /// Proxy IP, might end with a star "*"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter};
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
 | 
			
		||||
pub mod auth_controller;
 | 
			
		||||
pub mod server_controller;
 | 
			
		||||
 | 
			
		||||
/// Custom error to ease controller writing
 | 
			
		||||
#[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::IdentityMiddleware;
 | 
			
		||||
use actix_remote_ip::RemoteIPConfig;
 | 
			
		||||
@@ -9,9 +10,9 @@ use actix_web::web::Data;
 | 
			
		||||
use actix_web::{web, App, HttpServer};
 | 
			
		||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
			
		||||
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::{constants, virtweb_client};
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
@@ -40,15 +41,28 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            .login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
 | 
			
		||||
            .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()
 | 
			
		||||
            .wrap(Logger::default())
 | 
			
		||||
            .wrap(AuthChecker)
 | 
			
		||||
            .wrap(identity_middleware)
 | 
			
		||||
            .wrap(session_mw)
 | 
			
		||||
            .wrap(cors)
 | 
			
		||||
            .app_data(state_manager.clone())
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig {
 | 
			
		||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
			
		||||
            }))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/server/config",
 | 
			
		||||
                web::get().to(server_controller::config),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/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"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fluentui/react-components": "^9.49.0",
 | 
			
		||||
    "react": "^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 {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  place-items: center;
 | 
			
		||||
  min-width: 320px;
 | 
			
		||||
  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 "./index.css";
 | 
			
		||||
import { App } from "./App";
 | 
			
		||||
import {
 | 
			
		||||
  FluentProvider,
 | 
			
		||||
  teamsHighContrastTheme,
 | 
			
		||||
} from "@fluentui/react-components";
 | 
			
		||||
 | 
			
		||||
ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <FluentProvider
 | 
			
		||||
      theme={teamsHighContrastTheme}
 | 
			
		||||
      style={{ display: "flex", flex: 1 }}
 | 
			
		||||
    >
 | 
			
		||||
      <App />
 | 
			
		||||
    </FluentProvider>
 | 
			
		||||
  </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