diff --git a/service/src/airports/model.rs b/service/src/airports/model.rs index 21a2ac6..d8f0527 100644 --- a/service/src/airports/model.rs +++ b/service/src/airports/model.rs @@ -27,8 +27,7 @@ pub struct InsertAirport { #[derive(Debug)] pub struct QueryFilters { - pub name: Option, - pub icao: Option, + pub search: Option, pub bounds: Option>, pub category: Option, pub order_field: Option, @@ -38,8 +37,7 @@ pub struct QueryFilters { impl Default for QueryFilters { fn default() -> Self { QueryFilters { - name: None, - icao: None, + search: None, bounds: None, category: None, order_field: None, @@ -131,16 +129,17 @@ impl QueryAirport { if let Some(category) = &filters.category { query = query.filter(airports::category.eq(category)); } - if let Some(icao) = &filters.icao { - if let Some(name) = &filters.name { - query = query.filter( - airports::icao.ilike(format!("%{}%", icao)).or( - airports::full_name.ilike(format!("%{}%", name)) - ) - ) - } else { - query = query.filter(airports::icao.ilike(format!("%{}%", icao))) - } + if let Some(search) = &filters.search { + query = query.filter( + airports::icao.ilike(format!("%{}%", search)) + .or(airports::full_name.ilike(format!("%{}%", search))) + .or(airports::iso_country.ilike(format!("%{}%", search))) + .or(airports::iso_region.ilike(format!("%{}%", search))) + .or(airports::municipality.ilike(format!("%{}%", search))) + .or(airports::gps_code.ilike(format!("%{}%", search))) + .or(airports::iata_code.ilike(format!("%{}%", search))) + .or(airports::local_code.ilike(format!("%{}%", search))) + ) } if let Some(order_by) = &filters.order_by { @@ -193,16 +192,17 @@ impl QueryAirport { if let Some(category) = &filters.category { query = query.filter(airports::category.eq(category)); } - if let Some(icao) = &filters.icao { - if let Some(name) = &filters.name { - query = query.filter( - airports::icao.ilike(format!("%{}%", icao)).or( - airports::full_name.ilike(format!("%{}%", name)) - ) - ) - } else { - query = query.filter(airports::icao.ilike(format!("%{}%", icao))) - } + if let Some(search) = &filters.search { + query = query.filter( + airports::icao.ilike(format!("%{}%", search)) + .or(airports::full_name.ilike(format!("%{}%", search))) + .or(airports::iso_country.ilike(format!("%{}%", search))) + .or(airports::iso_region.ilike(format!("%{}%", search))) + .or(airports::municipality.ilike(format!("%{}%", search))) + .or(airports::gps_code.ilike(format!("%{}%", search))) + .or(airports::iata_code.ilike(format!("%{}%", search))) + .or(airports::local_code.ilike(format!("%{}%", search))) + ) } let count: i64 = query.get_result(&mut conn)?; @@ -224,19 +224,19 @@ impl QueryAirport { Ok(airport) } - pub fn update(id: i32, airport: InsertAirport) -> Result { + pub fn update(icao: String, airport: InsertAirport) -> Result { let mut conn = db::connection()?; let airport = diesel::update(airports::table) - .filter(airports::id.eq(id)) + .filter(airports::icao.eq(icao)) .set(airport) .get_result(&mut conn)?; Ok(airport) } - pub fn delete(id: Option) -> Result { + pub fn delete(icao: Option) -> Result { let mut conn = db::connection()?; - let res = match id { - Some(id) => diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?, + let res = match icao { + Some(icao) => diesel::delete(airports::table.filter(airports::icao.eq(icao))).execute(&mut conn)?, None => diesel::delete(airports::table).execute(&mut conn)? }; Ok(res) diff --git a/service/src/airports/routes.rs b/service/src/airports/routes.rs index ddc17c9..577cf8e 100644 --- a/service/src/airports/routes.rs +++ b/service/src/airports/routes.rs @@ -8,8 +8,7 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] struct GetAllParameters { - name: Option, - icao: Option, + search: Option, bounds: Option, category: Option, order_field: Option, @@ -18,7 +17,7 @@ struct GetAllParameters { page: Option } -#[get("/import")] +#[post("/import")] async fn import(auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, @@ -31,12 +30,11 @@ async fn import(auth: JwtAuth) -> HttpResponse { }) } -#[get("/search")] +#[get("")] async fn get_all(req: HttpRequest) -> HttpResponse { let params = web::Query::::from_query(req.query_string()).unwrap(); let mut filters = QueryFilters::default(); - filters.name = params.name.clone(); - filters.icao = params.icao.clone(); + filters.search = params.search.clone(); filters.category = params.category.clone(); filters.bounds = match ¶ms.bounds { Some(b) => { @@ -119,7 +117,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { } } -#[get("/search/{icao}")] +#[get("/{icao}")] async fn get(icao: web::Path) -> HttpResponse { match QueryAirport::find(icao.into_inner()) { Ok(a) => HttpResponse::Ok().json(Response { @@ -133,7 +131,7 @@ async fn get(icao: web::Path) -> HttpResponse { } } -#[post("/create")] +#[post("")] async fn create(airport: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, @@ -148,8 +146,8 @@ async fn create(airport: web::Json, auth: JwtAuth) -> HttpRespons } } -#[put("/update/{icao}")] -async fn update(icao: web::Path, airport: web::Json, auth: JwtAuth) -> HttpResponse { +#[put("/{icao}")] +async fn update(icao: web::Path, airport: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -163,8 +161,8 @@ async fn update(icao: web::Path, airport: web::Json, auth: J } } -#[delete("/remove")] -async fn remove_all(auth: JwtAuth) -> HttpResponse { +#[delete("")] +async fn delete_all(auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -178,8 +176,8 @@ async fn remove_all(auth: JwtAuth) -> HttpResponse { } } -#[delete("/remove/{icao}")] -async fn remove(icao: web::Path, auth: JwtAuth) -> HttpResponse { +#[delete("/{icao}")] +async fn delete(icao: web::Path, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) @@ -199,8 +197,8 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(get) .service(create) .service(update) - .service(remove) - .service(remove_all) + .service(delete) + .service(delete_all) .service(import) ); } \ No newline at end of file diff --git a/service/src/metars/model.rs b/service/src/metars/model.rs index 9969e1c..c03b626 100644 --- a/service/src/metars/model.rs +++ b/service/src/metars/model.rs @@ -275,18 +275,19 @@ impl Metar { return insert_metars; } - pub async fn get_all(icaos: String) -> Result, ServiceError> { - if icaos.is_empty() { + pub async fn get_all(icao_string: String) -> Result, ServiceError> { + if icao_string.is_empty() { return Ok(vec![]); } - let station_icaos: Vec<&str> = icaos.split(',').collect(); - let mut db_metars = match QueryMetar::get_all(&station_icaos) { + let icaos: Vec<&str> = icao_string.split(",").collect(); + + let mut db_metars = match QueryMetar::get_all(&icaos) { Ok(m) => Self::from_query(m), Err(err) => return Err(err) }; - let missing_icaos = Self::get_missing_metar_icaos(&db_metars, &station_icaos); + let missing_icaos = Self::get_missing_metar_icaos(&db_metars, &icaos); if missing_icaos.is_empty() { return Ok(db_metars); } diff --git a/service/src/metars/routes.rs b/service/src/metars/routes.rs index d705a7a..2ab58d6 100644 --- a/service/src/metars/routes.rs +++ b/service/src/metars/routes.rs @@ -1,6 +1,6 @@ use crate::{error_handler::ServiceError, db::Metadata}; use crate::metars::Metar; -use actix_web::{get, web, HttpResponse}; +use actix_web::{get, web, HttpResponse, HttpRequest}; use log::error; use serde::{Deserialize, Serialize}; @@ -10,23 +10,35 @@ pub struct MetarsResponse { pub meta: Metadata } -#[get("metars/{ids}")] -async fn get_all(ids: web::Path) -> HttpResponse { - let airports = match web::block(|| Ok::<_, ServiceError>(async {Metar::get_all(ids.into_inner()).await})) - .await - .unwrap() - .unwrap() - .await { - Ok(a) => a, - Err(err) => { - error!("{}", err); - return err.to_http_response(); - } - }; - HttpResponse::Ok().json(MetarsResponse { - data: airports, - meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } - }) +#[derive(Debug, Serialize, Deserialize)] +struct GetAllParameters { + icaos: Option +} + +#[get("metars")] +async fn get_all(req: HttpRequest) -> HttpResponse { + let params = web::Query::::from_query(req.query_string()).unwrap(); + let icao_option = params.icaos.clone(); + let icao_string = match icao_option { + Some(i) => i, + None => return HttpResponse::UnprocessableEntity().body("Missing icaos parameter") + }; + + let airports = match web::block(|| Ok::<_, ServiceError>(async {Metar::get_all(icao_string).await})) + .await + .unwrap() + .unwrap() + .await { + Ok(a) => a, + Err(err) => { + error!("{}", err); + return err.to_http_response(); + } + }; + HttpResponse::Ok().json(MetarsResponse { + data: airports, + meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 } + }) } pub fn init_routes(config: &mut web::ServiceConfig) { diff --git a/ui/src/api/airport.ts b/ui/src/api/airport.ts index 5a84ca0..992cf5d 100644 --- a/ui/src/api/airport.ts +++ b/ui/src/api/airport.ts @@ -1,22 +1,21 @@ -import { AirportOrderField, Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types'; -import { getRequest, deleteRequest } from '.'; +import { Airport, AirportOrderField, Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types'; +import { getRequest, deleteRequest, postRequest, putRequest } from '.'; interface GetAirportProps { icao: string; } export async function getAirport({ icao }: GetAirportProps): Promise { - const response = await getRequest(`airports/search/${icao}`); + const response = await getRequest(`airports/${icao}`); return response?.json() || { data: undefined }; } interface GetAirportsProps { bounds?: Bounds; category?: string; - name?: string; + search?: string; order_field?: AirportOrderField; order_by?: 'asc' | 'desc'; - icao?: string; page?: number; limit?: number; } @@ -24,20 +23,18 @@ interface GetAirportsProps { export async function getAirports({ bounds, category, - name, - icao, + search, order_field, order_by, limit = 10, page = 1 }: GetAirportsProps): Promise { - const response = await getRequest('airports/search', { + const response = await getRequest('airports', { bounds: bounds ? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}` : undefined, category: category ?? undefined, - name: name ?? undefined, - icao: icao ?? undefined, + search: search ?? undefined, order_field: order_field ?? undefined, order_by: order_by ?? undefined, limit, @@ -49,14 +46,24 @@ export async function getAirports({ export async function removeAirport({ icao }: { icao?: string }): Promise { let response if (icao) { - response = await deleteRequest(`airports/remove/${icao}`); + response = await deleteRequest(`airports/${icao}`); } else { - response = await deleteRequest('airports/remove'); + response = await deleteRequest('airports'); } return response.status == 204; } -export async function importAirports(): Promise { - const response = await getRequest('airports/import'); +export async function createAirport({ airport }: { airport: Airport }): Promise { + const response = await postRequest(`airports`, airport); + return response?.json() || { data: undefined }; +} + +export async function updateAirport({ airport }: { airport: Airport }): Promise { + const response = await putRequest(`airports`, airport); + return response?.json() || { data: undefined }; +} + +export async function importAirports(): Promise { + const response = await postRequest('airports/import'); return response?.json() || { data: undefined }; } diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 554f37a..861b5aa 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -41,6 +41,28 @@ export async function postRequest(endpoint: string, body?: any, options?: PostOp return response; } +export async function putRequest(endpoint: string, body?: any, options?: PostOptions): Promise { + const url = `${baseURL}/${endpoint}`; + let response; + if (body && (!options?.type || options.type === 'json')) { + response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(body) + }); + } else { + response = await fetch(url, { + method: 'PUT', + credentials: 'include', + body + }); + } + return response; +} + export async function deleteRequest(endpoint: string): Promise { const url = `${baseURL}/${endpoint}`; const response = await fetch(url, { diff --git a/ui/src/api/metar.ts b/ui/src/api/metar.ts index 110cba6..a98b239 100644 --- a/ui/src/api/metar.ts +++ b/ui/src/api/metar.ts @@ -10,6 +10,6 @@ export async function getMetars(icaos: string[]): Promise { return { data: [] }; } const stationICAOs: string = icaos.map((icao) => icao).join(','); - const response = await getRequest(`metars/${stationICAOs}`, {}); + const response = await getRequest(`metars`, { icaos: stationICAOs }); return response?.json() || { data: [] }; } diff --git a/ui/src/app/admin/page.tsx b/ui/src/app/admin/page.tsx index 65f4720..0a72a40 100644 --- a/ui/src/app/admin/page.tsx +++ b/ui/src/app/admin/page.tsx @@ -3,26 +3,31 @@ import { Airport } from "@/api/airport.types"; import AirportTablePanel from "@/components/Admin/AirportTablePanel"; import CreateAirportPanel from "@/components/Admin/CreateAirportPanel"; -import { Container, Grid } from "@mantine/core"; +import UpdateAirportModal from "@/components/Admin/UpdateAirportModal"; +import { Container, Grid, SimpleGrid } from "@mantine/core"; import { useEffect, useState } from "react"; export default function Page() { const [airport, setAirport] = useState(undefined); useEffect(() => { - console.log(airport); }, [airport]); - return - - - - - - - - - ; + return ( + + + + + + + + + + + + + + ); } diff --git a/ui/src/components/Admin/AirportTablePanel.tsx b/ui/src/components/Admin/AirportTablePanel.tsx index 1d48f54..cdf62ff 100644 --- a/ui/src/components/Admin/AirportTablePanel.tsx +++ b/ui/src/components/Admin/AirportTablePanel.tsx @@ -1,6 +1,6 @@ import { getAirports, importAirports, removeAirport } from "@/api/airport"; -import { Airport, AirportCategory, AirportOrderField, airportCategoryToText } from "@/api/airport.types"; -import { Text, Button, Card, Group, Pagination, ScrollArea, Table, TextInput, rem, UnstyledButton, Center } from "@mantine/core"; +import { Airport, airportCategoryToText } from "@/api/airport.types"; +import { Text, Button, Card, Group, Pagination, Table, TextInput, rem, UnstyledButton, Center, Flex, Container, Grid, Space } from "@mantine/core"; import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi"; import { useEffect, useState } from "react"; import { CiSearch } from "react-icons/ci"; @@ -14,6 +14,7 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport async function getAirportData() { const response = await getAirports({ + search, page, limit: 100 }); @@ -45,14 +46,12 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport {airport.gps_code} {airport.iata_code} {airport.local_code} - {airport.point.x} - {airport.point.y} )) return } value={search} @@ -72,28 +71,36 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport GPS Code IATA Code Local Code - Latitude - Longitude {rows} - - - { - await importAirports(); - await getAirportData(); - }}> - Import - - { - await removeAirport({}); - await getAirportData(); - }}> - Remove All - - + + + + + + + + { + await importAirports(); + await getAirportData(); + }}> + Import + + + + { + await removeAirport({}); + await getAirportData(); + }}> + Remove All + + + + + } @@ -103,7 +110,6 @@ function PanelButton({ children, color = 'blue', onClick }: {children: any, colo loading={loading} variant='light' color={color} - mt={'md'} radius={'md'} onClick={() => { setLoading(true); diff --git a/ui/src/components/Admin/CreateAirportPanel.tsx b/ui/src/components/Admin/CreateAirportPanel.tsx index b64810c..46a12ef 100644 --- a/ui/src/components/Admin/CreateAirportPanel.tsx +++ b/ui/src/components/Admin/CreateAirportPanel.tsx @@ -1,9 +1,10 @@ +import { createAirport } from "@/api/airport"; import { Airport, AirportCategory } from "@/api/airport.types"; import { Card, TextInput, Select, Group, Flex, Space, Button } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useEffect } from "react"; -export default function CreateAirportPanel({ airport, setAirport } : { airport?: Airport, setAirport: (airport: Airport | undefined) => void }) { +export default function CreateAirportPanel() { const form = useForm({ initialValues: { icao: '', @@ -25,20 +26,12 @@ export default function CreateAirportPanel({ airport, setAirport } : { airport?: } }); - useEffect(() => { - console.log(airport); - if (airport) { - form.setValues(airport); - } - }, [airport]); - return Create Airport -
{ - if (airport) { - console.log('update'); - } else { - console.log('create'); + { + const response = await createAirport({ airport: values }); + if (response.success) { + form.reset(); } })}> - {airport ? 'Update' : 'Create'} + Create @@ -145,10 +138,7 @@ export default function CreateAirportPanel({ airport, setAirport } : { airport?: variant='light' color='red' radius={'md'} - onClick={() => { - form.reset(); - setAirport(undefined); - }} + onClick={() => form.reset()} > Reset diff --git a/ui/src/components/Admin/UpdateAirportModal.tsx b/ui/src/components/Admin/UpdateAirportModal.tsx new file mode 100644 index 0000000..bf51198 --- /dev/null +++ b/ui/src/components/Admin/UpdateAirportModal.tsx @@ -0,0 +1,148 @@ +import { removeAirport, updateAirport } from "@/api/airport"; +import { Airport, AirportCategory } from "@/api/airport.types"; +import { Button, Container, Flex, Group, Modal, Paper, Select, TextInput, Title } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { useEffect } from "react"; + +export default function UpdateAirportModal({ airport, setAirport }: { airport: Airport | undefined, setAirport: (airport: Airport | undefined) => void}) { + const form = useForm({ + initialValues: { + icao: airport?.icao || '', + category: airport?.category || AirportCategory.SMALL, + full_name: airport?.full_name || '', + elevation_ft: airport?.elevation_ft || 0, + continent: airport?.continent || '', + iso_country: airport?.iso_country || '', + iso_region: airport?.iso_region || '', + municipality: airport?.municipality || '', + gps_code: airport?.gps_code || '', + iata_code: airport?.iata_code || '', + local_code: airport?.local_code || '', + point: { + x: airport?.point.x || 0, + y: airport?.point.y || 0, + srid: airport?.point.srid || 4326 + } + } + }); + + useEffect(() => { + if (airport) { + form.setValues(airport); + } + }, [airport]); + + return ( + setAirport(undefined)} withCloseButton={false} size={'50%'}> + + Update Airport + + { + const response = await updateAirport({ airport: values }); + if (response.success) { + setAirport(undefined); + } + })}> + +