diff --git a/frontend/src/main.rs b/frontend/src/main.rs index 41f9779..23902b3 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -92,7 +92,8 @@ fn directories() -> Html { spawn_local(async move { match Request::get(url).send().await { Ok(response) if response.ok() => match response.json::<Vec<String>>().await { - Ok(json) => { + Ok(mut json) => { + json.sort(); dirs.set(json); } _ => message.set("Failed to fetch directories".into()), @@ -159,7 +160,8 @@ fn configs(props: &ConfigsProps) -> Html { spawn_local(async move { match Request::get(&url).send().await { Ok(response) if response.ok() => match response.json::<Vec<String>>().await { - Ok(json) => { + Ok(mut json) => { + json.sort(); configs.set(json); } _ => message.set("Failed to fetch configs".into()), @@ -184,19 +186,30 @@ fn configs(props: &ConfigsProps) -> Html { 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="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> + <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> - <ul class="list-disc pl-5"> - { for (*configs).iter().map(|file_name| { - html! { - <li class="mb-2"> - <Link<Route> to={Route::Config { dir_name: props.dir.clone().to_string(), file_name: file_name.clone() }}>{file_name}</Link<Route>> - </li> - } - }) } - </ul> + <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> @@ -284,6 +297,7 @@ 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 = { @@ -291,6 +305,7 @@ fn generate(props: &GenerateProps) -> Html { 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(), @@ -300,6 +315,7 @@ fn generate(props: &GenerateProps) -> Html { 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") @@ -312,10 +328,11 @@ fn generate(props: &GenerateProps) -> Html { if let Ok(text) = response.text().await { result.set(text); done.set(true); + error.set("".into()); } } _ => { - gloo::console::log!("Failed to fetch response"); + error.set("Failed to generate config".into()); } } }) @@ -326,32 +343,35 @@ fn generate(props: &GenerateProps) -> Html { <div> if !*done.clone() { <div class="w-full"> - <p><button - onclick={generate_config.reform(|_| ())} - class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"> - { "Generate" } - </button></p> - <div class="mb-2 flex justify-between items-center"> - <p class="text-sm font-medium text-gray-900 dark:text-white">{"Enter the name of the config you want to generate:"}</p> - <form> - <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="common-name">{"Common Name:"}</label> - <input type="text" id="common-name" oninput={Callback::from({ + <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()); } - })} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500" /> + })} /></span> </form> - </div> + <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 { - <div> - <Link<Route> to={Route::Config{dir_name: dir_name.clone().to_string(), file_name: format!("{}.ovpn", config_name.clone().to_string())}}><button class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4">{ "Go to " } { config_name.clone().to_string() } </button></Link<Route>> - <p>{"Generated config for "}<b>{dir_name.clone()}</b>{" with name "}<b>{config_name.clone().to_string()}</b>{":"}</p> - <pre><code>{result.clone().to_string()}</code></pre> - </div> - + <Config dir={dir_name.clone()} file_name={format!("{}.ovpn", config_name.clone().to_string())} /> } </div> }