Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(map): download collection metrics [EP-3002] #328

Merged
merged 2 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/earth-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);

Expand Down Expand Up @@ -94,30 +98,66 @@ const CollectionDetails = (props: IProps) => {
</Card>

{hasLocations ? (
<Card className="c-legend-item-group">
{canEdit && (
<>
<Card className="c-legend-item-group">
{canEdit && (
<button
className="marapp-qa-actioneditinline ng-button ng-button-link ng-edit-card-button ng-text-transform-remove"
onClick={toggleEditPlaces}
>
{t('edit')}
</button>
)}
<h2 className="ng-text-display-s ng-body-color ng-margin-medium-bottom ng-margin-top-remove">
{t('Collection places')} ({locations.length})
</h2>
<p>
{locations
.filter((x) => !!x)
.map((location) => (
<Pill
label={location.name}
key={location.id}
className="marapp-qa-locationpill ng-margin-small-right ng-margin-small-bottom"
/>
))}
</p>
</Card>
<Card className="c-legend-item-group ng-margin-top">
<h2 className="ng-text-display-s ng-body-color ng-margin-medium-bottom ng-margin-top-remove">
{t('Download metrics')}
&nbsp;
<i className="ng-icon-download-outline" style={{ verticalAlign: 'middle' }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<i className="ng-icon-download-outline" style={{ verticalAlign: 'middle' }} />
<i className="ng-icon-download-outline ng-vertical-align-middle" />

</h2>
<p>
{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')}.
</>
)}
</p>
<button
className="marapp-qa-actioneditinline ng-button ng-button-link ng-edit-card-button ng-text-transform-remove"
onClick={toggleEditPlaces}
className="marapp-qa-actiondownloadmetrics ng-button ng-button-secondary"
onClick={() => setIsOnDownloadMetrics(true)}
disabled={isDownloadingMetrics}
>
{t('edit')}
{isDownloadingMetrics ? (
<>
<Spinner size="nano" position="relative" className="ng-display-inline" />
{t('Downloading metrics')}
</>
) : (
<>{t('Download metric data files')}</>
)}
</button>
)}
<h2 className="ng-text-display-s ng-body-color ng-margin-medium-bottom ng-margin-top-remove">
{t('Collection places')} ({locations.length})
</h2>
<p>
{locations
.filter((x) => !!x)
.map((location) => (
<Pill
label={location.name}
key={location.id}
className="marapp-qa-locationpill ng-margin-small-right ng-margin-small-bottom"
/>
))}
</p>
</Card>
{downloadError && <p className="ng-form-error-block ng-margin-top">{downloadError}</p>}
</Card>
</>
) : (
<Card className="c-legend-item-group">
<h2 className="ng-text-display-s ng-body-color ng-margin-bottom">
Expand Down Expand Up @@ -158,6 +198,16 @@ const CollectionDetails = (props: IProps) => {
{isDeleting && (
<CollectionDelete collection={data} isDeleting={isDeleting} setIsDeleting={setIsDeleting} />
)}

{isOnDownloadMetrics && (
<CollectionDownloadMetrics
collection={data}
onCancel={() => setIsOnDownloadMetrics(false)}
onDownloadStart={() => [setIsDownloadingMetrics(true), setIsOnDownloadMetrics(false)]}
onDownloadEnd={() => setIsDownloadingMetrics(false)}
onDownloadError={(err) => setDownloadError(err)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onDownloadError={(err) => setDownloadError(err)}
onDownloadError={setDownloadError}

/>
)}
</div>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine earth-shared imports


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 (
<form onSubmit={handleSubmit(onSubmit)} className="sidebar-content-full ng-form ng-form-dark">
<Card elevation="high" className="ng-margin-bottom">
<TitleHero title={name} subtitle={organization} extra={t('Collection')} />
</Card>

<div className="scroll-container">
<Card elevation="raised">
<label>{t('Select metrics for download')}</label>
<Controller
as={ReactSelect}
name="metrics"
type="metrics"
placeholder={t('Select metrics to download data files')}
className="marapp-qa-downloadmetricsdropdown ng-margin-medium-bottom"
options={metricSlugs}
isLoading={isLoadingMetricSlugs}
defaultValue={[]}
control={control}
isClearable={true}
isSearchable={true}
isMulti={true}
closeMenuOnSelect={false}
/>
<label>{t('Select a file type for download')}</label>
<div className="legend-item-group--radio ng-margin-top ng-margin-medium-bottom">
<div className="ng-display-inline-block ng-margin-medium-right">
<input
type="radio"
id={`radio-csv`}
value={'csv'}
name="fileType"
ref={register({
required: true,
})}
className="marapp-qa-downloadmetricscsv"
/>
<label htmlFor={`radio-csv`}>
<span className="legend-item-group--symbol" />
<span className="legend-item-group--name">CSV</span>
</label>
</div>
<div className="ng-display-inline-block ng-margin-medium-left">
<input
type="radio"
id={`radio-json`}
value={'json'}
name="fileType"
ref={register({
required: true,
})}
className="marapp-qa-downloadmetricsjson"
/>
<label htmlFor={`radio-json`}>
<span className="legend-item-group--symbol" />
<span className="legend-item-group--name">JSON</span>
</label>
</div>
</div>
<button
type="submit"
className="marapp-qa-actiondownload ng-button ng-button-primary ng-margin-right"
disabled={!isValid || isSubmitting || !dirty || !metricsWatcher?.length}
>
{t('Download')}
</button>
<button
className="marapp-qa-actioncancel ng-button ng-button-secondary"
onClick={onCancel}
>
{t('Cancel')}
</button>
</Card>
</div>
</form>
);

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, '#')),
}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can find a valid constant name to replace '#' eg: nr the code would become:

Suggested change
normalizedData.map((item) => ({
'#': item['#'],
...flatten(omit(item, '#')),
}))
normalizedData.map(({nr, ...item}) => ({
nr,
...flatten(item),
}))

)
);
} else {
zip.file(fileName, JSON.stringify(normalizedData));
}
});

const zipName = `${collectionSlug}-metrics.zip`;
const zipContent = await zip.generateAsync({ type: 'blob' });

FileSaver.saveAs(zipContent, zipName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should clear the previous error message somehow.
One suggestion could be to add onDownloadSuccess() prop and call it here

} catch (e) {
onDownloadError('Something went wrong');
console.log(e);
}

onDownloadEnd();
}
}
Original file line number Diff line number Diff line change
@@ -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';
10 changes: 10 additions & 0 deletions packages/earth-map/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions packages/earth-map/src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading