diff --git a/assets/@types/app.ts b/assets/@types/app.ts index 38f8b1b9..340405fe 100644 --- a/assets/@types/app.ts +++ b/assets/@types/app.ts @@ -259,6 +259,13 @@ export type StoredDataReport = { processing_executions: StoredDataReportProcessingExecution[]; }; +export type DeliveryReport = { + input_upload: Upload & { + file_tree: UploadTree; + checks: CheckDetailed[]; + }; +}; + export type StoredDataReportProcessingExecution = ProcessingExecution & { output: ProcessingExecutionOutputStoredDataDto; logs?: CheckOrProcessingExecutionLogs; diff --git a/assets/entrepot/api/upload.ts b/assets/entrepot/api/upload.ts index 43cf55ab..625c1bc4 100644 --- a/assets/entrepot/api/upload.ts +++ b/assets/entrepot/api/upload.ts @@ -1,5 +1,6 @@ import SymfonyRouting from "../../modules/Routing"; import { jsonFetch } from "../../modules/jsonFetch"; +import { DeliveryReport } from "../../@types/app"; import { Upload, UploadTree, UploadTypeEnum } from "../../@types/app"; const getList = (datastoreId: string, type?: UploadTypeEnum, otherOptions: RequestInit = {}) => { @@ -61,6 +62,13 @@ const remove = (datastoreId: string, uploadId: string) => { return jsonFetch(url, { method: "DELETE" }); }; +const getDeliveryReport = (datastoreId: string, uploadId: string, otherOptions: RequestInit = {}) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_upload_get_delivery_report", { datastoreId, uploadId }); + return jsonFetch(url, { + ...otherOptions, + }); +}; + const upload = { getList, add, @@ -69,6 +77,7 @@ const upload = { pingIntegrationProgress, getFileTree, remove, + getDeliveryReport, }; export default upload; diff --git a/assets/entrepot/pages/datasheet/DatasheetNew/DatasheetUploadIntegration/DatasheetUploadIntegrationDialog.tsx b/assets/entrepot/pages/datasheet/DatasheetNew/DatasheetUploadIntegration/DatasheetUploadIntegrationDialog.tsx index 7c16fd68..7e220246 100644 --- a/assets/entrepot/pages/datasheet/DatasheetNew/DatasheetUploadIntegration/DatasheetUploadIntegrationDialog.tsx +++ b/assets/entrepot/pages/datasheet/DatasheetNew/DatasheetUploadIntegration/DatasheetUploadIntegrationDialog.tsx @@ -205,6 +205,24 @@ const DatasheetUploadIntegrationDialog: FC )} + {integrationStatus === "at_least_one_failure" && uploadQuery.data?.tags.datasheet_name !== undefined && ( +
+ +
+ )} + {integrationStatus === "at_least_one_failure" && uploadQuery.data?.tags?.vectordb_id !== undefined && (
= ({ datastoreId, datasheet }) => { {unfinishedUploads && unfinishedUploads.length > 0 && (
- +
)} diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/UnfinishedUploadList.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/UnfinishedUploadList.tsx index 5ed363fe..8a0387b5 100644 --- a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/UnfinishedUploadList.tsx +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/UnfinishedUploadList.tsx @@ -2,15 +2,47 @@ import { fr } from "@codegouvfr/react-dsfr"; import Button from "@codegouvfr/react-dsfr/Button"; import { FC, memo } from "react"; import { symToStr } from "tsafe/symToStr"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import RQKeys from "../../../../../modules/entrepot/RQKeys"; +import api from "../../../../api"; +import { type DatasheetDetailed } from "../../../../../@types/app"; import { routes } from "../../../../../router/router"; import { Upload } from "../../../../../@types/app"; +import ReportStatusBadge from "../../../stored_data/StoredDataDetails/ReportTab/ReportStatusBadge"; +import { deleteDeliveryConfirmModal } from "../DatasheetView"; +import Wait from "../../../../../components/Utils/Wait"; +import LoadingIcon from "../../../../../components/Utils/LoadingIcon"; +import { useTranslation } from "../../../../../i18n/i18n"; type UnfinishedUploadListProps = { datastoreId: string; uploadList?: Upload[]; + nbPublications: number; + datasheet: DatasheetDetailed; }; -const UnfinishedUploadList: FC = ({ datastoreId, uploadList }) => { + +const UnfinishedUploadList: FC = ({ datastoreId, uploadList, nbPublications, datasheet }) => { + const { t } = useTranslation("DatastoreManageStorage"); + + const queryClient = useQueryClient(); + + const isLastUpload = (uploadList: Upload[]): boolean => { + return uploadList.length === 1 && nbPublications === 0; + }; + + const deleteUnfinishedUpload = useMutation({ + mutationFn: (uploadId: string) => api.upload.remove(datastoreId, uploadId), + onSuccess(uploadId) { + queryClient.setQueryData(RQKeys.datastore_datasheet(datastoreId, datasheet.name), (datasheet: { upload_list: Upload[] }) => { + return { + ...datasheet, + upload_list: datasheet.upload_list.filter((upload) => upload._id !== uploadId), + }; + }); + }, + }); + return ( <>
@@ -20,21 +52,92 @@ const UnfinishedUploadList: FC = ({ datastoreId, uplo
- {uploadList?.map((upload) => ( -
-
-
{upload.name}
-
+ {uploadList?.map((upload) => { + const integrationProgress = JSON.parse(upload.tags.integration_progress || "{}"); + const steps = Object.entries(integrationProgress); + const failureCase = steps.some(([, status]) => status === "failed"); + + return ( +
+
+
+ {upload.name} + {failureCase ? ( + + ) : ( + + )} +
+
-
-
- +
+
+ {failureCase ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+
+ ); + })} + {deleteUnfinishedUpload.isPending && ( + +
+
+ +
{t("storage.upload.deletion.in_progress")}
-
- ))} + + )} ); }; diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx index 9272596b..c9c49522 100644 --- a/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx @@ -31,6 +31,11 @@ const deleteDataConfirmModal = createModal({ isOpenedByDefault: false, }); +export const deleteDeliveryConfirmModal = createModal({ + id: "delete-delivery-confirm-modal", + isOpenedByDefault: false, +}); + export enum DatasheetViewActiveTabEnum { Metadata = "metadata", Dataset = "dataset", @@ -260,6 +265,26 @@ const DatasheetView: FC = ({ datastoreId, datasheetName }) = , document.body )} + {createPortal( + datasheetDeleteMutation.mutate(), + priority: "primary", + }, + ]} + > + En supprimant cette livraison, la fiche de données {datasheetName} sera supprimée. + , + document.body + )} ); diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/DeliveryDetails.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/DeliveryDetails.tsx new file mode 100644 index 00000000..f44356bf --- /dev/null +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/DeliveryDetails.tsx @@ -0,0 +1,95 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import Tabs from "@codegouvfr/react-dsfr/Tabs"; +import { useQuery } from "@tanstack/react-query"; +import { FC, useMemo } from "react"; + +import { DeliveryReport } from "../../../../@types/app"; +import DatastoreLayout from "../../../../components/Layout/DatastoreLayout"; +import LoadingIcon from "../../../../components/Utils/LoadingIcon"; +import RQKeys from "../../../../modules/entrepot/RQKeys"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import { routes } from "../../../../router/router"; +import api from "../../../api"; +import DeliveryPreviewTab from "./PreviewTab/DeliveryPreviewTab"; +import ReportTab from "./ReportTab/ReportTab"; + +type DeliveryDetailsProps = { + datastoreId: string; + uploadDataId: string; +}; + +const DeliveryDetails: FC = ({ datastoreId, uploadDataId }) => { + const datastoreQuery = useQuery({ + queryKey: RQKeys.datastore(datastoreId), + queryFn: ({ signal }) => api.datastore.get(datastoreId, { signal }), + staleTime: 3600000, + }); + + const reportQuery = useQuery({ + queryKey: RQKeys.datastore_delivery_report(datastoreId, uploadDataId), + queryFn: ({ signal }) => api.upload.getDeliveryReport(datastoreId, uploadDataId, { signal }), + staleTime: 3600000, + }); + + const datasheetName = useMemo(() => reportQuery?.data?.input_upload?.tags?.datasheet_name, [reportQuery?.data?.input_upload?.tags?.datasheet_name]); + + return ( + +
+ {datasheetName ? ( +
+ {reportQuery?.data?.input_upload?.name && ( +
+

{reportQuery?.data?.input_upload?.name}

+
+ )} + +
+ {reportQuery.isError && } +
+ + {reportQuery.data && ( +
+
+ , + }, + { + label: "Rapport de génération", + content: , + }, + ]} + /> +
+
+ )} +
+ ); +}; + +export default DeliveryDetails; diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/PreviewTab/DeliveryPreviewTab.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/PreviewTab/DeliveryPreviewTab.tsx new file mode 100644 index 00000000..473e43e1 --- /dev/null +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/PreviewTab/DeliveryPreviewTab.tsx @@ -0,0 +1,42 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import { FC } from "react"; + +import { DeliveryReport } from "../../../../../@types/app"; +import { niceBytes } from "../../../../../utils"; +import ReportStatusBadge from "../ReportTab/ReportStatusBadge"; + +type DeliveryPreviewTabProps = { + reportData: DeliveryReport; +}; + +const DeliveryPreviewTab: FC = ({ reportData }) => { + const uploadData = reportData.input_upload; + + return ( + +
    +
  • + Nom : {uploadData.name} +
  • +
  • + Identifiant technique : {uploadData._id} +
  • +
  • + Projection : {uploadData.srs} +
  • +
  • + Statut : {} +
  • +
  • + Taille : {uploadData.size && niceBytes(uploadData.size.toString())} +
  • +
  • + Type : {uploadData.type} +
  • +
+
+ ); +}; + +export default DeliveryPreviewTab; diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx index 11f2f809..67c06129 100644 --- a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx @@ -3,7 +3,7 @@ import Accordion from "@codegouvfr/react-dsfr/Accordion"; import { UseQueryResult } from "@tanstack/react-query"; import { FC } from "react"; -import { StoredDataReport as ReportTab } from "../../../../../@types/app"; +import { DeliveryReport, StoredDataReport as ReportTab } from "../../../../../@types/app"; import { CheckingExecutionDetailResponseDtoStatusEnum, ProcessingExecutionDetailResponseDtoStatusEnum } from "../../../../../@types/entrepot"; import { CartesApiException } from "../../../../../modules/jsonFetch"; import { niceBytes } from "../../../../../utils"; @@ -14,7 +14,7 @@ import UploadFileTree from "./UploadFileTree"; type ReportTabProps = { datastoreName?: string; - reportQuery: UseQueryResult; + reportQuery: UseQueryResult; }; const ReportTab: FC = ({ datastoreName, reportQuery }) => { @@ -65,24 +65,25 @@ const ReportTab: FC = ({ datastoreName, reportQuery }) => { ))} - {reportQuery?.data.processing_executions.map((procExec) => ( - -

{`${step++}. Traitement : ${procExec.processing.name}`}

- - - } - defaultExpanded={[ - ProcessingExecutionDetailResponseDtoStatusEnum.FAILURE, - ProcessingExecutionDetailResponseDtoStatusEnum.ABORTED, - ].includes(procExec.status)} - > - -
- ))} + {"processing_executions" in reportQuery.data && + reportQuery?.data.processing_executions.map((procExec) => ( + +

{`${step++}. Traitement : ${procExec.processing.name}`}

+ + + } + defaultExpanded={[ + ProcessingExecutionDetailResponseDtoStatusEnum.FAILURE, + ProcessingExecutionDetailResponseDtoStatusEnum.ABORTED, + ].includes(procExec.status)} + > + +
+ ))} ) ); diff --git a/assets/i18n/Breadcrumb.tsx b/assets/i18n/Breadcrumb.tsx index e9f133a4..0c41b2a0 100644 --- a/assets/i18n/Breadcrumb.tsx +++ b/assets/i18n/Breadcrumb.tsx @@ -32,6 +32,7 @@ export const { i18n } = declareComponentKeys< | "upload" | "datastore_datasheet_upload_integration" | "datastore_stored_data_details" + | "datastore_delivery_details" | "datastore_wfs_service_new" | "datastore_wfs_service_edit" | "datastore_wms_vector_service_new" @@ -74,6 +75,7 @@ export const BreadcrumbFrTranslations: Translations<"fr">["Breadcrumb"] = { upload: "Téléversement", datastore_datasheet_upload_integration: "Intégration de données", datastore_stored_data_details: "Détails d'une donnée stockée", + datastore_delivery_details: "Détails d'une livraison", datastore_wfs_service_new: "Création d'un service WFS", datastore_wfs_service_edit: "Modification d'un service WFS", datastore_wms_vector_service_new: "Création d'un service WMS", @@ -116,6 +118,7 @@ export const BreadcrumbEnTranslations: Translations<"en">["Breadcrumb"] = { upload: "Upload", datastore_datasheet_upload_integration: "Data integration", datastore_stored_data_details: "Details of stored data", + datastore_delivery_details: "Details of delivery", datastore_wfs_service_new: "Create a WFS service", datastore_wfs_service_edit: "Modify WFS service", datastore_wms_vector_service_new: "Create a WMS service", diff --git a/assets/modules/entrepot/RQKeys.ts b/assets/modules/entrepot/RQKeys.ts index 8159a76d..d5ab9221 100644 --- a/assets/modules/entrepot/RQKeys.ts +++ b/assets/modules/entrepot/RQKeys.ts @@ -20,6 +20,7 @@ const RQKeys = { datastore_stored_data: (datastoreId: string, storedDataId: string): string[] => ["datastore", datastoreId, "stored_data", storedDataId], datastore_stored_data_uses: (datastoreId: string, storedDataId: string): string[] => ["datastore", datastoreId, "stored_data", storedDataId, "uses"], datastore_stored_data_report: (datastoreId: string, storedDataId: string): string[] => ["datastore", datastoreId, "stored_data", storedDataId, "report"], + datastore_delivery_report: (datastoreId: string, uploadDataId: string): string[] => ["datastore", datastoreId, "upload_data", uploadDataId, "report"], datastore_datasheet_list: (datastoreId: string): string[] => ["datastore", datastoreId, "datasheet"], datastore_datasheet: (datastoreId: string, dataName: string): string[] => ["datastore", datastoreId, "datasheet", dataName], diff --git a/assets/modules/entrepot/breadcrumbs.ts b/assets/modules/entrepot/breadcrumbs.ts index acd87397..4ea6e79d 100644 --- a/assets/modules/entrepot/breadcrumbs.ts +++ b/assets/modules/entrepot/breadcrumbs.ts @@ -161,6 +161,21 @@ const getBreadcrumb = (route: Route, datastore?: Datastore): Brea }); } return { ...defaultProps, currentPageLabel: t("datastore_stored_data_details") }; + case "datastore_delivery_details": + defaultProps.segments = [ + ...defaultProps.segments, + ...[ + { label: t("dashboard_pro"), linkProps: routes.dashboard_pro().link }, + { label: datastore?.name, linkProps: routes.datasheet_list({ datastoreId: route.params.datastoreId }).link }, + ], + ]; + if ("datasheetName" in route.params && route.params.datasheetName) { + defaultProps.segments.push({ + label: route.params.datasheetName, + linkProps: routes.datastore_datasheet_view({ datastoreId: route.params.datastoreId, datasheetName: route.params.datasheetName }).link, + }); + } + return { ...defaultProps, currentPageLabel: t("datastore_delivery_details") }; case "datastore_wfs_service_new": case "datastore_wfs_service_edit": diff --git a/assets/router/RouterRenderer.tsx b/assets/router/RouterRenderer.tsx index 5fd209aa..c7f70fbd 100644 --- a/assets/router/RouterRenderer.tsx +++ b/assets/router/RouterRenderer.tsx @@ -43,6 +43,7 @@ const DatasheetUploadIntegrationPage = lazy(() => import("../entrepot/pages/data const DatasheetView = lazy(() => import("../entrepot/pages/datasheet/DatasheetView/DatasheetView")); const StoredDataDetails = lazy(() => import("../entrepot/pages/stored_data/StoredDataDetails/StoredDataDetails")); +const DeliveryReport = lazy(() => import("../entrepot/pages/stored_data/StoredDataDetails/DeliveryDetails")); const DatastoreCreationForm = lazy(() => import("../entrepot/pages/datastore/DatastoreCreationForm")); const Confirm = lazy(() => import("../entrepot/pages/datastore/Confirmation")); @@ -157,6 +158,8 @@ const RouterRenderer: FC = () => { return ; case "datastore_stored_data_details": return ; + case "datastore_delivery_details": + return ; case "datastore_wfs_service_new": return ; case "datastore_wfs_service_edit": diff --git a/assets/router/router.ts b/assets/router/router.ts index 57280d63..1983ec44 100644 --- a/assets/router/router.ts +++ b/assets/router/router.ts @@ -141,6 +141,14 @@ const routeDefs = { }, (p) => `${appRoot}/entrepot/${p.datastoreId}/donnees/${p.storedDataId}/details` ), + datastore_delivery_details: defineRoute( + { + datastoreId: param.path.string, + uploadDataId: param.path.string, + datasheetName: param.query.optional.string, + }, + (p) => `${appRoot}/entrepot/${p.datastoreId}/livraisons/${p.uploadDataId}/rapport` + ), // Creer et publier un service WFS datastore_wfs_service_new: defineRoute( diff --git a/src/Controller/Entrepot/UploadController.php b/src/Controller/Entrepot/UploadController.php index e04e0eaa..2a08120e 100644 --- a/src/Controller/Entrepot/UploadController.php +++ b/src/Controller/Entrepot/UploadController.php @@ -308,4 +308,33 @@ public function delete(string $datastoreId, string $uploadId): JsonResponse throw new CartesApiException($ex->getMessage(), JsonResponse::HTTP_INTERNAL_SERVER_ERROR); } } + + #[Route('/{uploadId}/delivery-report', name: 'get_delivery_report', methods: ['GET'])] + public function getDeliveryReport(string $datastoreId, string $uploadId): JsonResponse + { + try { + // Récupération des détails de l'upload ayant échoué + $inputUpload = $this->uploadApiService->get($datastoreId, $uploadId); + $inputUpload['file_tree'] = $this->uploadApiService->getFileTree($datastoreId, $inputUpload['_id']); + $inputUpload['checks'] = []; + $uploadChecks = $this->uploadApiService->getCheckExecutions($datastoreId, $inputUpload['_id']); + + foreach ($uploadChecks as &$checkType) { + foreach ($checkType as &$checkExecution) { + $checkExecution = array_merge($checkExecution, $this->uploadApiService->getCheckExecution($datastoreId, $checkExecution['_id'])); + try { + $checkExecution['logs'] = $this->uploadApiService->getCheckExecutionLogs($datastoreId, $checkExecution['_id']); + } catch (ApiException $ex) { + } + $inputUpload['checks'][] = $checkExecution; + } + } + + return $this->json([ + 'input_upload' => $inputUpload, + ]); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails()); + } + } }