From a273d4134bf93463ac30398c574ee0771a789a07 Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Mon, 12 May 2025 20:35:20 -0400 Subject: [PATCH] Update Airport admin page --- .env | 6 +- api/src/main.rs | 2 +- bruno/Users/Login.bru | 2 +- ui/package-lock.json | 97 ++++++++++++------- ui/package.json | 11 ++- ui/src/components/Administration.tsx | 5 +- .../AirportDrop/AirportDrop.module.css | 28 ++++++ ui/src/components/AirportDrop/index.tsx | 69 +++++++++++++ .../AirportTable/AirportTable.module.css | 19 ++++ ui/src/components/AirportTable/index.tsx | 57 +++++++++++ ui/src/components/Profile.tsx | 2 +- ui/src/lib/account.types.ts | 2 +- ui/src/lib/airport.ts | 11 ++- 13 files changed, 264 insertions(+), 47 deletions(-) create mode 100644 ui/src/components/AirportDrop/AirportDrop.module.css create mode 100644 ui/src/components/AirportDrop/index.tsx create mode 100644 ui/src/components/AirportTable/AirportTable.module.css create mode 100644 ui/src/components/AirportTable/index.tsx diff --git a/.env b/.env index 67008e3..7e60806 100644 --- a/.env +++ b/.env @@ -10,7 +10,7 @@ NGINX_INTERNAL_HOST=host.docker.internal POSTGRES_HOST=localhost POSTGRES_USER=aviation -POSTGRES_PASSWORD=CHANGEME +POSTGRES_PASSWORD=changeme POSTGRES_NAME=aviation POSTGRES_PORT=5432 @@ -19,7 +19,7 @@ REDIS_PORT=6379 MINIO_HOST=localhost MINIO_ROOT_USER=aviation -MINIO_ROOT_PASSWORD=CHANGEME +MINIO_ROOT_PASSWORD=changeme MINIO_BUCKET=aviation MINIO_PROTOCOL=http MINIO_PORT=9000 @@ -41,6 +41,6 @@ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS:${NGINX_HOST} ENVIRONMENT=development ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=CHANGEME +ADMIN_PASSWORD=changeme AVIATION_WEATHER_URL=https://aviationweather.gov/api/data diff --git a/api/src/main.rs b/api/src/main.rs index ba5ce96..f5b164b 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> Result<(), Box> { log::debug!("Creating default administrator"); let password = admin_password.unwrap(); let password_hash = hash(&password)?; - if email == "admin@example.com" || password == "CHANGEME" { + if email == "admin@example.com" || password == "changeme" { log::warn!( "Default admin credentials are in use, update the ADMIN_EMAIL and ADMIN_PASSWORD." ); diff --git a/bruno/Users/Login.bru b/bruno/Users/Login.bru index c245953..a7332e4 100644 --- a/bruno/Users/Login.bru +++ b/bruno/Users/Login.bru @@ -13,6 +13,6 @@ post { body:json { { "email": "admin@example.com", - "password": "CHANGEME" + "password": "changeme" } } diff --git a/ui/package-lock.json b/ui/package-lock.json index 454f554..932c993 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,11 +8,12 @@ "name": "aviation-ui", "version": "0.1.0", "dependencies": { - "@mantine/core": "^7.17.2", - "@mantine/form": "^7.17.2", - "@mantine/hooks": "^7.17.2", - "@mantine/modals": "^7.17.2", - "@mantine/notifications": "^7.17.2", + "@mantine/core": "^8.0.0", + "@mantine/dropzone": "^8.0.0", + "@mantine/form": "^8.0.0", + "@mantine/hooks": "^8.0.0", + "@mantine/modals": "^8.0.0", + "@mantine/notifications": "^8.0.0", "@tabler/icons-react": "^3.31.0", "d3": "^7.9.0", "js-cookie": "^3.0.5", @@ -1106,28 +1107,43 @@ } }, "node_modules/@mantine/core": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.2.tgz", - "integrity": "sha512-R6MYhitJ0JEgrhadd31Nw9FhRaQwDHjXUs5YIlitKH/fTOz9gKSxKjzmNng3bEBQCcbEDOkZj3FRcBgTUh/F0Q==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.0.0.tgz", + "integrity": "sha512-TskeJS2/+DbmUe85fXDoUAyErkSvR4YlbUl8MLqhjFBJUqwc72ZrLynmN13wuKtlVPakDYYjq4/IEDMReh3CYA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", - "react-textarea-autosize": "8.5.6", + "react-textarea-autosize": "8.5.9", "type-fest": "^4.27.0" }, "peerDependencies": { - "@mantine/hooks": "7.17.2", + "@mantine/hooks": "8.0.0", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/dropzone": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.0.0.tgz", + "integrity": "sha512-eSQbYg0M6MuvPvCJuiM3HKJufcNRqjjwaa157tXRGV7iUPfzXxdF1EMP1osljXRjMEGH/A+CiDN3eCsNTzt53A==", + "license": "MIT", + "dependencies": { + "react-dropzone-esm": "15.2.0" + }, + "peerDependencies": { + "@mantine/core": "8.0.0", + "@mantine/hooks": "8.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/form": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.17.2.tgz", - "integrity": "sha512-MxZPKXXhaZ7M1ZJOpS2wifhh186DMvNjcXa2bP04Tp9TdvTlbLAJZxKjZkQnGGgt8Atsf6/3gdeJMfG704Km6g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.0.0.tgz", + "integrity": "sha512-ErbbEFMEiRsK2Rn0jmFE5ohNJXHSMSbuJsL2vDUVsbIaXo6svw6ockw1WWGdiU8oEGqxM6Pd618yI9cJWNHF3g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1138,46 +1154,46 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.2.tgz", - "integrity": "sha512-tbErVcGZu0E4dSmE6N0k6Tv1y9R3SQmmQgwqorcc+guEgKMdamc36lucZGlJnSGUmGj+WLUgELkEQ0asdfYBDA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.0.tgz", + "integrity": "sha512-hrcgZMHUPsgu+VBfUVcJOqMG7Qi+AshYjFyc/qo0Cz8TEhqWmD0I1yJW+qj4sDTTDWRQC6kvI5c1h+87/9MvoA==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@mantine/modals": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.17.2.tgz", - "integrity": "sha512-Ms8MYLJCZcxRnGfIQr4riGK2g5mpklxiEAU84vbptoAlQ2d5Iqu+CQ0XpDfamCQl/ltmPmYJYkrq52zhQWIS3w==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-8.0.0.tgz", + "integrity": "sha512-yki3KzW9Pykf6hVSezWjeHC0FCiYD3mK2r2Sn6qE0ag+EeXZs1cbrqpjZHYov2rh6j0xzW2jnaoVbKEqYw1vUQ==", "license": "MIT", "peerDependencies": { - "@mantine/core": "7.17.2", - "@mantine/hooks": "7.17.2", + "@mantine/core": "8.0.0", + "@mantine/hooks": "8.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/notifications": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.2.tgz", - "integrity": "sha512-vg0L8cmihz0ODg4WJ9MAyK06WPt/6g67ksIUFxd4F8RfdJbIMLTsNG9yWoSfuhtXenUg717KaA917IWLjDSaqw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.0.0.tgz", + "integrity": "sha512-sWldvQmq4YJsknHURBNKkc3CAU0qDb0LuQGKIZGxqFlwEiXNIAI8mtfr7stgzVx+mteVW1g+HBb7FaZp07jRxQ==", "license": "MIT", "dependencies": { - "@mantine/store": "7.17.2", + "@mantine/store": "8.0.0", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.17.2", - "@mantine/hooks": "7.17.2", + "@mantine/core": "8.0.0", + "@mantine/hooks": "8.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/store": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.2.tgz", - "integrity": "sha512-UoMUYQK/z58hMueCkpDIXc49gPgrVO/zcpb0k+B7MFU51EIUiFzHLxLFBmWrgCAM6rzJORqN8JjyCd/PB9j4aw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.0.0.tgz", + "integrity": "sha512-42RWCsXMNuhpX+d/hwr5aHj+HWyi5ltbc0R0xdiUnAmqSB7CHbWxDDLh4+DbmqPrN9pTeYvpPGp3v/CG2vuGBg==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" @@ -4286,6 +4302,21 @@ "react": "^19.0.0" } }, + "node_modules/react-dropzone-esm": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.2.0.tgz", + "integrity": "sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -4420,9 +4451,9 @@ } }, "node_modules/react-textarea-autosize": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.6.tgz", - "integrity": "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.13", diff --git a/ui/package.json b/ui/package.json index ddd41e5..ddac2da 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,11 +11,12 @@ "format": "prettier --write src" }, "dependencies": { - "@mantine/core": "^7.17.2", - "@mantine/form": "^7.17.2", - "@mantine/hooks": "^7.17.2", - "@mantine/modals": "^7.17.2", - "@mantine/notifications": "^7.17.2", + "@mantine/core": "^8.0.0", + "@mantine/dropzone": "^8.0.0", + "@mantine/form": "^8.0.0", + "@mantine/hooks": "^8.0.0", + "@mantine/modals": "^8.0.0", + "@mantine/notifications": "^8.0.0", "@tabler/icons-react": "^3.31.0", "d3": "^7.9.0", "js-cookie": "^3.0.5", diff --git a/ui/src/components/Administration.tsx b/ui/src/components/Administration.tsx index 93441ba..8c0b1d8 100644 --- a/ui/src/components/Administration.tsx +++ b/ui/src/components/Administration.tsx @@ -1,6 +1,8 @@ import { Header } from '@components/Header'; import { Navigate } from 'react-router'; import { useUserContext } from '@components/context/UserContext.tsx'; +import { AirportTable } from '@components/AirportTable'; +import { AirportDrop } from '@components/AirportDrop'; export function Administration() { const { user } = useUserContext(); @@ -12,7 +14,8 @@ export function Administration() { return ( <>
- Todo: administration {user?.email} + + ); } diff --git a/ui/src/components/AirportDrop/AirportDrop.module.css b/ui/src/components/AirportDrop/AirportDrop.module.css new file mode 100644 index 0000000..ffbc5b5 --- /dev/null +++ b/ui/src/components/AirportDrop/AirportDrop.module.css @@ -0,0 +1,28 @@ +.wrapper { + position: relative; + margin-bottom: 30px; +} + +.dropzone { + border-width: 1px; + padding-bottom: 50px; + color: var(--mantine-color-bright); +} + +.icon { + color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-white)); +} + +.control { + position: absolute; + width: 250px; + left: calc(50% - 125px); + bottom: -20px; +} + +.description { + text-align: center; + font-size: var(--mantine-font-size-sm); + color: var(--mantine-color-dimmed); + margin-top: var(--mantine-spacing-xs); +} \ No newline at end of file diff --git a/ui/src/components/AirportDrop/index.tsx b/ui/src/components/AirportDrop/index.tsx new file mode 100644 index 0000000..7a25264 --- /dev/null +++ b/ui/src/components/AirportDrop/index.tsx @@ -0,0 +1,69 @@ +import { useRef, useState } from 'react'; +import { IconCloudUpload, IconDownload, IconX } from '@tabler/icons-react'; +import { Button, Group, Text, useMantineTheme } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import classes from './AirportDrop.module.css'; +import { importAirports } from '@lib/airport.ts'; + +export function AirportDrop() { + const theme = useMantineTheme(); + const openRef = useRef<() => void>(null); + const [loading, setLoading] = useState(false); + + return ( +
+ { + if (files.length === 0) return; + setLoading(true); + try { + const formData = new FormData(); + files.forEach(file => { + formData.append('files', file, file.name); + }) + await importAirports(formData); + } catch (error) { + console.error('Upload error:', error); + } finally { + setLoading(false); + } + }} + className={classes.dropzone} + radius="md" + accept={['application/JSON']} + maxSize={30 * 1024 ** 2} + > +
+ + + + + + + + + + + + + + Drop files here + Json file less than 30mb + Upload JSON + + + + Drag'n'drop files here to upload. We can accept only .json files that + are less than 30mb in size. + +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/ui/src/components/AirportTable/AirportTable.module.css b/ui/src/components/AirportTable/AirportTable.module.css new file mode 100644 index 0000000..ffff651 --- /dev/null +++ b/ui/src/components/AirportTable/AirportTable.module.css @@ -0,0 +1,19 @@ +.header { + position: sticky; + top: 0; + background-color: var(--mantine-color-body); + transition: box-shadow 150ms ease; + + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-3)); + } +} + +.scrolled { + box-shadow: var(--mantine-shadow-sm); +} \ No newline at end of file diff --git a/ui/src/components/AirportTable/index.tsx b/ui/src/components/AirportTable/index.tsx new file mode 100644 index 0000000..72f638e --- /dev/null +++ b/ui/src/components/AirportTable/index.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import cx from 'clsx'; +import { Center, Pagination, ScrollArea, Table } from '@mantine/core'; +import classes from './AirportTable.module.css'; +import { getAirports } from '@lib/airport.ts'; +import { Airport } from '@lib/airport.types.ts'; + +export function AirportTable() { + const [data, setData] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const limit = 1000; + getAirports({ page, limit }).then(r => { + setData(r.data); + setTotalPages(r.total / r.data.length); + }); + },[page]); + + const rows = data.map((row, idx) => ( + + {row.name} + {row.icao} + {row.latitude} + {row.longitude} + + )); + + return ( + <> + setScrolled(y !== 0)}> + + + + Name + ICAO + Latitude + Longitude + + + {rows} +
+
+
+ +
+ + ); +} diff --git a/ui/src/components/Profile.tsx b/ui/src/components/Profile.tsx index 160a3ae..31701c8 100644 --- a/ui/src/components/Profile.tsx +++ b/ui/src/components/Profile.tsx @@ -12,7 +12,7 @@ export function Profile() { return ( <>
- Todo: profile {user?.email} + Todo: profile {user?.first_name} ); } diff --git a/ui/src/lib/account.types.ts b/ui/src/lib/account.types.ts index 24e0e61..9d42edc 100644 --- a/ui/src/lib/account.types.ts +++ b/ui/src/lib/account.types.ts @@ -6,7 +6,7 @@ export interface RegisterUser { } export interface User { - email: string; + email_verified: boolean; role: string; first_name: string; last_name: string; diff --git a/ui/src/lib/airport.ts b/ui/src/lib/airport.ts index d53727d..7762e86 100644 --- a/ui/src/lib/airport.ts +++ b/ui/src/lib/airport.ts @@ -1,5 +1,5 @@ import { Airport, AirportCategory, Bounds, GetAirportsResponse } from '@lib/airport.types.ts'; -import { getRequest } from '@lib/index.ts'; +import { getRequest, postRequest } from '@lib/index.ts'; const defaultLimit = import.meta.env.VITE_DEFAULT_LIMIT || 150; @@ -40,3 +40,12 @@ export async function getAirports({ }); return response?.json() || { data: [] }; } + +export async function importAirports(data: FormData) { + const response = await postRequest('airports/import', data, { + type: 'form' + }); + if (!response.ok) { + throw new Error('Upload failed'); + } +}