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 API_IMAGE=$DOCKER_HUB_USER_MANAGER_IMAGE
- export FOLDER=auth-domain - export FOLDER=auth-domain
- *stop-and-run - *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 API_IMAGE=$DOCKER_HUB_SUBSCRIPTION_IMAGE
- export FOLDER=subscription-domain - export FOLDER=subscription-domain

View File

@ -17,6 +17,14 @@ instance.interceptors.request.use((request) => {
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => { (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); return JSON.parse(response.data);
}, },
(error) => { (error) => {
@ -51,8 +59,13 @@ export const tokenStatus = (
}); });
}; };
export const fetchZones = (origin: string | null): Promise<Flight[]> => { interface FlightData {
return instance.get("flights" + (origin ? "?origin=" + origin : "")) 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 = ( export const createFlight = (

View File

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

View File

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

View File

@ -24,20 +24,17 @@ export const Card: React.FC<CardProps> = ({ flight }) => {
return ( return (
<div className="flight-card"> <div className="flight-card">
<Space size={8} align="center"> <Space size={8} align="center">
<Avatar size={64} icon={<RightOutlined />} />
<div> <div>
<Text strong>{flight.flight_code}</Text> <Text strong>{flight.flight_code} </Text>
<div> <Text type="secondary">
<Text type="secondary"> {flight.origin} <SwapOutlined /> {flight.destination}
{flight.origin} <SwapOutlined /> {flight.destination} </Text>
</Text>
</div>
</div> </div>
</Space> </Space>
<div className="flight-details"> <div className="flight-details">
<Space size={8} direction="vertical"> <Space size={8} direction="vertical">
<Text strong>Status:</Text> <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>
<Space size={8} direction="vertical"> <Space size={8} direction="vertical">
<Text strong>Departure:</Text> <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 { Card } from "./Card/Card";
import { useFetchZones } from "../../hooks/useFetchZones"; import { useFetchFlights } from "../../hooks/useFetchFlights";
import { Flight } from "../../Types"; import { Flight } from "../../Types";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import useAuth from "../../useAuth"; import useAuth from "../../useAuth";
@ -12,25 +12,56 @@ interface Props {
export const Home: React.FC<Props> = (props) => { export const Home: React.FC<Props> = (props) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const origin = urlParams.get('origin'); 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 navigate = useNavigate()
const [currentPage, setCurrentPage] = useState(initialPage);
const { loading, isAirline } = useAuth(); 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) { if (loading) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return ( return (
<div className="Box"> <div className="Box">
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>CREATE FLIGHT</button> : <></>} {isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>}
<h2>Flights</h2> <h2>Flights</h2>
<div className="Items"> <div className="Items">
{(props.flights ? props.flights : zones).map((u) => { {(props.flights ? props.flights : flights).map((u) => {
return <Card key={u.id} flight={u} />; return <Card key={u.id} flight={u} />;
})} })}
{error ? <div className="Disconnected">{error}</div> : <></>} {error ? <div className="Disconnected">{error}</div> : <></>}
</div> </div>
<div>
{checkMinPage() && <button onClick={goToPrevPage}>Prev</button>}
<span> Page {currentPage} </span>
{checkMaxPage() && <button onClick={goToNextPage}>Next</button>}
</div>
</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 { .Items {
height: 100%; display: grid;
width: 100%; grid-template-columns: repeat(4, minmax(80px, 250px));
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 20px; 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() return db.query(Flight).filter(Flight.id == flight_id).first()
def get_flights(db: Session, skip: int = 0, limit: int = 100): def get_flights(db: Session, page: int = 1, limit: int = 8):
return db.query(Flight).offset(skip).limit(limit).all() 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): def create_flight(db: Session, flight: FlightPydantic):

View File

@ -1,7 +1,15 @@
from typing import Annotated, Optional from typing import Annotated, Optional
from asyncreq import request 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 sqlalchemy.orm import Session
from src.api.config import API_MESSAGES from src.api.config import API_MESSAGES
@ -67,10 +75,12 @@ async def update_flight(
@router.get("", response_model=list[Flight]) @router.get("", response_model=list[Flight])
def get_flights( def get_flights(
response: Response,
origin: Optional[str] = None, origin: Optional[str] = None,
destination: Optional[str] = None, destination: Optional[str] = None,
lastUpdated: Optional[str] = None, lastUpdated: Optional[str] = None,
future: Optional[str] = None, future: Optional[str] = None,
page: Optional[int] = 1,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
if origin and lastUpdated: if origin and lastUpdated:
@ -86,7 +96,8 @@ def get_flights(
db=db, destination=destination, future=future db=db, destination=destination, future=future
) )
else: 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: if not flights:
raise HTTPException(status_code=404, detail="Flights not found") 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.config import API_DEBUG
from src.api.routes import auth, flights, health, notifications, subscriptions, users from src.api.routes import auth, flights, health, notifications, subscriptions, users
# from src.api.log import RouterLoggingMiddleware, logging_config
logging.config.dictConfig(logging_config) logging.config.dictConfig(logging_config)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,5 +30,6 @@ app.add_middleware(
allow_credentials=True, allow_credentials=True,
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"], allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["x-count"],
) )
app.add_middleware(RouterLoggingMiddleware, logger=logger, api_debug=API_DEBUG) app.add_middleware(RouterLoggingMiddleware, logger=logger, api_debug=API_DEBUG)

View File

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