From 2eb8cd3583a879c2000d01c4037fd31883872346 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:43:26 +0100 Subject: [PATCH 01/11] feat #309 migrer endpoint /metrics/activity-in-mpas --- backend/bloom/domain/metrics.py | 5 +++ backend/bloom/routers/v1/metrics.py | 20 ++++++++++ backend/bloom/services/metrics.py | 62 ++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index d59af644..133c5d75 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -39,6 +39,11 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel: VesselListView total_time_at_sea: Optional[timedelta] +class ResponseMetricsVesselInMpasSchema(BaseModel): + model_config = ConfigDict(from_attributes=True) + vessel: VesselListView + total_time_in_mpas: Optional[timedelta] + class ResponseMetricsZoneVisitedSchema(BaseModel): zone: ZoneListView visiting_duration: timedelta diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index 0f3e6cdf..1b5566bc 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -123,3 +123,23 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, category=category, sub_category=sub_category) return jsonable_encoder(payload) + + +@router.get("/metrics/vessels/activity-in-mpas") +# @cache +async def read_metrics_all_vessels_visiting_time_in_mpas( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_vessels_in_mpas( + datetime_range=datetime_range, + pagination=pagination, + order=order + ) + return jsonable_encoder(payload) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index c9a08ef1..6279d94c 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -18,6 +18,7 @@ from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInMpasSchema, ResponseMetricsZoneVisitingTimeByVesselSchema, ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, ResponseMetricsVesselVisitingTimeByZoneSchema) @@ -72,7 +73,50 @@ def getVesselsInActivity(self, total_time_at_sea=item[1] )\ for item in payload] - + + def get_vessels_in_mpas(self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest): + payload=[] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Vessel, + func.sum(sql_model.Metrics.duration_total).label( + "total_time_in_mpas" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Vessel, + sql_model.Vessel.id == sql_model.Metrics.vessel_id, + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .where(sql_model.Metrics.zone_category == "amp") + .group_by(sql_model.Vessel) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + stmt = ( + stmt.order_by(asc("total_time_in_mpas")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("total_time_in_mpas")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload=session.execute(stmt).all() + + return [ + ResponseMetricsVesselInMpasSchema( + vessel=VesselRepository.map_to_domain(item[0]).model_dump(), + total_time_in_mpas=item[1], + ) + for item in payload + ] + def getVesselsAtSea(self, datetime_range: DatetimeRangeRequest, ): @@ -95,7 +139,6 @@ def getVesselsAtSea(self, ) return session.execute(stmt).scalar() - def getZoneVisited(self, datetime_range: DatetimeRangeRequest, pagination: PageParams, @@ -137,7 +180,7 @@ def getZoneVisited(self, visiting_duration=item[1] )\ for item in payload] - + def getZoneVisitingTimeByVessel(self, zone_id: int, datetime_range: DatetimeRangeRequest, @@ -145,7 +188,7 @@ def getZoneVisitingTimeByVessel(self, pagination: PageParams,): payload=[] with self.session_factory() as session: - + stmt=select( sql_model.Zone, sql_model.Vessel, @@ -166,7 +209,7 @@ def getZoneVisitingTimeByVessel(self, ) )\ .group_by(sql_model.Zone.id,sql_model.Vessel.id) - + stmt = stmt.order_by(func.sum(sql_model.Segment.segment_duration).asc())\ if order.order == OrderByEnum.ascending \ else stmt.order_by(func.sum(sql_model.Segment.segment_duration).desc()) @@ -185,7 +228,7 @@ def getZoneVisitingTimeByVessel(self, zone_visiting_time_by_vessel=item[2] )\ for item in payload] - + def getVesselVisitingTimeByZone(self, order: OrderByRequest, datetime_range: DatetimeRangeRequest, @@ -226,14 +269,12 @@ def getVesselVisitingTimeByZone(self, stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt if vessel_id is not None: stmt=stmt.where(sql_model.Vessel.id==vessel_id) - - + return [ResponseMetricsVesselVisitingTimeByZoneSchema( vessel=VesselListView(**VesselRepository.map_to_domain(model[0]).model_dump()), zone=ZoneListView(**ZoneRepository.map_to_domain(model[1]).model_dump()), vessel_visiting_time_by_zone=model[2]) for model in session.execute(stmt).all()] - def getVesselVisitsByActivityType(self, vessel_id: int, activity_type: TotalTimeActivityTypeRequest, @@ -261,10 +302,9 @@ def getVesselVisitsByActivityType(self, literal_column('0 seconds'), )) payload=session.execute(stmt.limit(1)).scalar_one_or_none() - + return [ ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema( vessel_id=item.id, activity=item.activity, total_activity_time=item.total_activity_time, ) for item in payload] - \ No newline at end of file From 2dcfa2ae1a0e6b7a3eeb92adeb9298693c2afdb6 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:06:46 +0100 Subject: [PATCH 02/11] feat #309: migrate getTopVesselsInActivity to getTopVesselsInMpas (dashboard) --- frontend/app/dashboard/page.tsx | 26 ++++++++++------ .../dashboard/dashboard-overview.tsx | 31 +++++++++++-------- frontend/libs/mapper.tsx | 2 +- frontend/services/backend-rest-client.ts | 4 +-- frontend/services/dashboard.service.ts | 20 ++++++------ frontend/types/vessel.ts | 2 +- 6 files changed, 48 insertions(+), 37 deletions(-) diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 95bd06e8..46457ba5 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,11 +1,17 @@ -"use client" +"use client"; + +import { useMemo, useState } from "react"; +import { useDashboardData } from "@/services/dashboard.service"; + + + +import { getDateRange } from "@/libs/dateUtils"; +import DashboardHeader from "@/components/dashboard/dashboard-header"; +import DashboardOverview from "@/components/dashboard/dashboard-overview"; + + -import { useMemo, useState } from "react" -import { useDashboardData } from "@/services/dashboard.service" -import { getDateRange } from "@/libs/dateUtils" -import DashboardHeader from "@/components/dashboard/dashboard-header" -import DashboardOverview from "@/components/dashboard/dashboard-overview" export default function DashboardPage() { const [selectedDays, setSelectedDays] = useState(7) @@ -14,7 +20,7 @@ export default function DashboardPage() { }, [selectedDays]) const { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, @@ -31,7 +37,7 @@ export default function DashboardPage() {
{ setSelectedDays(Number(value)) }} - topVesselsInActivityLoading={isLoading.topVesselsInActivity} + topVesselsInMpasLoading={isLoading.topVesselsInMpas} topAmpsVisitedLoading={isLoading.topAmpsVisited} totalVesselsActiveLoading={isLoading.totalVesselsInActivity} totalAmpsVisitedLoading={isLoading.totalAmpsVisited} @@ -49,4 +55,4 @@ export default function DashboardPage() {
) -} +} \ No newline at end of file diff --git a/frontend/components/dashboard/dashboard-overview.tsx b/frontend/components/dashboard/dashboard-overview.tsx index 7f4c3c1d..ad992733 100644 --- a/frontend/components/dashboard/dashboard-overview.tsx +++ b/frontend/components/dashboard/dashboard-overview.tsx @@ -1,16 +1,21 @@ -"use client" +"use client"; -import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants" +import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants"; -import { Item } from "@/types/item" -import ListCard from "@/components/ui/list-card" -import KPICard from "@/components/dashboard/kpi-card" -import { DateRangeSelector } from "../ui/date-range-selector" + +import { Item } from "@/types/item"; +import ListCard from "@/components/ui/list-card"; +import KPICard from "@/components/dashboard/kpi-card"; + + + +import { DateRangeSelector } from "../ui/date-range-selector"; + type Props = { - topVesselsInActivity: Item[] - topVesselsInActivityLoading: boolean + topVesselsInMpas: Item[] + topVesselsInMpasLoading: boolean topAmpsVisited: Item[] topAmpsVisitedLoading: boolean totalVesselsActive: number @@ -23,8 +28,8 @@ type Props = { } export default function DashboardOverview({ - topVesselsInActivity, - topVesselsInActivityLoading, + topVesselsInMpas, + topVesselsInMpasLoading, topAmpsVisited, topAmpsVisitedLoading, totalVesselsActive, @@ -74,9 +79,9 @@ export default function DashboardOverview({ @@ -84,4 +89,4 @@ export default function DashboardOverview({ ) -} +} \ No newline at end of file diff --git a/frontend/libs/mapper.tsx b/frontend/libs/mapper.tsx index 9799cb0e..6778da8f 100644 --- a/frontend/libs/mapper.tsx +++ b/frontend/libs/mapper.tsx @@ -11,7 +11,7 @@ export function convertVesselDtoToItem(metrics: VesselMetrics[]): Item[] { id: `${vessel.id}`, title: vessel.ship_name, description: `IMO ${vessel.imo} / MMSI ${vessel.mmsi} / ${vessel.length} m`, - value: convertDurationToString(vesselMetrics.total_time_at_sea), + value: convertDurationToString(vesselMetrics.total_time_in_mpas), type: "vessel", countryIso3: vessel.country_iso3, } diff --git a/frontend/services/backend-rest-client.ts b/frontend/services/backend-rest-client.ts index f4f600f6..3aa81fe2 100644 --- a/frontend/services/backend-rest-client.ts +++ b/frontend/services/backend-rest-client.ts @@ -83,12 +83,12 @@ export async function getVesselFirstExcursionSegments(vesselId: number) { } } -export function getTopVesselsInActivity( +export function getTopVesselsInMpas( startAt: string, endAt: string, topVesselsLimit: number ) { - const url = `${BASE_URL}/metrics/vessels-in-activity?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` + const url = `${BASE_URL}/metrics/vessels/activity-in-mpas?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` console.log(`GET ${url}`) return axios.get(url) } diff --git a/frontend/services/dashboard.service.ts b/frontend/services/dashboard.service.ts index ef74e743..832c5b96 100644 --- a/frontend/services/dashboard.service.ts +++ b/frontend/services/dashboard.service.ts @@ -1,6 +1,6 @@ import { TOTAL_VESSELS } from "@/constants/totals.constants" import { - getTopVesselsInActivity, + getTopVesselsInMpas, getTopZonesVisited, getVesselsAtSea, getVesselsTrackedCount, @@ -13,13 +13,13 @@ import { convertVesselDtoToItem, convertZoneDtoToItem } from "@/libs/mapper" const TOP_ITEMS_SIZE = 5 type DashboardData = { - topVesselsInActivity: any[] + topVesselsInMpas: any[] topAmpsVisited: any[] totalVesselsInActivity: number totalAmpsVisited: number totalVesselsTracked: number isLoading: { - topVesselsInActivity: boolean + topVesselsInMpas: boolean topAmpsVisited: boolean totalVesselsInActivity: boolean totalAmpsVisited: boolean @@ -32,13 +32,13 @@ export const useDashboardData = ( endAt: string ): DashboardData => { const { - data: topVesselsInActivity = [], - isLoading: topVesselsInActivityLoading, + data: topVesselsInMpas = [], + isLoading: topVesselsInMpasLoading, } = useSWR( - `topVesselsInActivity-${startAt}`, + `topVesselsInMpas-${startAt}`, async () => { try { - const response = await getTopVesselsInActivity( + const response = await getTopVesselsInMpas( startAt, endAt, TOP_ITEMS_SIZE @@ -46,7 +46,7 @@ export const useDashboardData = ( return convertVesselDtoToItem(response?.data || []) } catch (error) { console.log( - "An error occurred while fetching top vessels in activity: " + error + "An error occurred while fetching top vessels in MPAs: " + error ) return [] } @@ -137,13 +137,13 @@ export const useDashboardData = ( ) return { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, totalVesselsTracked, isLoading: { - topVesselsInActivity: topVesselsInActivityLoading, + topVesselsInMpas: topVesselsInMpasLoading, topAmpsVisited: topAmpsVisitedLoading, totalVesselsInActivity: totalVesselsInActivityLoading, totalAmpsVisited: totalAmpsVisitedLoading, diff --git a/frontend/types/vessel.ts b/frontend/types/vessel.ts index f8626549..af5933ed 100644 --- a/frontend/types/vessel.ts +++ b/frontend/types/vessel.ts @@ -17,7 +17,7 @@ export type Vessel = { export type VesselMetrics = { vessel: VesselDetails - total_time_at_sea: string + total_time_in_mpas: string } export type VesselDetails = { From 83553f3f0aad42a87a465b8cae11281014d133f6 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:40:41 +0100 Subject: [PATCH 03/11] feat #204: global endpoints for dashboard rankings --- backend/bloom/domain/metrics.py | 4 +- backend/bloom/routers/v1/metrics.py | 29 ++++++++-- backend/bloom/services/metrics.py | 86 +++++++++++++++++++++++------ 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index 133c5d75..c7294177 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -39,10 +39,10 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel: VesselListView total_time_at_sea: Optional[timedelta] -class ResponseMetricsVesselInMpasSchema(BaseModel): +class ResponseMetricsVesselInZonesSchema(BaseModel): model_config = ConfigDict(from_attributes=True) vessel: VesselListView - total_time_in_mpas: Optional[timedelta] + total_time_in_zones: Optional[timedelta] class ResponseMetricsZoneVisitedSchema(BaseModel): zone: ZoneListView diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index 1b5566bc..b86827e4 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -125,11 +125,12 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, return jsonable_encoder(payload) -@router.get("/metrics/vessels/activity-in-mpas") +@router.get("/metrics/vessels-activity/{category}") # @cache -async def read_metrics_all_vessels_visiting_time_in_mpas( +async def read_metrics_all_vessels_visiting_time_in_zones( request: Request, datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, pagination: PageParams = Depends(), order: OrderByRequest = Depends(), key: str = Depends(X_API_KEY_HEADER), @@ -137,9 +138,29 @@ async def read_metrics_all_vessels_visiting_time_in_mpas( check_apikey(key) use_cases = UseCases() MetricsService = use_cases.metrics_service() - payload = MetricsService.get_vessels_in_mpas( + payload = MetricsService.get_vessels_activity_in_zones( datetime_range=datetime_range, pagination=pagination, - order=order + order=order, + category=category, + ) + return jsonable_encoder(payload) + + +@router.get("/metrics/zones-visited/{category}") +# @cache +async def read_metrics_all_zones_visited( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_zones_visited( + datetime_range=datetime_range, pagination=pagination, order=order, category=category ) return jsonable_encoder(payload) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index 6279d94c..b9d11351 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -16,12 +16,14 @@ from bloom.infra.repositories.repository_zone import ZoneRepository from bloom.domain.metrics import TotalTimeActivityTypeRequest -from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsVesselInMpasSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema, - ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, - ResponseMetricsVesselVisitingTimeByZoneSchema) +from bloom.domain.metrics import ( + ResponseMetricsVesselInActivitySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInZonesSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema, + ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, + ResponseMetricsVesselVisitingTimeByZoneSchema, +) class MetricsService(): def __init__( @@ -74,17 +76,20 @@ def getVesselsInActivity(self, )\ for item in payload] - def get_vessels_in_mpas(self, - datetime_range: DatetimeRangeRequest, - pagination: PageParams, - order: OrderByRequest): + def get_vessels_activity_in_zones( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): payload=[] with self.session_factory() as session: stmt = ( select( sql_model.Vessel, func.sum(sql_model.Metrics.duration_total).label( - "total_time_in_mpas" + "total_time_in_zones" ), ) .select_from(sql_model.Metrics) @@ -97,22 +102,71 @@ def get_vessels_in_mpas(self, datetime_range.start_at, datetime_range.end_at ) ) - .where(sql_model.Metrics.zone_category == "amp") .group_by(sql_model.Vessel) ) stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) stmt = ( - stmt.order_by(asc("total_time_in_mpas")) + stmt.order_by(asc("total_time_in_zones")) if order.order == OrderByEnum.ascending - else stmt.order_by(desc("total_time_in_mpas")) + else stmt.order_by(desc("total_time_in_zones")) ) stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt payload=session.execute(stmt).all() return [ - ResponseMetricsVesselInMpasSchema( + ResponseMetricsVesselInZonesSchema( vessel=VesselRepository.map_to_domain(item[0]).model_dump(), - total_time_in_mpas=item[1], + total_time_in_zones=item[1], + ) + for item in payload + ] + + def get_zones_visited( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): + payload = [] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Zone, + func.sum(sql_model.Metrics.duration_total).label( + "visiting_duration" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Zone, + sql_model.Zone.id == sql_model.Metrics.zone_id, + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .where(sql_model.Metrics.zone_category == category) + .group_by(sql_model.Zone) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) + stmt = ( + stmt.order_by(asc("visiting_duration")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("visiting_duration")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload = session.execute(stmt).all() + + return [ + ResponseMetricsZoneVisitedSchema( + zone=ZoneRepository.map_to_domain(item[0]).model_dump(), + visiting_duration=item[1], ) for item in payload ] From 5a7b7db064ed30e8fecad5340f9a1d756c218f97 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:51:09 +0100 Subject: [PATCH 04/11] feat: #204 global endpoint for dashboard rankings --- backend/bloom/routers/v1/metrics.py | 4 ++-- backend/bloom/services/metrics.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index b86827e4..4cd3afbf 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -125,7 +125,7 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, return jsonable_encoder(payload) -@router.get("/metrics/vessels-activity/{category}") +@router.get("/metrics/vessels-activity") # @cache async def read_metrics_all_vessels_visiting_time_in_zones( request: Request, @@ -147,7 +147,7 @@ async def read_metrics_all_vessels_visiting_time_in_zones( return jsonable_encoder(payload) -@router.get("/metrics/zones-visited/{category}") +@router.get("/metrics/zones-visited") # @cache async def read_metrics_all_zones_visited( request: Request, diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index b9d11351..b1909d35 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -87,7 +87,7 @@ def get_vessels_activity_in_zones( with self.session_factory() as session: stmt = ( select( - sql_model.Vessel, + sql_model.Vessel, func.sum(sql_model.Metrics.duration_total).label( "total_time_in_zones" ), @@ -95,14 +95,17 @@ def get_vessels_activity_in_zones( .select_from(sql_model.Metrics) .join( sql_model.Vessel, - sql_model.Vessel.id == sql_model.Metrics.vessel_id, + sql_model.Metrics.vessel_id == sql_model.Vessel.id, + isouter=True, ) .where( sql_model.Metrics.timestamp.between( datetime_range.start_at, datetime_range.end_at ) ) - .group_by(sql_model.Vessel) + .group_by( + sql_model.Vessel + ) ) stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt if category: From 5039e44d7841328b9ab99f87fff35b9e3da573de Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:57:28 +0100 Subject: [PATCH 05/11] migrate endpoints ranking dashboard --- frontend/libs/mapper.tsx | 2 +- frontend/services/backend-rest-client.ts | 4 ++-- frontend/types/vessel.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/libs/mapper.tsx b/frontend/libs/mapper.tsx index 6778da8f..e0ef0a9e 100644 --- a/frontend/libs/mapper.tsx +++ b/frontend/libs/mapper.tsx @@ -11,7 +11,7 @@ export function convertVesselDtoToItem(metrics: VesselMetrics[]): Item[] { id: `${vessel.id}`, title: vessel.ship_name, description: `IMO ${vessel.imo} / MMSI ${vessel.mmsi} / ${vessel.length} m`, - value: convertDurationToString(vesselMetrics.total_time_in_mpas), + value: convertDurationToString(vesselMetrics.total_time_in_zones), type: "vessel", countryIso3: vessel.country_iso3, } diff --git a/frontend/services/backend-rest-client.ts b/frontend/services/backend-rest-client.ts index 3aa81fe2..d94e8321 100644 --- a/frontend/services/backend-rest-client.ts +++ b/frontend/services/backend-rest-client.ts @@ -88,7 +88,7 @@ export function getTopVesselsInMpas( endAt: string, topVesselsLimit: number ) { - const url = `${BASE_URL}/metrics/vessels/activity-in-mpas?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` + const url = `${BASE_URL}/metrics/vessels-activity?category=amp&start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` console.log(`GET ${url}`) return axios.get(url) } @@ -99,7 +99,7 @@ export function getTopZonesVisited( topZonesLimit: number, category?: string ) { - const url = `${BASE_URL}/metrics/zone-visited?${ + const url = `${BASE_URL}/metrics/zones-visited?${ category ? `category=${category}&` : "" }start_at=${startAt}&end_at=${endAt}&limit=${topZonesLimit}&order=DESC` console.log(`GET ${url}`) diff --git a/frontend/types/vessel.ts b/frontend/types/vessel.ts index af5933ed..b6e109d9 100644 --- a/frontend/types/vessel.ts +++ b/frontend/types/vessel.ts @@ -17,7 +17,7 @@ export type Vessel = { export type VesselMetrics = { vessel: VesselDetails - total_time_in_mpas: string + total_time_in_zones: string } export type VesselDetails = { From 21bc9dcb390e84f74ab3fa7d73b792017e63434f Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:43:26 +0100 Subject: [PATCH 06/11] feat #309 migrer endpoint /metrics/activity-in-mpas --- backend/bloom/domain/metrics.py | 5 +++ backend/bloom/routers/v1/metrics.py | 20 ++++++++++ backend/bloom/services/metrics.py | 62 ++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index d59af644..133c5d75 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -39,6 +39,11 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel: VesselListView total_time_at_sea: Optional[timedelta] +class ResponseMetricsVesselInMpasSchema(BaseModel): + model_config = ConfigDict(from_attributes=True) + vessel: VesselListView + total_time_in_mpas: Optional[timedelta] + class ResponseMetricsZoneVisitedSchema(BaseModel): zone: ZoneListView visiting_duration: timedelta diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index 0f3e6cdf..1b5566bc 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -123,3 +123,23 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, category=category, sub_category=sub_category) return jsonable_encoder(payload) + + +@router.get("/metrics/vessels/activity-in-mpas") +# @cache +async def read_metrics_all_vessels_visiting_time_in_mpas( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_vessels_in_mpas( + datetime_range=datetime_range, + pagination=pagination, + order=order + ) + return jsonable_encoder(payload) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index c9a08ef1..6279d94c 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -18,6 +18,7 @@ from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInMpasSchema, ResponseMetricsZoneVisitingTimeByVesselSchema, ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, ResponseMetricsVesselVisitingTimeByZoneSchema) @@ -72,7 +73,50 @@ def getVesselsInActivity(self, total_time_at_sea=item[1] )\ for item in payload] - + + def get_vessels_in_mpas(self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest): + payload=[] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Vessel, + func.sum(sql_model.Metrics.duration_total).label( + "total_time_in_mpas" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Vessel, + sql_model.Vessel.id == sql_model.Metrics.vessel_id, + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .where(sql_model.Metrics.zone_category == "amp") + .group_by(sql_model.Vessel) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + stmt = ( + stmt.order_by(asc("total_time_in_mpas")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("total_time_in_mpas")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload=session.execute(stmt).all() + + return [ + ResponseMetricsVesselInMpasSchema( + vessel=VesselRepository.map_to_domain(item[0]).model_dump(), + total_time_in_mpas=item[1], + ) + for item in payload + ] + def getVesselsAtSea(self, datetime_range: DatetimeRangeRequest, ): @@ -95,7 +139,6 @@ def getVesselsAtSea(self, ) return session.execute(stmt).scalar() - def getZoneVisited(self, datetime_range: DatetimeRangeRequest, pagination: PageParams, @@ -137,7 +180,7 @@ def getZoneVisited(self, visiting_duration=item[1] )\ for item in payload] - + def getZoneVisitingTimeByVessel(self, zone_id: int, datetime_range: DatetimeRangeRequest, @@ -145,7 +188,7 @@ def getZoneVisitingTimeByVessel(self, pagination: PageParams,): payload=[] with self.session_factory() as session: - + stmt=select( sql_model.Zone, sql_model.Vessel, @@ -166,7 +209,7 @@ def getZoneVisitingTimeByVessel(self, ) )\ .group_by(sql_model.Zone.id,sql_model.Vessel.id) - + stmt = stmt.order_by(func.sum(sql_model.Segment.segment_duration).asc())\ if order.order == OrderByEnum.ascending \ else stmt.order_by(func.sum(sql_model.Segment.segment_duration).desc()) @@ -185,7 +228,7 @@ def getZoneVisitingTimeByVessel(self, zone_visiting_time_by_vessel=item[2] )\ for item in payload] - + def getVesselVisitingTimeByZone(self, order: OrderByRequest, datetime_range: DatetimeRangeRequest, @@ -226,14 +269,12 @@ def getVesselVisitingTimeByZone(self, stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt if vessel_id is not None: stmt=stmt.where(sql_model.Vessel.id==vessel_id) - - + return [ResponseMetricsVesselVisitingTimeByZoneSchema( vessel=VesselListView(**VesselRepository.map_to_domain(model[0]).model_dump()), zone=ZoneListView(**ZoneRepository.map_to_domain(model[1]).model_dump()), vessel_visiting_time_by_zone=model[2]) for model in session.execute(stmt).all()] - def getVesselVisitsByActivityType(self, vessel_id: int, activity_type: TotalTimeActivityTypeRequest, @@ -261,10 +302,9 @@ def getVesselVisitsByActivityType(self, literal_column('0 seconds'), )) payload=session.execute(stmt.limit(1)).scalar_one_or_none() - + return [ ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema( vessel_id=item.id, activity=item.activity, total_activity_time=item.total_activity_time, ) for item in payload] - \ No newline at end of file From 0b514d0a221b28e1176c63e03c3099031f4fda95 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:06:46 +0100 Subject: [PATCH 07/11] feat #309: migrate getTopVesselsInActivity to getTopVesselsInMpas (dashboard) --- frontend/app/dashboard/page.tsx | 26 ++++++++++------ .../dashboard/dashboard-overview.tsx | 31 +++++++++++-------- frontend/libs/mapper.tsx | 2 +- frontend/services/backend-rest-client.ts | 4 +-- frontend/services/dashboard.service.ts | 20 ++++++------ frontend/types/vessel.ts | 2 +- 6 files changed, 48 insertions(+), 37 deletions(-) diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 95bd06e8..46457ba5 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,11 +1,17 @@ -"use client" +"use client"; + +import { useMemo, useState } from "react"; +import { useDashboardData } from "@/services/dashboard.service"; + + + +import { getDateRange } from "@/libs/dateUtils"; +import DashboardHeader from "@/components/dashboard/dashboard-header"; +import DashboardOverview from "@/components/dashboard/dashboard-overview"; + + -import { useMemo, useState } from "react" -import { useDashboardData } from "@/services/dashboard.service" -import { getDateRange } from "@/libs/dateUtils" -import DashboardHeader from "@/components/dashboard/dashboard-header" -import DashboardOverview from "@/components/dashboard/dashboard-overview" export default function DashboardPage() { const [selectedDays, setSelectedDays] = useState(7) @@ -14,7 +20,7 @@ export default function DashboardPage() { }, [selectedDays]) const { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, @@ -31,7 +37,7 @@ export default function DashboardPage() {
{ setSelectedDays(Number(value)) }} - topVesselsInActivityLoading={isLoading.topVesselsInActivity} + topVesselsInMpasLoading={isLoading.topVesselsInMpas} topAmpsVisitedLoading={isLoading.topAmpsVisited} totalVesselsActiveLoading={isLoading.totalVesselsInActivity} totalAmpsVisitedLoading={isLoading.totalAmpsVisited} @@ -49,4 +55,4 @@ export default function DashboardPage() {
) -} +} \ No newline at end of file diff --git a/frontend/components/dashboard/dashboard-overview.tsx b/frontend/components/dashboard/dashboard-overview.tsx index 7f4c3c1d..ad992733 100644 --- a/frontend/components/dashboard/dashboard-overview.tsx +++ b/frontend/components/dashboard/dashboard-overview.tsx @@ -1,16 +1,21 @@ -"use client" +"use client"; -import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants" +import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants"; -import { Item } from "@/types/item" -import ListCard from "@/components/ui/list-card" -import KPICard from "@/components/dashboard/kpi-card" -import { DateRangeSelector } from "../ui/date-range-selector" + +import { Item } from "@/types/item"; +import ListCard from "@/components/ui/list-card"; +import KPICard from "@/components/dashboard/kpi-card"; + + + +import { DateRangeSelector } from "../ui/date-range-selector"; + type Props = { - topVesselsInActivity: Item[] - topVesselsInActivityLoading: boolean + topVesselsInMpas: Item[] + topVesselsInMpasLoading: boolean topAmpsVisited: Item[] topAmpsVisitedLoading: boolean totalVesselsActive: number @@ -23,8 +28,8 @@ type Props = { } export default function DashboardOverview({ - topVesselsInActivity, - topVesselsInActivityLoading, + topVesselsInMpas, + topVesselsInMpasLoading, topAmpsVisited, topAmpsVisitedLoading, totalVesselsActive, @@ -74,9 +79,9 @@ export default function DashboardOverview({ @@ -84,4 +89,4 @@ export default function DashboardOverview({ ) -} +} \ No newline at end of file diff --git a/frontend/libs/mapper.tsx b/frontend/libs/mapper.tsx index 9799cb0e..6778da8f 100644 --- a/frontend/libs/mapper.tsx +++ b/frontend/libs/mapper.tsx @@ -11,7 +11,7 @@ export function convertVesselDtoToItem(metrics: VesselMetrics[]): Item[] { id: `${vessel.id}`, title: vessel.ship_name, description: `IMO ${vessel.imo} / MMSI ${vessel.mmsi} / ${vessel.length} m`, - value: convertDurationToString(vesselMetrics.total_time_at_sea), + value: convertDurationToString(vesselMetrics.total_time_in_mpas), type: "vessel", countryIso3: vessel.country_iso3, } diff --git a/frontend/services/backend-rest-client.ts b/frontend/services/backend-rest-client.ts index 58293271..834ee4d4 100644 --- a/frontend/services/backend-rest-client.ts +++ b/frontend/services/backend-rest-client.ts @@ -114,12 +114,12 @@ export async function getVesselFirstExcursionSegments(vesselId: number) { } } -export function getTopVesselsInActivity( +export function getTopVesselsInMpas( startAt: string, endAt: string, topVesselsLimit: number ) { - const url = `${BASE_URL}/metrics/vessels-in-activity?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` + const url = `${BASE_URL}/metrics/vessels/activity-in-mpas?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` console.log(`GET ${url}`) return axios.get(url) } diff --git a/frontend/services/dashboard.service.ts b/frontend/services/dashboard.service.ts index ef74e743..832c5b96 100644 --- a/frontend/services/dashboard.service.ts +++ b/frontend/services/dashboard.service.ts @@ -1,6 +1,6 @@ import { TOTAL_VESSELS } from "@/constants/totals.constants" import { - getTopVesselsInActivity, + getTopVesselsInMpas, getTopZonesVisited, getVesselsAtSea, getVesselsTrackedCount, @@ -13,13 +13,13 @@ import { convertVesselDtoToItem, convertZoneDtoToItem } from "@/libs/mapper" const TOP_ITEMS_SIZE = 5 type DashboardData = { - topVesselsInActivity: any[] + topVesselsInMpas: any[] topAmpsVisited: any[] totalVesselsInActivity: number totalAmpsVisited: number totalVesselsTracked: number isLoading: { - topVesselsInActivity: boolean + topVesselsInMpas: boolean topAmpsVisited: boolean totalVesselsInActivity: boolean totalAmpsVisited: boolean @@ -32,13 +32,13 @@ export const useDashboardData = ( endAt: string ): DashboardData => { const { - data: topVesselsInActivity = [], - isLoading: topVesselsInActivityLoading, + data: topVesselsInMpas = [], + isLoading: topVesselsInMpasLoading, } = useSWR( - `topVesselsInActivity-${startAt}`, + `topVesselsInMpas-${startAt}`, async () => { try { - const response = await getTopVesselsInActivity( + const response = await getTopVesselsInMpas( startAt, endAt, TOP_ITEMS_SIZE @@ -46,7 +46,7 @@ export const useDashboardData = ( return convertVesselDtoToItem(response?.data || []) } catch (error) { console.log( - "An error occurred while fetching top vessels in activity: " + error + "An error occurred while fetching top vessels in MPAs: " + error ) return [] } @@ -137,13 +137,13 @@ export const useDashboardData = ( ) return { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, totalVesselsTracked, isLoading: { - topVesselsInActivity: topVesselsInActivityLoading, + topVesselsInMpas: topVesselsInMpasLoading, topAmpsVisited: topAmpsVisitedLoading, totalVesselsInActivity: totalVesselsInActivityLoading, totalAmpsVisited: totalAmpsVisitedLoading, diff --git a/frontend/types/vessel.ts b/frontend/types/vessel.ts index ca4ff5e7..7fe7e09c 100644 --- a/frontend/types/vessel.ts +++ b/frontend/types/vessel.ts @@ -19,7 +19,7 @@ export type Vessel = { export type VesselMetrics = { vessel: VesselDetails - total_time_at_sea: string + total_time_in_mpas: string } export type VesselDetails = { From c7e924991b7c5e5121cc19786d8773f04c142782 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:40:41 +0100 Subject: [PATCH 08/11] feat #204: global endpoints for dashboard rankings --- backend/bloom/domain/metrics.py | 4 +- backend/bloom/routers/v1/metrics.py | 29 ++++++++-- backend/bloom/services/metrics.py | 86 +++++++++++++++++++++++------ 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index 133c5d75..c7294177 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -39,10 +39,10 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel: VesselListView total_time_at_sea: Optional[timedelta] -class ResponseMetricsVesselInMpasSchema(BaseModel): +class ResponseMetricsVesselInZonesSchema(BaseModel): model_config = ConfigDict(from_attributes=True) vessel: VesselListView - total_time_in_mpas: Optional[timedelta] + total_time_in_zones: Optional[timedelta] class ResponseMetricsZoneVisitedSchema(BaseModel): zone: ZoneListView diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index 1b5566bc..b86827e4 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -125,11 +125,12 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, return jsonable_encoder(payload) -@router.get("/metrics/vessels/activity-in-mpas") +@router.get("/metrics/vessels-activity/{category}") # @cache -async def read_metrics_all_vessels_visiting_time_in_mpas( +async def read_metrics_all_vessels_visiting_time_in_zones( request: Request, datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, pagination: PageParams = Depends(), order: OrderByRequest = Depends(), key: str = Depends(X_API_KEY_HEADER), @@ -137,9 +138,29 @@ async def read_metrics_all_vessels_visiting_time_in_mpas( check_apikey(key) use_cases = UseCases() MetricsService = use_cases.metrics_service() - payload = MetricsService.get_vessels_in_mpas( + payload = MetricsService.get_vessels_activity_in_zones( datetime_range=datetime_range, pagination=pagination, - order=order + order=order, + category=category, + ) + return jsonable_encoder(payload) + + +@router.get("/metrics/zones-visited/{category}") +# @cache +async def read_metrics_all_zones_visited( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_zones_visited( + datetime_range=datetime_range, pagination=pagination, order=order, category=category ) return jsonable_encoder(payload) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index 6279d94c..b9d11351 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -16,12 +16,14 @@ from bloom.infra.repositories.repository_zone import ZoneRepository from bloom.domain.metrics import TotalTimeActivityTypeRequest -from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsVesselInMpasSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema, - ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, - ResponseMetricsVesselVisitingTimeByZoneSchema) +from bloom.domain.metrics import ( + ResponseMetricsVesselInActivitySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInZonesSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema, + ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, + ResponseMetricsVesselVisitingTimeByZoneSchema, +) class MetricsService(): def __init__( @@ -74,17 +76,20 @@ def getVesselsInActivity(self, )\ for item in payload] - def get_vessels_in_mpas(self, - datetime_range: DatetimeRangeRequest, - pagination: PageParams, - order: OrderByRequest): + def get_vessels_activity_in_zones( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): payload=[] with self.session_factory() as session: stmt = ( select( sql_model.Vessel, func.sum(sql_model.Metrics.duration_total).label( - "total_time_in_mpas" + "total_time_in_zones" ), ) .select_from(sql_model.Metrics) @@ -97,22 +102,71 @@ def get_vessels_in_mpas(self, datetime_range.start_at, datetime_range.end_at ) ) - .where(sql_model.Metrics.zone_category == "amp") .group_by(sql_model.Vessel) ) stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) stmt = ( - stmt.order_by(asc("total_time_in_mpas")) + stmt.order_by(asc("total_time_in_zones")) if order.order == OrderByEnum.ascending - else stmt.order_by(desc("total_time_in_mpas")) + else stmt.order_by(desc("total_time_in_zones")) ) stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt payload=session.execute(stmt).all() return [ - ResponseMetricsVesselInMpasSchema( + ResponseMetricsVesselInZonesSchema( vessel=VesselRepository.map_to_domain(item[0]).model_dump(), - total_time_in_mpas=item[1], + total_time_in_zones=item[1], + ) + for item in payload + ] + + def get_zones_visited( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): + payload = [] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Zone, + func.sum(sql_model.Metrics.duration_total).label( + "visiting_duration" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Zone, + sql_model.Zone.id == sql_model.Metrics.zone_id, + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .where(sql_model.Metrics.zone_category == category) + .group_by(sql_model.Zone) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) + stmt = ( + stmt.order_by(asc("visiting_duration")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("visiting_duration")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload = session.execute(stmt).all() + + return [ + ResponseMetricsZoneVisitedSchema( + zone=ZoneRepository.map_to_domain(item[0]).model_dump(), + visiting_duration=item[1], ) for item in payload ] From 183ffa5dc99dcca086c37a414fa8396f201490f0 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:51:09 +0100 Subject: [PATCH 09/11] feat: #204 global endpoint for dashboard rankings --- backend/bloom/routers/v1/metrics.py | 4 ++-- backend/bloom/services/metrics.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index b86827e4..4cd3afbf 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -125,7 +125,7 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, return jsonable_encoder(payload) -@router.get("/metrics/vessels-activity/{category}") +@router.get("/metrics/vessels-activity") # @cache async def read_metrics_all_vessels_visiting_time_in_zones( request: Request, @@ -147,7 +147,7 @@ async def read_metrics_all_vessels_visiting_time_in_zones( return jsonable_encoder(payload) -@router.get("/metrics/zones-visited/{category}") +@router.get("/metrics/zones-visited") # @cache async def read_metrics_all_zones_visited( request: Request, diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index b9d11351..b1909d35 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -87,7 +87,7 @@ def get_vessels_activity_in_zones( with self.session_factory() as session: stmt = ( select( - sql_model.Vessel, + sql_model.Vessel, func.sum(sql_model.Metrics.duration_total).label( "total_time_in_zones" ), @@ -95,14 +95,17 @@ def get_vessels_activity_in_zones( .select_from(sql_model.Metrics) .join( sql_model.Vessel, - sql_model.Vessel.id == sql_model.Metrics.vessel_id, + sql_model.Metrics.vessel_id == sql_model.Vessel.id, + isouter=True, ) .where( sql_model.Metrics.timestamp.between( datetime_range.start_at, datetime_range.end_at ) ) - .group_by(sql_model.Vessel) + .group_by( + sql_model.Vessel + ) ) stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt if category: From 32eca8dadfbe56a46fe6b7ed1fc020ee4b579fe6 Mon Sep 17 00:00:00 2001 From: marthevienne <123016211+marthevienne@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:57:28 +0100 Subject: [PATCH 10/11] migrate endpoints ranking dashboard --- frontend/libs/mapper.tsx | 2 +- frontend/services/backend-rest-client.ts | 4 ++-- frontend/types/vessel.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/libs/mapper.tsx b/frontend/libs/mapper.tsx index 6778da8f..e0ef0a9e 100644 --- a/frontend/libs/mapper.tsx +++ b/frontend/libs/mapper.tsx @@ -11,7 +11,7 @@ export function convertVesselDtoToItem(metrics: VesselMetrics[]): Item[] { id: `${vessel.id}`, title: vessel.ship_name, description: `IMO ${vessel.imo} / MMSI ${vessel.mmsi} / ${vessel.length} m`, - value: convertDurationToString(vesselMetrics.total_time_in_mpas), + value: convertDurationToString(vesselMetrics.total_time_in_zones), type: "vessel", countryIso3: vessel.country_iso3, } diff --git a/frontend/services/backend-rest-client.ts b/frontend/services/backend-rest-client.ts index 834ee4d4..fbd05297 100644 --- a/frontend/services/backend-rest-client.ts +++ b/frontend/services/backend-rest-client.ts @@ -119,7 +119,7 @@ export function getTopVesselsInMpas( endAt: string, topVesselsLimit: number ) { - const url = `${BASE_URL}/metrics/vessels/activity-in-mpas?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` + const url = `${BASE_URL}/metrics/vessels-activity?category=amp&start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` console.log(`GET ${url}`) return axios.get(url) } @@ -130,7 +130,7 @@ export function getTopZonesVisited( topZonesLimit: number, category?: string ) { - const url = `${BASE_URL}/metrics/zone-visited?${ + const url = `${BASE_URL}/metrics/zones-visited?${ category ? `category=${category}&` : "" }start_at=${startAt}&end_at=${endAt}&limit=${topZonesLimit}&order=DESC` console.log(`GET ${url}`) diff --git a/frontend/types/vessel.ts b/frontend/types/vessel.ts index 7fe7e09c..43153845 100644 --- a/frontend/types/vessel.ts +++ b/frontend/types/vessel.ts @@ -19,7 +19,7 @@ export type Vessel = { export type VesselMetrics = { vessel: VesselDetails - total_time_in_mpas: string + total_time_in_zones: string } export type VesselDetails = { From cc731b50e4c965aab19b1e27e1cd32f7dc4093ed Mon Sep 17 00:00:00 2001 From: RV Date: Tue, 17 Dec 2024 20:53:58 +0100 Subject: [PATCH 11/11] fix: request - isouter + join dim_zone (to check) --- backend/bloom/services/metrics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index b1909d35..7984178a 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -95,8 +95,11 @@ def get_vessels_activity_in_zones( .select_from(sql_model.Metrics) .join( sql_model.Vessel, - sql_model.Metrics.vessel_id == sql_model.Vessel.id, - isouter=True, + sql_model.Metrics.vessel_id == sql_model.Vessel.id + ) + .join( + sql_model.Zone, + sql_model.Metrics.zone_id == sql_model.Zone.id ) .where( sql_model.Metrics.timestamp.between(