frontend: ui improvements
This commit is contained in:
parent
a8debbb7f0
commit
79a7c71326
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -101,6 +101,12 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "binascii"
|
name = "binascii"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -1367,7 +1373,9 @@ dependencies = [
|
|||||||
name = "peazyweb-frontend"
|
name = "peazyweb-frontend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"gloo 0.11.0",
|
"gloo 0.11.0",
|
||||||
|
"lazy_static",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -4,7 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
gloo = "0.11.0"
|
gloo = "0.11.0"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
wasm-bindgen = "0.2.100"
|
wasm-bindgen = "0.2.100"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Peazyweb - OpenVPN config creator</title>
|
<title>Peazyweb - OpenVPN config creator</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-gradient-to-r from-[#f0f9ff] to-[#e6f3ff]">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init from './pkg/frontend.js';
|
import init from './pkg/frontend.js';
|
||||||
|
@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use yew::Properties;
|
use yew::Properties;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
struct GenerationRequest {
|
struct GenerationRequest {
|
||||||
@ -29,11 +31,15 @@ pub enum Route {
|
|||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref API_BASE_URL: String = std::env::var("API_BASE_URL").unwrap_or("http://127.0.0.1:8000/api/v1".into());
|
||||||
|
}
|
||||||
|
|
||||||
pub struct APIEndpoints {}
|
pub struct APIEndpoints {}
|
||||||
|
|
||||||
impl APIEndpoints {
|
impl APIEndpoints {
|
||||||
pub fn get_base() -> String {
|
pub fn get_base() -> &'static str {
|
||||||
std::env::var("BASE_URL").unwrap_or("http://127.0.0.1:8000/api/v1".into())
|
&API_BASE_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_dirs() -> String {
|
pub fn get_dirs() -> String {
|
||||||
@ -68,6 +74,7 @@ fn navbar(props: &NavProps) -> Html {
|
|||||||
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 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 has_dir = props.currrent_dir.len() > 0;
|
||||||
let is_current_home = props.currrent_route == Route::Home;
|
let is_current_home = props.currrent_route == Route::Home;
|
||||||
|
let is_current_dir = props.currrent_route == Route::Directories;
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||||
@ -92,7 +99,7 @@ fn navbar(props: &NavProps) -> Html {
|
|||||||
}
|
}
|
||||||
}><Link<Route> to={Route::Configs{dir_name: props.currrent_dir.to_string()}}>{ props.currrent_dir.clone() }</Link<Route>></li>
|
}><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>
|
<li class={if is_current_dir {classes_active} else {classes_inactive}}><Link<Route> to={Route::Directories}>{ "Directories" }</Link<Route>></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -248,17 +255,25 @@ fn configs(props: &ConfigsProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn string_to_base64_data_url(data: &str, content_type: &str) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
general_purpose::STANDARD.encode_string(data, &mut buf);
|
||||||
|
format!("data:{};base64,{}", content_type, buf)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct ConfigProps {
|
pub struct ConfigProps {
|
||||||
pub dir: AttrValue,
|
pub dir: AttrValue,
|
||||||
pub file_name: AttrValue,
|
pub file_name: AttrValue,
|
||||||
|
#[prop_or("".into())]
|
||||||
|
pub contents: AttrValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Config)]
|
#[function_component(Config)]
|
||||||
fn config(props: &ConfigProps) -> Html {
|
fn config(props: &ConfigProps) -> Html {
|
||||||
let dir_name = props.dir.clone();
|
let dir_name = props.dir.clone();
|
||||||
let file_name = props.file_name.clone();
|
let file_name = props.file_name.clone();
|
||||||
let contents = use_state(|| "".to_string());
|
let contents = use_state(|| props.contents.to_string());
|
||||||
|
|
||||||
let get_contents = {
|
let get_contents = {
|
||||||
let contents = contents.clone();
|
let contents = contents.clone();
|
||||||
@ -281,8 +296,11 @@ fn config(props: &ConfigProps) -> Html {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let get_contents_clone = get_contents.clone();
|
let get_contents_clone = get_contents.clone();
|
||||||
|
let is_contents_empty = props.contents.is_empty();
|
||||||
use_effect_with((), move |_| {
|
use_effect_with((), move |_| {
|
||||||
get_contents_clone.emit(());
|
if is_contents_empty {
|
||||||
|
get_contents_clone.emit(());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,16 +311,15 @@ fn config(props: &ConfigProps) -> Html {
|
|||||||
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">
|
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">
|
||||||
{ "Refresh" }
|
{ "Refresh" }
|
||||||
</button></p>
|
</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">
|
<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>
|
<p class="text-sm font-medium text-gray-900">{"Config "}<b>{&props.file_name}</b>{" for "}<b>{&props.dir}</b>{":"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative bg-gray-50 rounded-lg dark:bg-gray-700 p-4">
|
<div class="relative bg-gray-50 rounded-lg dark:bg-gray-700 p-4">
|
||||||
<div class="overflow-scroll max-h-full">
|
<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>
|
<pre><code id="code-block" class="text-sm text-gray-500 dark:text-gray-400 whitespace-pre">{&*contents}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-2 end-2 bg-gray-50 dark:bg-gray-700">
|
<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">
|
<a href={string_to_base64_data_url(&*contents, "application/octet-stream")} download={&props.file_name}><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">
|
<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="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"/>
|
<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"/>
|
||||||
@ -365,8 +382,10 @@ fn generate(props: &GenerateProps) -> Html {
|
|||||||
};
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<>
|
||||||
if !*done.clone() {
|
if !*done.clone() {
|
||||||
|
<div class="grid place-items-center">
|
||||||
|
<br/>
|
||||||
<div class="w-full max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:p-6 md:p-8 dark:bg-gray-800 dark:border-gray-700">
|
<div class="w-full max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:p-6 md:p-8 dark:bg-gray-800 dark:border-gray-700">
|
||||||
<form class="space-y-6" action="#">
|
<form class="space-y-6" action="#">
|
||||||
<h5 class="text-xl font-medium text-gray-900 dark:text-white">{format!("Generate config for {}:", props.dir.clone())}</h5>
|
<h5 class="text-xl font-medium text-gray-900 dark:text-white">{format!("Generate config for {}:", props.dir.clone())}</h5>
|
||||||
@ -381,12 +400,13 @@ fn generate(props: &GenerateProps) -> Html {
|
|||||||
}
|
}
|
||||||
})} />
|
})} />
|
||||||
</div>
|
</div>
|
||||||
<button onclick={generate_config.reform(|_| ())}
|
<button type="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">
|
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" }
|
{ "Generate" }
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
if !error.is_empty() {
|
if !error.is_empty() {
|
||||||
<br/>
|
<br/>
|
||||||
<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">
|
<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">
|
||||||
@ -400,9 +420,12 @@ fn generate(props: &GenerateProps) -> Html {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
<Config dir={dir_name.clone()} file_name={format!("{}.ovpn", config_name.clone().to_string())} />
|
<div class="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||||
|
<span class="font-medium">{"Success!"}</span>{" Config file successfully created!"}
|
||||||
|
</div>
|
||||||
|
<Config dir={dir_name.clone()} file_name={format!("{}.ovpn", config_name.clone().to_string())} contents={result.as_str().to_owned()} />
|
||||||
}
|
}
|
||||||
</div>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user