Files
peazyweb/frontend/src/main.rs
2025-03-24 22:51:25 +03:00

435 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use gloo::net::http::Request;
use serde::{Deserialize, Serialize};
use wasm_bindgen_futures::spawn_local;
use yew::Properties;
use yew::prelude::*;
#[derive(Serialize, Deserialize, Clone, Debug)]
struct GenerationRequest {
directory: String,
common_name: String,
}
use yew_router::prelude::*;
#[derive(Clone, Routable, PartialEq)]
pub enum Route {
#[at("/")]
Home,
#[at("/dir")]
Directories,
#[at("/dir/:dir_name")]
Configs { dir_name: String },
#[at("/dir/:dir_name/:file_name")]
Config { dir_name: String, file_name: String },
#[at("/gen/:dir_name")]
Generate { dir_name: String },
#[not_found]
#[at("/404")]
NotFound,
}
#[derive(Properties, PartialEq)]
pub struct NavProps {
#[prop_or("".into())]
pub currrent_dir: AttrValue,
#[prop_or(Route::Home)]
pub currrent_route: Route,
}
// component to show the navbar
#[function_component(Navbar)]
fn navbar(props: &NavProps) -> Html {
let classes_active = "block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500";
let classes_inactive = "block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent";
let has_dir = props.currrent_dir.len() > 0;
let is_current_home = props.currrent_route == Route::Home;
html! {
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">{"Home"}</span>
</a>
<button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
<span class="sr-only">{"Open main menu"}</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li class={if is_current_home {classes_active} else {classes_inactive}}><Link<Route> to={Route::Home}>{ "Home" }</Link<Route>></li>
if has_dir {
<li class={
match props.currrent_route.clone() {
Route::Configs{dir_name} if dir_name == props.currrent_dir.to_string() => classes_active,
_ => classes_inactive,
}
}><Link<Route> to={Route::Configs{dir_name: props.currrent_dir.to_string()}}>{ props.currrent_dir.clone() }</Link<Route>></li>
}
<li class={if !is_current_home {classes_active} else {classes_inactive}}><Link<Route> to={Route::Directories}>{ "Directories" }</Link<Route>></li>
</ul>
</div>
</div>
</nav>
}
}
// component to show the directories
#[function_component(Directories)]
fn directories() -> Html {
let dirs = use_state(Vec::new);
let message = use_state(|| "".to_string());
let get_dirs = {
let dirs = dirs.clone();
let message = message.clone();
Callback::from(move |_| {
let url = "http://127.0.0.1:8000/api/v1/get/";
let dirs = dirs.clone();
let message = message.clone();
spawn_local(async move {
match Request::get(url).send().await {
Ok(response) if response.ok() => match response.json::<Vec<String>>().await {
Ok(mut json) => {
json.sort();
dirs.set(json);
}
_ => message.set("Failed to fetch directories".into()),
},
_ => message.set("Failed to fetch directories".into()),
}
})
})
};
// Добавляем use_effect с пустым вектором зависимостей
{
let get_dirs = get_dirs.clone();
use_effect_with((), move |_| {
get_dirs.emit(());
()
});
}
html! {
<div>
<p><button
onclick={get_dirs.reform(|_| ())}
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">
{ "Refresh" }
</button></p>
<h1>{"Directories"}</h1>
<h2 class="text-2xl font-bold text-gray-700 mb-2">{ "Directory List" }</h2>
<ul class="list-disc pl-5">
{ for (*dirs).iter().map(|dir| {
html! {
<li class="mb-2">
<Link<Route> to={Route::Configs { dir_name: dir.to_string() }}>{dir}</Link<Route>>
</li>
}
}) }
</ul>
if !message.is_empty() {
<p class="text-green-500 mt-2">{ &*message }</p>
}
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct ConfigsProps {
pub dir: AttrValue,
}
#[function_component(Configs)]
fn configs(props: &ConfigsProps) -> Html {
let configs = use_state(Vec::new);
let message = use_state(|| "".to_string());
let dir_name = props.dir.clone();
let get_configs = {
let configs = configs.clone();
let message = message.clone();
Callback::from(move |_| {
let url = format!("http://127.0.0.1:8000/api/v1/get/{}", dir_name.clone());
let configs = configs.clone();
let message = message.clone();
spawn_local(async move {
match Request::get(&url).send().await {
Ok(response) if response.ok() => match response.json::<Vec<String>>().await {
Ok(mut json) => {
json.sort();
configs.set(json);
}
_ => message.set("Failed to fetch configs".into()),
},
_ => message.set("Failed to fetch configs".into()),
}
})
})
};
{
let get_configs_clone = get_configs.clone();
use_effect_with((), move |_| {
get_configs_clone.emit(());
()
});
}
html! {
<div>
<p><button
onclick={get_configs.reform(|_| ())}
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">
{ "Refresh" } </button>
<Link<Route> to={Route::Generate { dir_name: props.dir.clone().to_string() }}> <button class="text-white bg-gradient-to-br from-pink-500 to-orange-400 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">
{ "Generate" } </button> </Link<Route>> </p>
<h1>{ "Configs" }</h1>
<h2 class="text-2xl font-bold text-gray-700 mb-2">{format!("Configs for {}:", props.dir.clone())}</h2>
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
{"Name"}
</th>
</tr>
</thead>
<tbody>
{ for (*configs).iter().map(|file_name| {
html! {
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<Link<Route> to={Route::Config { dir_name: props.dir.clone().to_string(), file_name: file_name.clone() }}>{file_name}</Link<Route>>
</th>
</tr>
}
}) }
</tbody>
</table>
if !message.is_empty() {
<p class="text-green-500 mt-2">{ &*message }</p>
}
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct ConfigProps {
pub dir: AttrValue,
pub file_name: AttrValue,
}
#[function_component(Config)]
fn config(props: &ConfigProps) -> Html {
let dir_name = props.dir.clone();
let file_name = props.file_name.clone();
let contents = use_state(|| "".to_string());
let get_contents = {
let contents = contents.clone();
Callback::from(move |_| {
let url = format!(
"http://127.0.0.1:8000/api/v1/get/{}/{}",
dir_name.clone(),
file_name.clone()
);
let contents = contents.clone();
spawn_local(async move {
match Request::get(&url).send().await {
Ok(response) if response.ok() => {
if let Ok(text) = response.text().await {
contents.set(text);
}
}
_ => contents.set("Failed to fetch contents".into()),
}
})
})
};
{
let get_contents_clone = get_contents.clone();
use_effect_with((), move |_| {
get_contents_clone.emit(());
()
});
}
html! {
<div class="w-full">
<p><button
onclick={get_contents.reform(|_| ())}
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">
{ "Refresh" }
</button></p>
<p>{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p>
<div class="mb-2 flex justify-between items-center">
<p class="text-sm font-medium text-gray-900 dark:text-white">{"Config "}<b>{props.file_name.clone()}</b>{" for "}<b>{props.dir.clone()}</b>{":"}</p>
</div>
<div class="relative bg-gray-50 rounded-lg dark:bg-gray-700 p-4">
<div class="overflow-scroll max-h-full">
<pre><code id="code-block" class="text-sm text-gray-500 dark:text-gray-400 whitespace-pre">{&*contents}</code></pre>
</div>
<div class="absolute top-2 end-2 bg-gray-50 dark:bg-gray-700">
<a href={format!("http://127.0.0.1:8000/api/v1/get/{}/{}", props.dir.clone(), props.file_name.clone())} download={props.file_name.clone()}><button type="button" class="text-white bg-gradient-to-br from-purple-600 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M13 11.15V4a1 1 0 1 0-2 0v7.15L8.78 8.374a1 1 0 1 0-1.56 1.25l4 5a1 1 0 0 0 1.56 0l4-5a1 1 0 1 0-1.56-1.25L13 11.15Z" clip-rule="evenodd"/>
<path fill-rule="evenodd" d="M9.657 15.874 7.358 13H5a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2.358l-2.3 2.874a3 3 0 0 1-4.685 0ZM17 16a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z" clip-rule="evenodd"/>
</svg>{"Download"}</button></a>
</div>
</div>
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct GenerateProps {
pub dir: AttrValue,
}
#[function_component(Generate)]
fn generate(props: &GenerateProps) -> Html {
let dir_name = props.dir.clone();
let config_name = use_state(|| "".to_string());
let result = use_state(|| "".to_string());
let error = use_state(|| "".to_string());
let done = use_state(|| false);
let generate_config = {
let dir_name = dir_name.clone();
let config_name = config_name.clone();
let done = done.clone();
let result = result.clone();
let error = error.clone();
Callback::from(move |_| {
let request_body = GenerationRequest {
directory: dir_name.clone().to_string(),
common_name: config_name.clone().to_string(),
};
let json_payload = serde_json::to_string(&request_body).unwrap();
let url = "http://127.0.0.1:8000/api/v1/generate/";
let result = result.clone();
let done = done.clone();
let error = error.clone();
spawn_local(async move {
match Request::post(url)
.header("Content-Type", "application/json")
.body(json_payload)
.unwrap()
.send()
.await
{
Ok(response) if response.ok() => {
if let Ok(text) = response.text().await {
result.set(text);
done.set(true);
error.set("".into());
}
}
_ => {
error.set("Failed to generate config".into());
}
}
})
})
};
html! {
<div>
if !*done.clone() {
<div class="w-full">
<h2 class="text-2xl font-bold text-gray-700 mb-2">{format!("Generate config for {}:", props.dir.clone())}</h2>
<form class="mb-2">
<span><label class="block mb-2 text-sm font-medium px-4 py-2" for="common-name">{"Common Name:"}
</label> <input type="text" id="common-name" placeholder="enter common name" class="border rounded px-4 py-2 mr-2" oninput={Callback::from({
let config_name = config_name.clone();
move |e: InputEvent| {
let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
config_name.set(input.value());
}
})} /></span>
</form>
<button onclick={generate_config.reform(|_| ())}
class="text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 shadow-lg shadow-red-500/50 dark:shadow-lg dark:shadow-red-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2">
{ "Generate" }
</button>
if !error.is_empty() {
<div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<svg class="shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<span class="sr-only">{"Info"}</span>
<div>
<span class="font-medium">{"Error!"}</span> {" "} { &*error }
</div>
</div>
}
</div>
} else {
<Config dir={dir_name.clone()} file_name={format!("{}.ovpn", config_name.clone().to_string())} />
}
</div>
}
}
fn switch(routes: Route) -> Html {
match routes.clone() {
Route::Home => html! {
<div>
<Navbar currrent_route={routes.clone()} />
<h1>{ "Home" }</h1> <Link<Route> to={Route::Directories}>{ "Directories" }</Link<Route>>
</div> },
Route::Directories => html! {
<div>
<Navbar currrent_route={routes.clone()} />
<Directories />
</div>
},
Route::Configs { dir_name } => html! {
<div>
<Navbar currrent_route={routes.clone()} currrent_dir={dir_name.clone()} />
<Configs dir={dir_name.clone()} />
</div>
},
Route::Generate { dir_name } => html! {
<div>
<Navbar currrent_route={routes.clone()} currrent_dir={dir_name.clone()} />
<Generate dir={dir_name.clone()} />
</div>
},
Route::Config {
dir_name,
file_name,
} => html! {
<div>
<Navbar currrent_route={routes.clone()} currrent_dir={dir_name.clone()} />
<Config dir={dir_name.clone()} file_name={file_name.clone()} />
</div>
},
_ => html! {
<div>
<Navbar />
<h1>{ "todo" }</h1>
</div>
},
}
}
#[function_component]
fn App() -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={switch} />
</BrowserRouter>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}