From 5c7e535da1a8cae3a74a50e9b4c4ed8d52cffd21 Mon Sep 17 00:00:00 2001 From: dan-qc <62541139+dan-qc@users.noreply.github.com> Date: Thu, 26 Nov 2020 17:37:52 +0200 Subject: [PATCH 1/2] feat(map): download collection metrics [EP-3002] * wip * feat(map): download collection metrics [EP-3002] --- packages/earth-map/package.json | 2 + .../collection-details/CollectionDetails.tsx | 92 ++++++-- .../CollectionDownloadMetrics.tsx | 198 ++++++++++++++++++ .../collection-downloadmetrics/index.ts | 20 ++ .../earth-map/src/locales/en/translation.json | 10 + .../earth-map/src/locales/es/translation.json | 10 + .../earth-map/src/locales/fr/translation.json | 10 + .../earth-map/src/services/MetricService.tsx | 22 +- yarn.lock | 5 + 9 files changed, 345 insertions(+), 24 deletions(-) create mode 100644 packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx create mode 100644 packages/earth-map/src/components/collection/collection-downloadmetrics/index.ts diff --git a/packages/earth-map/package.json b/packages/earth-map/package.json index d3d979bd..56c15868 100644 --- a/packages/earth-map/package.json +++ b/packages/earth-map/package.json @@ -23,8 +23,10 @@ "@types/react-redux": "^7.1.7", "chroma-js": "^2.1.0", "core-js": "^3.6.5", + "file-saver": "^2.0.5", "fuse.js": "^3.4.4", "i18next": "^19.8.3", + "jszip": "^3.5.0", "lodash": "^4.17.15", "orbit-controls-es6": "^2.0.0", "prettier": "^1.19.1", diff --git a/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx b/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx index b9d38a42..04446e24 100644 --- a/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx +++ b/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx @@ -35,6 +35,7 @@ import { import CollectionDelete from '../collection-delete'; import { CollectionEditPlaces } from '../collection-editplaces'; import { CollectionRename } from '../collection-rename'; +import { CollectionDownloadMetrics } from '../collection-downloadmetrics'; import './styles.scss'; interface IProps { @@ -54,6 +55,9 @@ const CollectionDetails = (props: IProps) => { const [isAddingPlaces, setIsAddingPlaces] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + const [isOnDownloadMetrics, setIsOnDownloadMetrics] = useState(false); + const [isDownloadingMetrics, setIsDownloadingMetrics] = useState(false); + const [downloadError, setDownloadError] = useState(''); const canEdit = privateGroups.includes(data.organization); @@ -94,30 +98,66 @@ const CollectionDetails = (props: IProps) => { {hasLocations ? ( - - {canEdit && ( + <> + + {canEdit && ( + + )} +

+ {t('Collection places')} ({locations.length}) +

+

+ {locations + .filter((x) => !!x) + .map((location) => ( + + ))} +

+
+ +

+ {t('Download metrics')} +   + +

+

+ {isDownloadingMetrics ? ( + <>{t('Your selected metric files should be ready soon')}. + ) : ( + <> + {t( + 'Individual metrics related to each of the places in your collection can be viewed once downloaded' + )} + .{t('Select single or multiple metric data files for download')}. + + )} +

- )} -

- {t('Collection places')} ({locations.length}) -

-

- {locations - .filter((x) => !!x) - .map((location) => ( - - ))} -

-
+ {downloadError &&

{downloadError}

} +
+ ) : (

@@ -158,6 +198,16 @@ const CollectionDetails = (props: IProps) => { {isDeleting && ( )} + + {isOnDownloadMetrics && ( + setIsOnDownloadMetrics(false)} + onDownloadStart={() => [setIsDownloadingMetrics(true), setIsOnDownloadMetrics(false)]} + onDownloadEnd={() => setIsDownloadingMetrics(false)} + onDownloadError={(err) => setDownloadError(err)} + /> + )} ); diff --git a/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx b/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx new file mode 100644 index 00000000..0c7fc0b8 --- /dev/null +++ b/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx @@ -0,0 +1,198 @@ +/* + * Copyright 2018-2020 National Geographic Society + * + * Use of this software does not constitute endorsement by National Geographic + * Society (NGS). The NGS name and NGS logo may not be used for any purpose without + * written permission from NGS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import { ICollection } from 'modules/collections/model'; +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Controller, useForm } from 'react-hook-form'; + +import { TitleHero, Card } from '@marapp/earth-shared'; +import MetricService from 'services/MetricService'; +import { ReactSelect, serializeFilters } from '@marapp/earth-shared'; + +import JSZip from 'jszip'; +import FileSaver from 'file-saver'; +import json2csv from 'json2csv'; +import { groupBy, omit } from 'lodash'; +import flatten from 'flat'; + +interface IProps { + collection: ICollection; + onCancel: () => void; + onDownloadStart: () => void; + onDownloadEnd: () => void; + onDownloadError: (err: string) => void; +} + +export function CollectionDownloadMetrics(props: IProps) { + const { collection, onCancel, onDownloadStart, onDownloadEnd, onDownloadError } = props; + const { t } = useTranslation(); + const { name, organization, slug: collectionSlug } = collection; + const [metricSlugs, setMetricSlugs] = useState([]); + const [isLoadingMetricSlugs, setIsLoadingMetricSlugs] = useState(false); + const { register, handleSubmit, formState, control, watch } = useForm({ + mode: 'onChange', + }); + const { dirty, isValid, isSubmitting } = formState; + const metricsWatcher = watch('metrics'); + + useEffect(() => { + (async () => { + setIsLoadingMetricSlugs(true); + + const data = await MetricService.fetchMetricSlugs({ group: organization }); + + setMetricSlugs(data.map((item) => ({ value: item.slug, label: item.slug }))); + + setIsLoadingMetricSlugs(false); + })(); + }, []); + + return ( +
+ + + + +
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ); + + async function onSubmit(values) { + onDownloadStart(); + + const { metrics, fileType } = values; + + try { + const data = await MetricService.downloadMetrics(collectionSlug, { + filter: serializeFilters({ + slug: metrics.map((metric) => metric.value), + }), + group: organization, + include: 'location', + select: 'location.name', + }); + + const zip = new JSZip(); + + const metricTypes = groupBy(data, 'slug'); + + Object.keys(metricTypes).forEach((metricType) => { + const normalizedData = metricTypes[metricType].map((item) => ({ + '#': item.location.name, + ...item.metric, + })); + const fileName = `${metricType}.${fileType}`; + + if (fileType === 'csv') { + const json2csvParser = new json2csv.Parser(); + + zip.file( + fileName, + json2csvParser.parse( + normalizedData.map((item) => ({ + '#': item['#'], + ...flatten(omit(item, '#')), + })) + ) + ); + } else { + zip.file(fileName, JSON.stringify(normalizedData)); + } + }); + + const zipName = `${collectionSlug}-metrics.zip`; + const zipContent = await zip.generateAsync({ type: 'blob' }); + + FileSaver.saveAs(zipContent, zipName); + } catch (e) { + onDownloadError('Something went wrong'); + console.log(e); + } + + onDownloadEnd(); + } +} diff --git a/packages/earth-map/src/components/collection/collection-downloadmetrics/index.ts b/packages/earth-map/src/components/collection/collection-downloadmetrics/index.ts new file mode 100644 index 00000000..7be0bbc6 --- /dev/null +++ b/packages/earth-map/src/components/collection/collection-downloadmetrics/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2018-2020 National Geographic Society + * + * Use of this software does not constitute endorsement by National Geographic + * Society (NGS). The NGS name and NGS logo may not be used for any purpose without + * written permission from NGS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +export { CollectionDownloadMetrics } from './CollectionDownloadMetrics'; diff --git a/packages/earth-map/src/locales/en/translation.json b/packages/earth-map/src/locales/en/translation.json index 93bdf8e5..7cb5e299 100644 --- a/packages/earth-map/src/locales/en/translation.json +++ b/packages/earth-map/src/locales/en/translation.json @@ -42,6 +42,16 @@ "Renaming collection": "Renaming collection", "Delete": "Delete", "Are you sure you want to permanently delete this collection": "Are you sure you want to permanently delete this collection", + "Download metrics": "Download metrics", + "Downloading metrics": "Downloading metrics", + "Individual metrics related to each of the places in your collection can be viewed once downloaded": "Individual metrics related to each of the places in your collection can be viewed once downloaded", + "Select single or multiple metric data files for download": "Select single or multiple metric data files for download", + "Download metric data files": "Download metric data files", + "Your selected metric files should be ready soon": "Your selected metric files should be ready soon", + "Select metrics for download": "Select metrics for download", + "Select metrics to download data files": "Select metrics to download data files", + "Select a file type for download": "Select a file type for download", + "Download": "Download", "Search results": "Search results", "Download metric as a": "Download metric as a", "Featured places": "Featured places", diff --git a/packages/earth-map/src/locales/es/translation.json b/packages/earth-map/src/locales/es/translation.json index 564858ff..8788fee3 100644 --- a/packages/earth-map/src/locales/es/translation.json +++ b/packages/earth-map/src/locales/es/translation.json @@ -42,6 +42,16 @@ "Renaming collection": "Renombrar colección", "Delete": "Eliminar", "Are you sure you want to permanently delete this collection": "¿Estás seguro de que deseas eliminar esta colección de forma permanente", + "Download metrics": "Descargar métricas", + "Downloading metrics": "Descarga de métricas", + "Individual metrics related to each of the places in your collection can be viewed once downloaded": "Las métricas individuales relacionadas con cada uno de los lugares de su colección se pueden ver una vez descargadas", + "Select single or multiple metric data files for download": "Seleccione archivos de datos de métricas únicos o múltiples para descargar", + "Download metric data files": "Descargar archivos de datos de métricas", + "Your selected metric files should be ready soon": "Sus archivos de métricas seleccionados deberían estar listos pronto", + "Select metrics for download": "Seleccionar métricas para descargar", + "Select metrics to download data files": "Seleccionar métricas para descargar archivos de datos", + "Select a file type for download": "Seleccione un tipo de archivo para descargar", + "Download": "Descargar", "Search results": "Resultados de la búsqueda", "Download metric as a": "Descargar métrica como", "Featured places": "Lugares destacados", diff --git a/packages/earth-map/src/locales/fr/translation.json b/packages/earth-map/src/locales/fr/translation.json index 878c3517..591468e4 100644 --- a/packages/earth-map/src/locales/fr/translation.json +++ b/packages/earth-map/src/locales/fr/translation.json @@ -42,6 +42,16 @@ "Renaming collection": "Renommer la collection", "Delete": "Supprimer", "Are you sure you want to permanently delete this collection": "Êtes-vous sûr de vouloir supprimer définitivement cette collection", + "Download metrics": "Télécharger les métriques", + "Downloading metrics": "Téléchargement de métriques", + "Individual metrics related to each of the places in your collection can be viewed once downloaded": "Les métriques individuelles liées à chacun des lieux de votre collection peuvent être consultées une fois téléchargées", + "Select single or multiple metric data files for download": "Sélectionnez un ou plusieurs fichiers de données métriques à télécharger", + "Download metric data files": "Télécharger les fichiers de données métriques", + "Your selected metric files should be ready soon": "Les fichiers de métriques sélectionnés devraient être prêts bientôt", + "Select metrics for download": "Sélectionnez les métriques à télécharger", + "Select metrics to download data files": "Sélectionnez les métriques pour télécharger les fichiers de données", + "Select a file type for download": "Sélectionnez un type de fichier à télécharger", + "Download": "Télécharger", "Search results": "Résultats de recherche", "Download metric as a": "Téléchargez la métrique sous forme de", "Featured places": "Lieux en vedette", diff --git a/packages/earth-map/src/services/MetricService.tsx b/packages/earth-map/src/services/MetricService.tsx index fde26d93..7041ad16 100644 --- a/packages/earth-map/src/services/MetricService.tsx +++ b/packages/earth-map/src/services/MetricService.tsx @@ -17,10 +17,26 @@ specific language governing permissions and limitations under the License. */ -import { BaseAPIService, RequestQuery } from './base/APIBase'; +import { BaseAPIService, RequestQuery, metaDeserializer } from './base/APIBase'; const fetchMetricById = async (id: string, query?: RequestQuery): Promise => { - return BaseAPIService.request(`/metrics/${id}`, { query }); + return BaseAPIService.request(`/metrics/${id}`, { query }, metaDeserializer); }; -export default { fetchMetricById }; +const fetchMetricSlugs = async (query?: RequestQuery): Promise => { + return BaseAPIService.request(`/metrics/slugs`, { query }); +}; + +const downloadMetrics = async ( + collectionId: string, + query?: RequestQuery, + page = 1 +): Promise => { + const { data, meta } = await fetchMetricById(collectionId, { ...query, 'page[number]': page }); + + return meta.pagination.page < meta.pagination.total + ? data.concat(await downloadMetrics(collectionId, query, page + 1)) + : data; +}; + +export default { fetchMetricById, fetchMetricSlugs, downloadMetrics }; diff --git a/yarn.lock b/yarn.lock index f9f41423..3a1e9652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10713,6 +10713,11 @@ file-loader@^1.1.11: loader-utils "^1.0.2" schema-utils "^0.4.5" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + file-type@^3.8.0: version "3.9.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" From 8c682d38f0663bbbae338a4b37d081e301a75612 Mon Sep 17 00:00:00 2001 From: Bogdan Bruma Date: Fri, 27 Nov 2020 13:55:34 +0200 Subject: [PATCH 2/2] code review --- .../collection-details/CollectionDetails.tsx | 5 ++-- .../CollectionDownloadMetrics.tsx | 25 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx b/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx index 04446e24..fd9669aa 100644 --- a/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx +++ b/packages/earth-map/src/components/collection/collection-details/CollectionDetails.tsx @@ -127,7 +127,7 @@ const CollectionDetails = (props: IProps) => {

{t('Download metrics')}   - +

{isDownloadingMetrics ? ( @@ -205,7 +205,8 @@ const CollectionDetails = (props: IProps) => { onCancel={() => setIsOnDownloadMetrics(false)} onDownloadStart={() => [setIsDownloadingMetrics(true), setIsOnDownloadMetrics(false)]} onDownloadEnd={() => setIsDownloadingMetrics(false)} - onDownloadError={(err) => setDownloadError(err)} + onDownloadError={setDownloadError} + onDownloadSuccess={() => setDownloadError('')} /> )} diff --git a/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx b/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx index 0c7fc0b8..fdeab4ad 100644 --- a/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx +++ b/packages/earth-map/src/components/collection/collection-downloadmetrics/CollectionDownloadMetrics.tsx @@ -22,14 +22,13 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Controller, useForm } from 'react-hook-form'; -import { TitleHero, Card } from '@marapp/earth-shared'; +import { TitleHero, Card, ReactSelect, serializeFilters } from '@marapp/earth-shared'; import MetricService from 'services/MetricService'; -import { ReactSelect, serializeFilters } from '@marapp/earth-shared'; import JSZip from 'jszip'; import FileSaver from 'file-saver'; import json2csv from 'json2csv'; -import { groupBy, omit } from 'lodash'; +import { groupBy } from 'lodash'; import flatten from 'flat'; interface IProps { @@ -38,10 +37,18 @@ interface IProps { onDownloadStart: () => void; onDownloadEnd: () => void; onDownloadError: (err: string) => void; + onDownloadSuccess: () => void; } export function CollectionDownloadMetrics(props: IProps) { - const { collection, onCancel, onDownloadStart, onDownloadEnd, onDownloadError } = props; + const { + collection, + onCancel, + onDownloadStart, + onDownloadEnd, + onDownloadError, + onDownloadSuccess, + } = props; const { t } = useTranslation(); const { name, organization, slug: collectionSlug } = collection; const [metricSlugs, setMetricSlugs] = useState([]); @@ -159,10 +166,11 @@ export function CollectionDownloadMetrics(props: IProps) { const zip = new JSZip(); const metricTypes = groupBy(data, 'slug'); + const locationNameField = '#'; Object.keys(metricTypes).forEach((metricType) => { const normalizedData = metricTypes[metricType].map((item) => ({ - '#': item.location.name, + [locationNameField]: item.location.name, ...item.metric, })); const fileName = `${metricType}.${fileType}`; @@ -173,9 +181,9 @@ export function CollectionDownloadMetrics(props: IProps) { zip.file( fileName, json2csvParser.parse( - normalizedData.map((item) => ({ - '#': item['#'], - ...flatten(omit(item, '#')), + normalizedData.map(({ [locationNameField]: locationName, ...item }) => ({ + [locationNameField]: locationName, + ...flatten(item), })) ) ); @@ -188,6 +196,7 @@ export function CollectionDownloadMetrics(props: IProps) { const zipContent = await zip.generateAsync({ type: 'blob' }); FileSaver.saveAs(zipContent, zipName); + onDownloadSuccess(); } catch (e) { onDownloadError('Something went wrong'); console.log(e);