implements csv+json downloads

This commit is contained in:
msramalho
2022-04-06 18:57:09 +02:00
parent 68362d64b5
commit 799f67ea49
12 changed files with 265 additions and 35 deletions

View File

@@ -147,6 +147,7 @@ module.exports = {
COLOR_BY_ASSOCIATION: true,
USE_ASSOCIATIONS: true,
USE_FULLSCREEN: true,
USE_DOWNLOAD: true,
USE_SOURCES: true,
USE_SPOTLIGHTS: false,
USE_SHAPES: false,

85
package-lock.json generated
View File

@@ -47,6 +47,7 @@
"jest-resolve": "26.6.0",
"jest-watch-typeahead": "0.6.1",
"joi": "^14.0.1",
"json2csv": "^5.0.7",
"leaflet": "^1.0.3",
"lint-staged": "^10.5.3",
"marked": "^0.7.0",
@@ -15261,6 +15262,31 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"devOptional": true
},
"node_modules/json2csv": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz",
"integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==",
"dependencies": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
},
"bin": {
"json2csv": "bin/json2csv.js"
},
"engines": {
"node": ">= 10",
"npm": ">= 6.13.0"
}
},
"node_modules/json2csv/node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"engines": {
"node": ">= 6"
}
},
"node_modules/json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
@@ -15288,6 +15314,14 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"engines": [
"node >= 0.2.0"
]
},
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -15845,6 +15879,11 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -24434,19 +24473,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
"integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
@@ -37962,6 +37988,23 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"devOptional": true
},
"json2csv": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz",
"integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==",
"requires": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
},
"dependencies": {
"commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
}
}
},
"json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
@@ -37981,6 +38024,11 @@
"universalify": "^2.0.0"
}
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
},
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -38407,6 +38455,11 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -45128,12 +45181,6 @@
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
"integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
"peer": true
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",

View File

@@ -54,6 +54,7 @@
"jest-resolve": "26.6.0",
"jest-watch-typeahead": "0.6.1",
"joi": "^14.0.1",
"json2csv": "^5.0.7",
"leaflet": "^1.0.3",
"lint-staged": "^10.5.3",
"marked": "^0.7.0",

View File

@@ -11,6 +11,7 @@ export const DEFAULT_TAB_ICONS = {
NARRATIVE: "timeline",
FILTER: "filter_list",
SHAPE: "change_history",
DOWNLOAD: "download"
};
export const AVAILABLE_SHAPES = {

View File

@@ -159,7 +159,24 @@
"explore_by_shapes__title": "Explore events by shape breakdown",
"explore_by_shape__description": "Shapes map to a given type of event that appears on the timeline.<br><br>Select the shape marker to toggle this type of event on / off",
"fullscreen_enter": "Fullscreen",
"fullscreen_exit": "Exit Fullscreen"
"fullscreen_exit": "Exit Fullscreen",
"download": {
"button": "Download",
"panel":{
"title": "Download events",
"description": "Export the most recent available events in different formats.",
"formats": {
"csv": {
"label": "CSV",
"description": "CSV file where sources and filters are concatenated into a single column due to data structure limitations."
},
"json": {
"label": "JSON",
"description": "JSON file where each event is a structured object containing nested arrays of sources and filters."
}
}
}
}
},
"timeline": {
"labels_title": "Testimonies",

View File

@@ -576,4 +576,17 @@ export function getFilterIdx(
else return 0;
}
export function downloadAsFile(filename, content) {
let element = document.createElement('a');
element.setAttribute('href', `data:application/octet-stream;charset=utf-8,${encodeURIComponent(content)}`);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
export const isEmptyString = (s) => s.length === 0;

View File

@@ -301,12 +301,13 @@ class Dashboard extends React.Component {
overflowY: "scroll",
textAlign: "justify",
};
return (
<div>
{checkMobile ? null : (
<Toolbar
isNarrative={!!app.associations.narrative}
domain={domain}
methods={{
onTitle: actions.toggleCover,
onSelectFilter: (filters) =>

View File

@@ -23,6 +23,7 @@ import {
} from "../common/utilities.js";
import { ToolbarButton } from "./controls/atoms/ToolbarButton";
import { FullscreenToggle } from "./controls/FullScreenToggle";
import DownloadPanel from "./controls/DownloadPanel";
class Toolbar extends React.Component {
constructor(props) {
@@ -180,6 +181,21 @@ class Toolbar extends React.Component {
}
}
renderToolbarDownloadPanel() {
const { panels } = this.props.toolbarCopy;
return (
<TabPanel>
<DownloadPanel
language={this.props.language}
title={panels.download.label}
description={panels.download.description}
domain={this.props.domain}
/>
</TabPanel>
);
}
renderToolbarTab(_selected, label, iconKey, key) {
return (
<ToolbarButton
@@ -223,6 +239,7 @@ class Toolbar extends React.Component {
{features.USE_CATEGORIES ? this.renderToolbarCategoriesPanel() : null}
{features.USE_ASSOCIATIONS ? this.renderToolbarFilterPanel() : null}
{features.USE_SHAPES ? this.renderToolbarShapePanel() : null}
{features.USE_DOWNLOAD ? this.renderToolbarDownloadPanel() : null}
</div>
);
}
@@ -267,7 +284,8 @@ class Toolbar extends React.Component {
features.USE_CATEGORIES,
numCategoryPanels || 0
);
const shapesIdx = filtersIdx + 1;
const shapesIdx = filtersIdx + features.USE_SHAPES;
const downloadIdx = shapesIdx + features.USE_DOWNLOAD;
return (
<div className="toolbar">
@@ -278,27 +296,34 @@ class Toolbar extends React.Component {
<TabList>
{narrativesExist
? this.renderToolbarTab(
narrativesIdx,
panels.narratives.label,
panels.narratives.icon
)
narrativesIdx,
panels.narratives.label,
panels.narratives.icon
)
: null}
{features.USE_CATEGORIES
? this.renderToolbarCategoryTabs(categoryIdxs)
: null}
{features.USE_ASSOCIATIONS
? this.renderToolbarTab(
filtersIdx,
panels.filters.label,
panels.filters.icon
)
filtersIdx,
panels.filters.label,
panels.filters.icon
)
: null}
{features.USE_SHAPES
? this.renderToolbarTab(
shapesIdx,
panels.shapes.label,
panels.shapes.icon
)
shapesIdx,
panels.shapes.label,
panels.shapes.icon
)
: null}
{features.USE_DOWNLOAD
? this.renderToolbarTab(
downloadIdx,
panels.download.label,
panels.download.icon
)
: null}
{features.USE_FULLSCREEN && (
<FullscreenToggle language={this.props.language} />

View File

@@ -0,0 +1,84 @@
import React from "react";
import copy from "../../common/data/copy.json";
import { parse } from 'json2csv'
import { downloadAsFile } from "../../common/utilities"
export class DownloadButton extends React.Component {
onDownload(format, domain) {
let filename = `ukr-civharm-${this.datetimeToDateString(new Date())}`;
if (format === "csv") {
let outputData = this.getCsvData(domain)
downloadAsFile(`${filename}.csv`, outputData);
} else if (format === "json") {
let outputData = this.getJsonData(domain)
downloadAsFile(`${filename}.json`, outputData);
}
}
getCsvData(domain) {
const { events, sources } = domain;
const exportEvents = events.map(e => {
return {
id: e.civId,
date: this.datetimeToDateString(e.datetime),
latitude: e.latitude,
longitude: e.longitude,
location: e.location,
description: e.description,
sources: e.sources.map(s => sources[s].paths[0]).join(","),
associations: e.associations.map(a => a.filter_paths.join("=")).join(",")
}
})
return parse(exportEvents, { flatten: true })
}
getJsonData(domain) {
const { events, sources } = domain;
const exportEvents = events.map(e => {
return {
id: e.civId,
date: this.datetimeToDateString(e.datetime),
latitude: e.latitude,
longitude: e.longitude,
location: e.location,
description: e.description,
sources: e.sources.map(id => {
const s = sources[id]
return {
id,
path: s.paths[0],
description: s.description
}
}),
filters: e.associations.map(a => {
return {
key: a.filter_paths[0],
value: a.filter_paths[1]
}
})
}
});
return JSON.stringify(exportEvents);
}
datetimeToDateString(datetime) {
try {
return datetime.toISOString().split("T")[0]
} catch (_) { }
return "";
}
render() {
const { language, domain, format } = this.props;
const textByFormat = copy[language].toolbar.download.panel.formats[format]
return (
<div className="download-row">
<span
className="download-button"
key={`download-${format}`}
onClick={() => this.onDownload(format, domain)}
>
<i className="material-icons">{"download"}</i>
<span className="tab-caption">{textByFormat.label}</span>
</span>
<span className="download-description">{textByFormat.description}</span>
</div>
);
}
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import { DownloadButton } from "./DownloadButton";
const DownloadPanel = ({
language,
title,
description,
domain
}) => {
return (
<div className="react-innertabpanel">
<h2>{title}</h2>
<p>{description}</p>
<hr/>
<DownloadButton language={language} domain={domain} format="csv" />
<DownloadButton language={language} domain={domain} format="json" />
</div>
);
};
export default DownloadPanel;

View File

@@ -186,7 +186,20 @@
}
}
.toolbar-tab {
.download-row{
display: flex;
flex-direction: row;
}
.download-button{
flex: 1 1 auto;
}
.download-description{
flex: 1 5 auto;
text-align: justify;
margin: auto;
}
.toolbar-tab, .download-button {
display: flex;
align-items: center;
justify-content: center;

View File

@@ -139,6 +139,12 @@ const initial = {
title: copy[language].toolbar.explore_by_shape__title,
description: copy[language].toolbar.explore_by_shape__description,
},
download: {
icon: DEFAULT_TAB_ICONS.DOWNLOAD,
label: copy[language].toolbar.download.button,
title: copy[language].toolbar.download.panel.title,
description: copy[language].toolbar.download.panel.description,
},
},
},
loading: false,