Implement pagination and update frontend

This commit is contained in:
Santiago Lo Coco 2023-11-12 12:25:13 -03:00
parent 51f0c7b168
commit 7f057dbbaf
14 changed files with 116 additions and 223 deletions

View File

@ -450,8 +450,6 @@ deploy-prod:
- export API_IMAGE=$DOCKER_HUB_USER_MANAGER_IMAGE
- export FOLDER=auth-domain
- *stop-and-run
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec auth-api python manage.py recreate_db
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec auth-api python manage.py seed_db
- export API_IMAGE=$DOCKER_HUB_SUBSCRIPTION_IMAGE
- export FOLDER=subscription-domain

View File

@ -17,6 +17,14 @@ instance.interceptors.request.use((request) => {
instance.interceptors.response.use(
(response) => {
console.log(response.headers)
if (response.headers["x-count"]) {
let json: any = {}
json["flights"] = JSON.parse(response.data);
json["count"] = response.headers["x-count"]
console.log(json)
return json
}
return JSON.parse(response.data);
},
(error) => {
@ -51,8 +59,13 @@ export const tokenStatus = (
});
};
export const fetchZones = (origin: string | null): Promise<Flight[]> => {
return instance.get("flights" + (origin ? "?origin=" + origin : ""))
interface FlightData {
flights: Flight[]
count: number
}
export const fetchFlights = (origin: string | null, page: number | null): Promise<FlightData> => {
return instance.get("flights" + (origin ? "?origin=" + origin : "") + (page ? "?page=" + page : ""))
};
export const createFlight = (

View File

@ -11,9 +11,9 @@ export const CreateFlight = () => {
const [flightData, setFlightData] = useState<FlightCreate>({
flight_code: "ABC123",
status: "En ruta",
origin: "Ciudad A",
destination: "Ciudad B",
status: "Scheduled",
origin: "Frankfurt",
destination: "Rome",
departure_time: "2023-10-09 10:00 AM",
arrival_time: "2023-10-09 12:00 PM",
gate: "A1",

View File

@ -1,8 +1,7 @@
.flight-card {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;

View File

@ -24,20 +24,17 @@ export const Card: React.FC<CardProps> = ({ flight }) => {
return (
<div className="flight-card">
<Space size={8} align="center">
<Avatar size={64} icon={<RightOutlined />} />
<div>
<Text strong>{flight.flight_code} </Text>
<div>
<Text type="secondary">
{flight.origin} <SwapOutlined /> {flight.destination}
</Text>
</div>
</div>
</Space>
<div className="flight-details">
<Space size={8} direction="vertical">
<Text strong>Status:</Text>
<Tag color={flight.status === "En ruta" ? "green" : "orange"}>{flight.status}</Tag>
<Tag color={flight.status === "Scheduled" ? "green" : "orange"}>{flight.status}</Tag>
</Space>
<Space size={8} direction="vertical">
<Text strong>Departure:</Text>

View File

@ -1,6 +1,6 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Card } from "./Card/Card";
import { useFetchZones } from "../../hooks/useFetchZones";
import { useFetchFlights } from "../../hooks/useFetchFlights";
import { Flight } from "../../Types";
import { useNavigate } from "react-router";
import useAuth from "../../useAuth";
@ -12,25 +12,56 @@ interface Props {
export const Home: React.FC<Props> = (props) => {
const urlParams = new URLSearchParams(window.location.search);
const origin = urlParams.get('origin');
const { zones, error } = useFetchZones(origin);
const initialPage = parseInt(urlParams.get('page') || '1', 10);
const { flights, count, error } = useFetchFlights(origin, initialPage);
const navigate = useNavigate()
const [currentPage, setCurrentPage] = useState(initialPage);
const { loading, isAirline } = useAuth();
useEffect(() => {
const newParams = new URLSearchParams(window.location.search);
newParams.set('page', currentPage.toString());
navigate(`?${newParams.toString()}`);
}, [currentPage, navigate]);
const goToPrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const goToNextPage = () => {
setCurrentPage(currentPage + 1);
};
const checkMaxPage = () => {
return currentPage * 8 >= count ? false : true
}
const checkMinPage = () => {
return currentPage > 1 ? true : false
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="Box">
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>CREATE FLIGHT</button> : <></>}
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>}
<h2>Flights</h2>
<div className="Items">
{(props.flights ? props.flights : zones).map((u) => {
{(props.flights ? props.flights : flights).map((u) => {
return <Card key={u.id} flight={u} />;
})}
{error ? <div className="Disconnected">{error}</div> : <></>}
</div>
<div>
{checkMinPage() && <button onClick={goToPrevPage}>Prev</button>}
<span> Page {currentPage} </span>
{checkMaxPage() && <button onClick={goToNextPage}>Next</button>}
</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
import React, { useEffect } from "react";
import { useState } from "react";
import { User, Flight } from "../Types";
import { fetchFlights } from "../Api";
export const useFetchFlights = (origin: string | null, page: number | null) => {
const [error, setError] = useState<string | null>(null);
const [flights, setFlights] = useState<Flight[]>([]);
const [count, setCount] = useState<number>(0);
useEffect(() => {
setError(null);
fetchFlights(origin, page)
.then((data) => {
setCount(data.count)
setFlights(data.flights);
})
.catch((error) => { });
}, [page]);
return { flights, count, error };
};

View File

@ -1,21 +0,0 @@
import React, { useEffect } from "react";
import { useState } from "react";
import { User, Flight } from "../Types";
import { fetchZones } from "../Api";
export const useFetchZones = (origin: string | null) => {
const [error, setError] = useState<string | null>(null);
const [zones, setZones] = useState<Flight[]>([]);
useEffect(() => {
setError(null);
fetchZones(origin)
.then((data) => {
setZones(data);
})
.catch((error) => { });
}, []);
return { zones, error };
};

View File

@ -85,22 +85,7 @@ code {
}
.Items {
height: 100%;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
display: grid;
grid-template-columns: repeat(4, minmax(80px, 250px));
gap: 20px;
}
.List {
width: 100%;
height: 500px;
gap: 30px;
padding: 20px;
overflow-y: auto;
display: flex;
align-items: center;
flex-direction: column;
}

View File

@ -59,8 +59,12 @@ def get_flight_by_id(db: Session, flight_id: int):
return db.query(Flight).filter(Flight.id == flight_id).first()
def get_flights(db: Session, skip: int = 0, limit: int = 100):
return db.query(Flight).offset(skip).limit(limit).all()
def get_flights(db: Session, page: int = 1, limit: int = 8):
if page <= 0:
page = 1
skip = (page - 1) * limit
count = db.query(Flight).count()
return db.query(Flight).offset(skip).limit(limit).all(), count
def create_flight(db: Session, flight: FlightPydantic):

View File

@ -1,7 +1,15 @@
from typing import Annotated, Optional
from asyncreq import request
from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, Request
from fastapi import (
APIRouter,
BackgroundTasks,
Depends,
Header,
HTTPException,
Request,
Response,
)
from sqlalchemy.orm import Session
from src.api.config import API_MESSAGES
@ -67,10 +75,12 @@ async def update_flight(
@router.get("", response_model=list[Flight])
def get_flights(
response: Response,
origin: Optional[str] = None,
destination: Optional[str] = None,
lastUpdated: Optional[str] = None,
future: Optional[str] = None,
page: Optional[int] = 1,
db: Session = Depends(get_db),
):
if origin and lastUpdated:
@ -86,7 +96,8 @@ def get_flights(
db=db, destination=destination, future=future
)
else:
flights = flight_crud.get_flights(db=db)
flights, count = flight_crud.get_flights(db=db, page=page)
response.headers["X-Count"] = str(count)
if not flights:
raise HTTPException(status_code=404, detail="Flights not found")

View File

@ -1,150 +0,0 @@
import json
import logging
import sys
import time
from typing import Callable
from uuid import uuid4
from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import Message
logging_config = {
"version": 1,
"formatters": {
"json": {
"class": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(process)s %(levelname)s",
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "json",
"stream": sys.stderr,
}
},
"root": {"level": "DEBUG", "handlers": ["console"], "propagate": True},
}
class RouterLoggingMiddleware(BaseHTTPMiddleware):
def __init__(
self, app: FastAPI, *, logger: logging.Logger, api_debug: bool = False
) -> None:
self._logger = logger
self.api_debug = api_debug
super().__init__(app)
async def dispatch(self, request: Request, call_next: Callable) -> Response:
request_header = request.headers.get("x-api-request-id")
if request_header is not None:
request_id = request_header
else:
request_id: str = str(uuid4())
logging_dict = {"X-API-REQUEST-ID": request_id}
if self.api_debug:
await self.set_body(request)
response, response_dict = await self._log_response(
call_next, request, request_id
)
request_dict = await self._log_request(request)
logging_dict["request"] = request_dict
logging_dict["response"] = response_dict
self._logger.info(logging_dict)
return response
async def set_body(self, request: Request):
_receive = await request._receive()
async def receive() -> Message:
return _receive
request._receive = receive
async def _log_request(self, request: Request) -> str:
path = request.url.path
if request.query_params:
path += f"?{request.query_params}"
request_logging = {
"method": request.method,
"path": path,
"ip": request.client.host,
}
if self.api_debug:
try:
body = await request.json()
request_logging["body"] = body
except ValueError:
body = None
return request_logging
async def _log_response(
self, call_next: Callable, request: Request, request_id: str
) -> Response:
start_time = time.perf_counter()
response = await self._execute_request(call_next, request, request_id)
finish_time = time.perf_counter()
overall_status = "successful" if response.status_code < 400 else "failed"
execution_time = finish_time - start_time
response_logging = {
"status": overall_status,
"status_code": response.status_code,
"time_taken": f"{execution_time:0.4f}s",
}
if self.api_debug:
resp_body = [
section async for section in response.__dict__["body_iterator"]
]
response.__setattr__("body_iterator", AsyncIteratorWrapper(resp_body))
try:
resp_body = json.loads(resp_body[0].decode())
except ValueError:
resp_body = str(resp_body)
response_logging["body"] = resp_body
return response, response_logging
async def _execute_request(
self, call_next: Callable, request: Request, request_id: str
) -> Response:
try:
request.state.request_id = request_id
response: Response = await call_next(request)
response.headers["X-API-Request-ID"] = request_id
return response
except Exception as e:
self._logger.exception(
{"path": request.url.path, "method": request.method, "reason": e}
)
class AsyncIteratorWrapper:
def __init__(self, obj):
self._it = iter(obj)
def __aiter__(self):
return self
async def __anext__(self):
try:
value = next(self._it)
except StopIteration:
raise StopAsyncIteration
return value

View File

@ -7,9 +7,6 @@ from logmiddleware import RouterLoggingMiddleware, logging_config
from src.api.config import API_DEBUG
from src.api.routes import auth, flights, health, notifications, subscriptions, users
# from src.api.log import RouterLoggingMiddleware, logging_config
logging.config.dictConfig(logging_config)
logger = logging.getLogger(__name__)
@ -33,5 +30,6 @@ app.add_middleware(
allow_credentials=True,
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
expose_headers=["x-count"],
)
app.add_middleware(RouterLoggingMiddleware, logger=logger, api_debug=API_DEBUG)

View File

@ -1,7 +1,7 @@
from typing import Annotated, Optional
from asyncreq import request
from fastapi import APIRouter, Header, HTTPException, Request
from fastapi import APIRouter, Header, HTTPException, Request, Response
from src.api.config import API_FLIGHTS
from src.api.routes.auth import checkAuth
@ -29,9 +29,9 @@ async def create_flight(
req: Request,
authorization: Annotated[str | None, Header()] = None,
):
auth = await checkAuth(req, authorization, isAirline=True)
id = await checkAuth(req, authorization, isAirline=True)
flight_data = flight.model_dump()
flight_data["user_id"] = auth["id"]
flight_data["user_id"] = id
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
@ -49,9 +49,9 @@ async def update_flight(
req: Request,
authorization: Annotated[str | None, Header()] = None,
):
auth = await checkAuth(req, authorization, isAirline=True)
id = await checkAuth(req, authorization, isAirline=True)
update = flight_update.model_dump()
update["user_id"] = auth["id"]
update["user_id"] = id
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
@ -65,9 +65,11 @@ async def update_flight(
@router.get("", response_model=list[Flight])
async def get_flights(
req: Request,
res: Response,
origin: Optional[str] = None,
destination: Optional[str] = None,
lastUpdated: Optional[str] = None,
page: Optional[int] = 1,
future: Optional[str] = None,
):
query = {}
@ -79,11 +81,14 @@ async def get_flights(
query["lastUpdated"] = lastUpdated
if future:
query["future"] = future
if page:
query["page"] = page
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
(response, status, headers) = await request(
f"{API_FLIGHTS}", "GET", query=query, headers=header
)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
res.headers["x-count"] = headers["x-count"]
return response