diff --git a/Makefile b/Makefile index 11bf2d0785..6a68e43b6d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,37 @@ INFRA_FOLDER="$(shell pwd)/infra/configurations/" HOST_MIGRATIONS_FOLDER=$(shell pwd)/backend/src/main/resources/db/migration -.PHONY: clean install test +SHELL := /bin/bash +.SHELLFLAGS = -ec +.SILENT: +MAKEFLAGS += --silent +.ONESHELL: + +.DEFAULT_GOAL: help + +.PHONY: help ##OTHER 🛟 To display this prompts. This will list all available targets with their documentation +help: + echo "❓ Use \`make ' where is one of 👇" + echo "" + echo -e "\033[1mLocal Development\033[0m:" + grep -E '^\.PHONY: [a-zA-Z0-9_-]+ .*?##LOCAL' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = "(: |##LOCAL)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + echo "" + echo -e "\033[1mTesting\033[0m:" + grep -E '^\.PHONY: [a-zA-Z0-9_-]+ .*?##TEST' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = "(: |##TEST)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + echo "" + echo -e "\033[1mCommands for RUN (STAGING and PROD)\033[0m:" + grep -E '^\.PHONY: [a-zA-Z0-9_-]+ .*?##RUN' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = "(: |##RUN)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + echo "" + echo -e "\033[1mOther commands\033[0m:" + grep -E '^\.PHONY: [a-zA-Z0-9_-]+ .*?##OTHER' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = "(: |##OTHER)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' + echo "" + echo "Tips 💡" + echo " - use tab for auto-completion" + echo " - use the dry run option '-n' to show what make is attempting to do. example: environmentName=dev make -n deploy" docker-env: cd ./infra/docker && ../../frontend/node_modules/.bin/import-meta-env-prepare -u -x ./.env.local.defaults\ @@ -10,9 +40,11 @@ docker-env: ################################################################################ # Local Development -check-clean-archi: +.PHONY: check-clean-archi ##LOCAL Check clean architecture imports +check-clean-archi: cd backend/tools && ./check-clean-architecture.sh +.PHONY: clean ##LOCAL Clean all backend assets and stop docker containers clean: docker-env rm -Rf ./backend/target docker compose down -v @@ -26,24 +58,25 @@ compile-back: init-local-sig: ./infra/local/postgis_insert_layers.sh && ./infra/init/geoserver_init_layers.sh +.PHONY: install-front ##LOCAL ⬇️ Install frontend dependencies install-front: cd ./frontend && npm i +.PHONY: run-back ##LOCAL ▶️ Run backend API run-back: run-stubbed-apis docker compose up -d --quiet-pull --wait db keycloak cd backend && ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' -run-back-for-cypress: run-stubbed-apis - docker compose up -d --quiet-pull --wait db keycloak - cd backend && MONITORFISH_OIDC_ENABLED=false ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' +.PHONY: run-front ##LOCAL ▶️ Run frontend for development +run-front: + cd ./frontend && npm run dev +.PHONY: run-back-with-monitorenv ##LOCAL ▶️ Run backend API when running MonitorEnv app (in another terminal) run-back-with-monitorenv: run-monitorenv docker compose up -d --quiet-pull --wait db cd backend && MONITORENV_URL=http://localhost:9880 ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' -run-front: - cd ./frontend && npm run dev - +.PHONY: run-monitorenv ##LOCAL ▶️ Run MonitorEnv app containers run-monitorenv: docker-env docker compose \ --project-directory ./infra/docker \ @@ -51,6 +84,13 @@ run-monitorenv: docker-env -f ./infra/docker/docker-compose.monitorenv.dev.yml \ up -d monitorenv_app +.PHONY: lint-back ##LOCAL 🪮 ✨ Lint and format backend code +lint-back: + cd ./backend && ./gradlew ktlintFormat | grep -v \ + -e "Exceeded max line length" \ + -e "Package name must not contain underscore" \ + -e "Wildcard import" + run-stubbed-apis: docker compose stop geoserver-monitorenv-stubs docker compose up -d --quiet-pull --wait geoserver-monitorenv-stubs @@ -71,6 +111,72 @@ dev-restore-db: @export CONFIG_FILE_PATH=$$(pwd)/infra/dev/database/pg_backup.config; \ ./infra/remote/backup/pg_restore.sh -t "$(TAG)" +################################################################################ +# Testing + +.PHONY: test ##TEST ✅ Run all tests +test: test-back + cd frontend && CI=true npm run test:unit -- --coverage + +.PHONY: run-back-for-cypress ##TEST ▶️ Run backend API when using Cypress 📝 +run-back-for-cypress: run-stubbed-apis + docker compose up -d --quiet-pull --wait db keycloak + cd backend && MONITORFISH_OIDC_ENABLED=false ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' + +.PHONY: run-front-for-cypress ##TEST ▶️ Run frontend when using Cypress 📝 +run-front-for-cypress: + cd ./frontend && npm run dev-cypress + +.PHONY: run-cypress ##TEST ▶️ Run Cypress 📝 +run-cypress: + cd ./frontend && npm run test:e2e:open + +test-back: check-clean-archi + @if [ -z "$(class)" ]; then \ + echo "Running all Backend tests..."; \ + cd backend && ./gradlew clean test; \ + else \ + echo "Running single Backend test class $(class)..."; \ + cd backend && ./gradlew test --console plain --no-continue --parallel --tests "$(class)"; \ + fi + +.PHONY: test-back-watch ##TEST ✅ Watch backend tests +test-back-watch: + ./backend/scripts/test-watch.sh + +.PHONY: run-back-for-puppeteer ##TEST ▶️ Run backend API when using Puppeteer 📝 +run-back-for-puppeteer: docker-env run-stubbed-apis + docker compose up -d --quiet-pull --wait db + docker compose -f ./infra/docker/docker-compose.puppeteer.yml up -d monitorenv-app + cd backend && MONITORENV_URL=http://localhost:9880 ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' + +.PHONY: run-front-for-puppeteer ##TEST ▶️ Run frontend when using Puppeteer 📝 +run-front-for-puppeteer: + cd ./frontend && npm run dev-puppeteer + +################################################################################ +# Remote (Integration / Production) + +# ---------------------------------------------------------- +# Remote: Run commands + +.PHONY: restart-remote-app ##RUN ▶️ Restart app +restart-remote-app: + cd infra/remote && docker compose pull && docker compose up -d --build app --force-recreate + +.PHONY: register-pipeline-flows-prod ##RUN ▶️ Register pipeline flows in PROD +register-pipeline-flows-prod: + docker pull docker.pkg.github.com/mtes-mct/monitorfish/monitorfish-pipeline:$(MONITORFISH_VERSION) && \ + infra/remote/data-pipeline/register-flows-prod.sh + +.PHONY: register-pipeline-flows-int ##RUN ▶️ Register pipeline flows in STAGING +register-pipeline-flows-int: + docker pull docker.pkg.github.com/mtes-mct/monitorfish/monitorfish-pipeline:$(MONITORFISH_VERSION) && \ + infra/remote/data-pipeline/register-flows-int.sh + +.PHONY: init-remote-sig ##RUN Initialize Geoserver layers +init-remote-sig: + ./infra/remote/postgis_insert_layers.sh && ./infra/init/geoserver_init_layers.sh ################################################################################ # Database upgrade @@ -113,38 +219,6 @@ add_timescaledb_to_shared_preload_libraries: bash -c "echo \"shared_preload_libraries = 'timescaledb'\" >> /var/lib/postgresql/data/postgresql.conf"; -################################################################################ -# Testing - -test: test-back - cd frontend && CI=true npm run test:unit -- --coverage - -test-back: check-clean-archi - @if [ -z "$(class)" ]; then \ - echo "Running all Backend tests..."; \ - cd backend && ./gradlew clean test; \ - else \ - echo "Running single Backend test class $(class)..."; \ - cd backend && ./gradlew test --console plain --no-continue --parallel --tests "$(class)"; \ - fi - -test-back-watch: - ./backend/scripts/test-watch.sh - -lint-back: - cd ./backend && ./gradlew ktlintFormat | grep -v \ - -e "Exceeded max line length" \ - -e "Package name must not contain underscore" \ - -e "Wildcard import" - -run-back-for-puppeteer: docker-env run-stubbed-apis - docker compose up -d --quiet-pull --wait db - docker compose -f ./infra/docker/docker-compose.puppeteer.yml up -d monitorenv-app - cd backend && MONITORENV_URL=http://localhost:9880 ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' - -run-front-for-puppeteer: - cd ./frontend && npm run dev-puppeteer - ################################################################################ # CI @@ -195,23 +269,6 @@ docker-push-pipeline: docker push docker.pkg.github.com/mtes-mct/monitorfish/monitorfish-pipeline:$(VERSION) -################################################################################ -# Remote (Integration / Production) - -# ---------------------------------------------------------- -# Remote: Run commands - -init-remote-sig: - ./infra/remote/postgis_insert_layers.sh && ./infra/init/geoserver_init_layers.sh -restart-remote-app: - cd infra/remote && docker compose pull && docker compose up -d --build app --force-recreate - -register-pipeline-flows-prod: - docker pull docker.pkg.github.com/mtes-mct/monitorfish/monitorfish-pipeline:$(MONITORFISH_VERSION) && \ - infra/remote/data-pipeline/register-flows-prod.sh -register-pipeline-flows-int: - docker pull docker.pkg.github.com/mtes-mct/monitorfish/monitorfish-pipeline:$(MONITORFISH_VERSION) && \ - infra/remote/data-pipeline/register-flows-int.sh # ---------------------------------------------------------- # Remote: Pipeline commands diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportings.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportings.kt index 2d58f4ac94..0c08b4c03c 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportings.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportings.kt @@ -12,6 +12,7 @@ import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository import fr.gouv.cnsp.monitorfish.domain.use_cases.control_units.GetAllControlUnits import org.slf4j.LoggerFactory import java.time.ZonedDateTime +import kotlin.time.measureTimedValue @UseCase class GetVesselReportings( @@ -29,41 +30,51 @@ class GetVesselReportings( vesselIdentifier: VesselIdentifier?, fromDate: ZonedDateTime, ): VesselReportings { - val controlUnits = getAllControlUnits.execute() - val reportings = - findReportings( + val (controlUnits, controlUnitsTimeTaken) = measureTimedValue { getAllControlUnits.execute() } + logger.info("TIME_RECORD - 'getAllControlUnits' took $controlUnitsTimeTaken") + + val (reportings, reportingsTimeTaken) = + measureTimedValue { findReportings( vesselId, vesselIdentifier, internalReferenceNumber, fromDate, ircs, externalReferenceNumber, - ) + ) } + logger.info("TIME_RECORD - 'findReportings' took $reportingsTimeTaken") - val current = - getReportingsAndOccurrences(reportings.filter { !it.isArchived }) - .sortedWith(compareByDescending { it.reporting.validationDate ?: it.reporting.creationDate }) - .map { reportingAndOccurrences -> - enrichWithInfractionAndControlUnit(reportingAndOccurrences, controlUnits) - } - - val yearsRange = fromDate.year..ZonedDateTime.now().year - val archivedYearsToReportings = - yearsRange.associateWith { year -> - val reportingsOfYear = - reportings - .filter { it.isArchived } - .filter { filterByYear(it, year) } - - getReportingsAndOccurrences(reportingsOfYear) + val (current, currentTimeTaken) = + measureTimedValue { + getReportingsAndOccurrences(reportings.filter { !it.isArchived }) .sortedWith(compareByDescending { it.reporting.validationDate ?: it.reporting.creationDate }) .map { reportingAndOccurrences -> enrichWithInfractionAndControlUnit(reportingAndOccurrences, controlUnits) } } + logger.info("TIME_RECORD - 'current' took $currentTimeTaken") + + val yearsRange = fromDate.year..ZonedDateTime.now().year + val (archivedYearsToReportings, archivedYearsToReportingsTimeTaken) = + measureTimedValue { + yearsRange.associateWith { year -> + val reportingsOfYear = + reportings + .filter { it.isArchived } + .filter { filterByYear(it, year) } + + getReportingsAndOccurrences(reportingsOfYear) + .sortedWith(compareByDescending { it.reporting.validationDate ?: it.reporting.creationDate }) + .map { reportingAndOccurrences -> + enrichWithInfractionAndControlUnit(reportingAndOccurrences, controlUnits) + } + } + } + logger.info("TIME_RECORD - 'archivedYearsToReportings' took $archivedYearsToReportingsTimeTaken") - val infractionSuspicionsSummary = getInfractionSuspicionsSummary(reportings.filter { it.isArchived }) + val (infractionSuspicionsSummary, infractionSuspicionsSummaryTimeTaken) = measureTimedValue { getInfractionSuspicionsSummary(reportings.filter { it.isArchived }) } + logger.info("TIME_RECORD - 'infractionSuspicionsSummary' took $infractionSuspicionsSummaryTimeTaken") val numberOfInfractionSuspicions = infractionSuspicionsSummary.sumOf { it.numberOfOccurrences } val numberOfObservation = reportings @@ -93,7 +104,7 @@ class GetVesselReportings( .groupBy { (it.value as AlertType).type } .map { (type, reportings) -> ReportingTitleAndNumberOfOccurrences( - title = type.alertName, + title = "${type.alertName} (NATINF ${reportings[0].value.natinfCode})", numberOfOccurrences = reportings.size, ) } @@ -113,7 +124,7 @@ class GetVesselReportings( } return@map ReportingTitleAndNumberOfOccurrences( - title = infraction?.infraction ?: "NATINF $natinfCode", + title = infraction?.infraction?.let {"$it (NATINF $natinfCode)"} ?: "NATINF $natinfCode", numberOfOccurrences = reportings.size, ) } diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportingsUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportingsUTests.kt index 272d3e2844..04d48ffc27 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportingsUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetVesselReportingsUTests.kt @@ -413,14 +413,14 @@ class GetVesselReportingsUTests { val infractionSuspicionsSummary = result.summary.infractionSuspicionsSummary assertThat(result.summary.infractionSuspicionsSummary).hasSize(4) assertThat(infractionSuspicionsSummary[0].numberOfOccurrences).isEqualTo(2) - assertThat(infractionSuspicionsSummary[0].title).isEqualTo("12 milles - Pêche sans droits historiques") + assertThat(infractionSuspicionsSummary[0].title).isEqualTo("12 milles - Pêche sans droits historiques (NATINF 2610)") assertThat(infractionSuspicionsSummary[1].numberOfOccurrences).isEqualTo(1) - assertThat(infractionSuspicionsSummary[1].title).isEqualTo("Non-emission de message \"FAR\" en 48h") + assertThat(infractionSuspicionsSummary[1].title).isEqualTo("Non-emission de message \"FAR\" en 48h (NATINF 27689)") assertThat(infractionSuspicionsSummary[2].numberOfOccurrences).isEqualTo(1) assertThat( infractionSuspicionsSummary[2].title, ).isEqualTo( - "Peche maritime non autorisee dans les eaux maritimes ou salees francaises par un navire de pays tiers a l'union europeenne", + "Peche maritime non autorisee dans les eaux maritimes ou salees francaises par un navire de pays tiers a l'union europeenne (NATINF 7059)", ) assertThat(infractionSuspicionsSummary[3].numberOfOccurrences).isEqualTo(1) assertThat(infractionSuspicionsSummary[3].title).isEqualTo("NATINF 123456") diff --git a/frontend/cypress/e2e/vessel_sidebar/reporting.spec.ts b/frontend/cypress/e2e/vessel_sidebar/reporting.spec.ts index a1700e92c7..13194d30a4 100644 --- a/frontend/cypress/e2e/vessel_sidebar/reporting.spec.ts +++ b/frontend/cypress/e2e/vessel_sidebar/reporting.spec.ts @@ -141,17 +141,14 @@ context('Vessel sidebar reporting tab', () => { // Then // Summary cy.get('[data-cy="vessel-reporting-summary"]').contains('Résumé des derniers signalements (6 dernières années)') - cy.get('[data-cy="vessel-reporting-summary"]').contains('Signalements "3 milles - Chaluts"') + cy.get('[data-cy="vessel-reporting-summary"]').contains('Signalements "3 milles - Chaluts (NATINF 7059)"') // Dates occurrences of an alert cy.get('*[data-cy="vessel-sidebar-reporting-tab-archive-year"]').eq(0).click() - cy.get('[data-cy="reporting-card"]').should('not.contain', '2è alerte le') cy.get('[data-cy="reporting-card"]').should('not.contain', '1ère alerte le') cy.clickLink('Voir les dates des autres alertes') - cy.get('[data-cy="reporting-card"]').should('contain', '2è alerte le') cy.get('[data-cy="reporting-card"]').should('contain', '1ère alerte le') cy.clickLink('Masquer les dates des autres alertes') - cy.get('[data-cy="reporting-card"]').should('not.contain', '2è alerte le') cy.get('[data-cy="reporting-card"]').should('not.contain', '1ère alerte le') cy.get('*[data-cy^="vessel-search-selected-vessel-close-title"]', { timeout: 10000 }).click() diff --git a/frontend/src/features/Reporting/components/VesselReportings/ReportingCard.tsx b/frontend/src/features/Reporting/components/VesselReportings/ReportingCard.tsx index 775492a1bb..7862beba38 100644 --- a/frontend/src/features/Reporting/components/VesselReportings/ReportingCard.tsx +++ b/frontend/src/features/Reporting/components/VesselReportings/ReportingCard.tsx @@ -36,7 +36,7 @@ export function ReportingCard({ reporting.type === ReportingType.ALERT ? reporting.validationDate : reporting.creationDate, true ) - const otherOccurrencesDates = [reporting].concat(otherOccurrencesOfSameAlert).map((alert, index, array) => { + const otherOccurrencesDates = otherOccurrencesOfSameAlert.map((alert, index, array) => { const dateTime = getDateTime(alert.validationDate, true) return `${getFrenchOrdinal(array.length - (index + 1))} alerte le ${dateTime}` @@ -107,14 +107,16 @@ export function ReportingCard({ )} {isArchived ? ( - {otherOccurrencesOfSameAlert.length + 1} + otherOccurrencesOfSameAlert.length > 0 && ( + {otherOccurrencesOfSameAlert.length + 1} + ) ) : ( 0}> {otherOccurrencesOfSameAlert.length > 0 && ( {otherOccurrencesOfSameAlert.length + 1} )} {reporting.type !== ReportingType.ALERT && ( - )} - dispatch(archiveReporting(reporting.id, reporting.type))} title="Archiver" /> - ` - margin-top: ${p => (p.isArchived ? 8 : 0)}px; + margin-top: 8px; margin-right: ${p => (p.isArchived ? '8px' : 'unset')}; margin-left: ${p => (p.isArchived ? 'auto' : 'unset')}; background: ${p => p.theme.color.maximumRed} 0% 0% no-repeat padding-box;