add manage page
This commit is contained in:
+4
-15
@@ -5,6 +5,8 @@ use leptos_router::{
|
||||
StaticSegment,
|
||||
};
|
||||
|
||||
use crate::manage::ManagePage;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
@@ -34,28 +36,15 @@ pub fn App() -> impl IntoView {
|
||||
<Stylesheet id="leptos" href="/pkg/code_submission.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
<Title text="Manage Server"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage/>
|
||||
<Route path=StaticSegment("manage") view=ManagePage/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let count = RwSignal::new(0);
|
||||
let on_click = move |_| *count.write() += 1;
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod app;
|
||||
pub mod manage;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static SERVER_LIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
const STATUS_SECRET_KEY: &str = "password";
|
||||
|
||||
#[server]
|
||||
async fn get_live_state() -> Result<bool, ServerFnError> {
|
||||
Ok(SERVER_LIVE.load(Ordering::SeqCst))
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn set_live_state(live: bool, secret_key: String) -> Result<(), ServerFnError> {
|
||||
if secret_key != STATUS_SECRET_KEY {
|
||||
return Err(ServerFnError::ServerError("Invalid secret key".to_string()));
|
||||
}
|
||||
|
||||
SERVER_LIVE.store(live, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders the server management page.
|
||||
#[component]
|
||||
pub fn ManagePage() -> impl IntoView {
|
||||
let live_state = Resource::new(|| (), |_| get_live_state());
|
||||
|
||||
view! {
|
||||
<section class="status-card">
|
||||
<p class="eyebrow">"CONTROL PANEL"</p>
|
||||
<h1>"Server status"</h1>
|
||||
<p class="intro">"Manage whether your server is accepting live traffic."</p>
|
||||
<Suspense fallback=|| view! { <p class="loading">"Loading server status..."</p> }>
|
||||
{move || Suspend::new(async move {
|
||||
match live_state.await {
|
||||
Ok(is_live) => view! { <LiveToggle initial=is_live/> }.into_any(),
|
||||
Err(error) => view! {
|
||||
<p class="error-message" role="alert">
|
||||
"Unable to load server status: " {error.to_string()}
|
||||
</p>
|
||||
}.into_any(),
|
||||
}
|
||||
})}
|
||||
</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LiveToggle(initial: bool) -> impl IntoView {
|
||||
let (live, set_live) = signal(initial);
|
||||
let (server_live, set_server_live) = signal(initial);
|
||||
let (updating, set_updating) = signal(false);
|
||||
let (secret_key, set_secret_key) = signal(String::new());
|
||||
let (error, set_error) = signal(None::<String>);
|
||||
|
||||
let on_click = move |_| {
|
||||
let previous = live.get_untracked();
|
||||
let checked = !previous;
|
||||
let key = secret_key.get_untracked();
|
||||
set_live.set(checked);
|
||||
set_updating.set(true);
|
||||
set_error.set(None);
|
||||
|
||||
leptos::task::spawn_local(async move {
|
||||
match set_live_state(checked, key).await {
|
||||
Ok(()) => set_server_live.set(checked),
|
||||
Err(error) => {
|
||||
set_live.set(previous);
|
||||
set_error.set(Some(error.to_string()));
|
||||
}
|
||||
}
|
||||
set_updating.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="status-controls">
|
||||
<label class="key-label" for="status-secret-key">"Secret key"</label>
|
||||
<input
|
||||
id="status-secret-key"
|
||||
class="key-input"
|
||||
type="password"
|
||||
placeholder="Enter your secret key"
|
||||
autocomplete="current-password"
|
||||
prop:disabled=updating
|
||||
on:input=move |event| set_secret_key.set(event_target_value(&event))
|
||||
/>
|
||||
<button
|
||||
class="status-button"
|
||||
class:go-live=move || !live.get()
|
||||
class:take-offline=move || live.get()
|
||||
type="button"
|
||||
prop:disabled=updating
|
||||
on:click=on_click
|
||||
>
|
||||
{move || if updating.get() {
|
||||
"Updating..."
|
||||
} else if live.get() {
|
||||
"Take Offline"
|
||||
} else {
|
||||
"Go Live"
|
||||
}}
|
||||
</button>
|
||||
<p
|
||||
class="status-indicator"
|
||||
class:is-live=move || server_live.get()
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="status-dot"></span>
|
||||
"Server status: "
|
||||
<strong>{move || if server_live.get() { "Live" } else { "Offline" }}</strong>
|
||||
</p>
|
||||
{move || error.get().map(|message| view! {
|
||||
<p class="error-message" role="alert">{message}</p>
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
+160
-2
@@ -1,4 +1,162 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(ellipse at 15% 10%, rgba(31, 111, 235, 0.2), transparent 38%),
|
||||
#0b1220;
|
||||
color: #e6edf7;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(100%, 480px);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: clamp(28px, 7vw, 48px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 24px;
|
||||
background: rgba(17, 27, 45, 0.92);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: #7cb3ff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 8vw, 2.6rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 12px 0 32px;
|
||||
color: #a9b6c9;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status-controls {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.key-label {
|
||||
color: #d3dceb;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.key-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 13px 15px;
|
||||
border: 1px solid #35445c;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
background: #0c1524;
|
||||
color: #f5f8fc;
|
||||
font: inherit;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.key-input::placeholder {
|
||||
color: #74839a;
|
||||
}
|
||||
|
||||
.key-input:focus {
|
||||
border-color: #65a4ff;
|
||||
box-shadow: 0 0 0 3px rgba(101, 164, 255, 0.18);
|
||||
}
|
||||
|
||||
.status-button {
|
||||
margin-top: 8px;
|
||||
padding: 14px 18px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease, transform 150ms ease, opacity 150ms ease;
|
||||
}
|
||||
|
||||
.status-button.go-live {
|
||||
background: #16804a;
|
||||
}
|
||||
|
||||
.status-button.go-live:hover:not(:disabled) {
|
||||
background: #1b9959;
|
||||
}
|
||||
|
||||
.status-button.take-offline {
|
||||
background: #b54739;
|
||||
}
|
||||
|
||||
.status-button.take-offline:hover:not(:disabled) {
|
||||
background: #d05242;
|
||||
}
|
||||
|
||||
.status-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.status-button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 14px 0 0;
|
||||
color: #a9b6c9;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.status-indicator strong {
|
||||
color: #d3dceb;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #8491a5;
|
||||
}
|
||||
|
||||
.status-indicator.is-live .status-dot {
|
||||
background: #37d488;
|
||||
box-shadow: 0 0 12px rgba(55, 212, 136, 0.65);
|
||||
}
|
||||
|
||||
.status-indicator.is-live strong {
|
||||
color: #6ee7a8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #a9b6c9;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 8px 0 0;
|
||||
color: #ff9387;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user