Add CSV export to all DataTables

Affects issues:
- Close #3413
This commit is contained in:
Aurora Lahtela 2024-01-21 10:02:39 +02:00
parent 673cb4cfdb
commit 34a731b70a
7 changed files with 98 additions and 22 deletions

View File

@ -283,6 +283,7 @@ public enum HtmlLang implements Lang {
LABEL_TABLE_VISIBLE_COLUMNS("html.label.table.visibleColumns", "Visible columns"),
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{m}} entries"),
LABEL_TABLE_SHOW_PER_PAGE("html.label.table.showPerPage", "Show per page"),
LABEL_EXPORT("html.label.export", "Export"),
LOGIN_LOGIN("html.login.login", "Login"),
LOGIN_LOGOUT("html.login.logout", "Logout"),

View File

@ -24,6 +24,7 @@
"@testing-library/user-event": "^14.5.2",
"axios": "^1.6.5",
"bootstrap": "^5.3.2",
"export-to-csv": "^1.2.2",
"highcharts": "^10.3.3",
"i18next": "^23.7.16",
"i18next-chained-backend": "^4.6.2",

View File

@ -7,12 +7,13 @@ import DataTablesTable from "../../table/DataTablesTable";
import {CardLoader} from "../../navigation/Loader";
import {Link} from "react-router-dom";
import {faCalendarCheck, faCalendarPlus, faClock} from "@fortawesome/free-regular-svg-icons";
import FormattedDate from "../../text/FormattedDate";
import FormattedTime from "../../text/FormattedTime";
import {formatDate, useDatePreferences} from "../../text/FormattedDate";
import {useTimePreferences} from "../../text/FormattedTime";
import ExtensionIcon from "../../extensions/ExtensionIcon";
import {ExtensionValueTableCell} from "../../extensions/ExtensionCard";
import {usePreferences} from "../../../hooks/preferencesHook";
import {formatDecimals} from "../../../util/formatters";
import {formatTimeAmount} from "../../../util/format/TimeAmountFormat.js";
const getActivityGroup = value => {
const VERY_ACTIVE = 3.75;
@ -38,6 +39,9 @@ const PlayerListCard = ({data, title, justList, orderBy}) => {
const [options, setOptions] = useState(undefined);
const timePreferences = useTimePreferences();
const datePreferences = useDatePreferences();
useEffect(() => {
if (!data) return;
@ -80,20 +84,25 @@ const PlayerListCard = ({data, title, justList, orderBy}) => {
}
}));
const formatDateEasy = date => {
return formatDate(date, datePreferences.offset, datePreferences.pattern, false, datePreferences.recentDaysPattern, t);
}
const rows = data.players.map(player => {
const row = {
name: player.playerName,
uuid: player.playerUUID,
link: <Link to={"/player/" + player.playerUUID}>{player.playerName}</Link>,
activityIndex: player.activityIndex,
activityGroup: t(getActivityGroup(player.activityIndex)),
activityIndexAndGroup: formatDecimals(player.activityIndex, decimalFormat) + " (" + t(getActivityGroup(player.activityIndex)) + ")",
activePlaytime: player.playtimeActive,
activePlaytimeFormatted: <FormattedTime timeMs={player.playtimeActive}/>,
activePlaytimeFormatted: formatTimeAmount(timePreferences, player.playtimeActive),
sessions: player.sessionCount,
registered: player.registered,
registeredFormatted: <FormattedDate date={player.registered}/>,
registeredFormatted: formatDateEasy(player.registered),
lastSeen: player.lastSeen,
lastSeenFormatted: <FormattedDate date={player.lastSeen}/>,
lastSeenFormatted: formatDateEasy(player.lastSeen),
country: player.country,
pingAverage: player.pingAverage,
pingAverageFormatted: formatDecimals(player.pingAverage, decimalFormat) + "ms",

View File

@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useState} from 'react';
import {useTheme} from "../../hooks/themeHook";
import {Card, InputGroup} from "react-bootstrap";
import {Card, Dropdown, InputGroup} from "react-bootstrap";
import Select from "../input/Select";
import SearchField from "../input/SearchField";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@ -17,6 +17,7 @@ import Toggle from "../input/Toggle";
import CollapseWithButton from "../layout/CollapseWithButton";
import {faMinusSquare, faPlusSquare} from "@fortawesome/free-regular-svg-icons";
import {Trans, useTranslation} from "react-i18next";
import {download, generateCsv, mkConfig} from "export-to-csv";
const PaginationOption = ({onClick, children, selected}) => (
<li>
@ -79,6 +80,51 @@ const VisibleColumnsSelector = ({columns, visibleColumnIndexes, toggleColumn}) =
)
}
const ExportMenu = ({matchingData}) => {
const {t} = useTranslation();
const [generating, setGenerating] = useState(false);
const hasData = matchingData.length > 0;
const exportCSV = useCallback(async () => {
setGenerating(true);
const rows = matchingData.map(row => {
const mapped = {};
for (let entry of Object.entries(row)) {
if (entry[1] === undefined || entry[1]["$$typeof"] === undefined) {
mapped[entry[0]] = entry[1];
}
}
return mapped;
})
const csvConfig = mkConfig({
useKeysAsHeaders: true,
filename: "data-" + new Date().toISOString().replaceAll(":", '').substring(0, 17)
});
const csvOutput = generateCsv(csvConfig)(rows);
await download(csvConfig)(csvOutput);
setGenerating(false)
}, [matchingData, setGenerating])
return (
<>{hasData &&
<Dropdown>
<Dropdown.Toggle variant={""} id="dropdown-basic">
{generating &&
<FontAwesomeIcon icon={"gear"} className={"fa-spin"} title={t('html.label.export')}/>}
{!generating && <FontAwesomeIcon icon={"file-export"} title={t('html.label.export')}/>}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={exportCSV}><FontAwesomeIcon
icon={"file-export"}/> {t('html.label.export')} CSV</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
}</>
)
}
const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
@ -205,6 +251,9 @@ const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => {
<VisibleColumnsSelector columns={columns} visibleColumnIndexes={visibleColumnIndexes}
toggleColumn={toggleColumn}/>
</div>}
<div className={"float-end dataTables_columns"}>
<ExportMenu matchingData={matchingData} columns={columns}/>
</div>
<table id={id}
className={"datatable table table-bordered table-striped" + (nightModeEnabled ? " table-dark" : '')}
style={{width: "100%"}}>

View File

@ -5,13 +5,11 @@ import {useMetadata} from "../../hooks/metadataHook";
import {useTranslation} from "react-i18next";
import {isNumber} from "../../util/isNumber.js";
const FormattedDate = ({date}) => {
const {t} = useTranslation();
export const useDatePreferences = () => {
const {timeZoneOffsetHours} = useMetadata();
const {preferencesLoaded, dateFormatNoSeconds, recentDaysInDateFormat} = usePreferences();
if (!preferencesLoaded || date === undefined || date === null) return <></>;
if (!isNumber(date)) return date;
if (!preferencesLoaded) return {};
const pattern = dateFormatNoSeconds;
const recentDays = recentDaysInDateFormat;
@ -19,6 +17,10 @@ const FormattedDate = ({date}) => {
const offset = timeZoneOffsetHours * 60 * 60 * 1000;
return {pattern, recentDays, recentDaysPattern, offset};
}
export function formatDate(date, offset, pattern, recentDays, recentDaysPattern, t) {
const dayMs = 24 * 60 * 60 * 1000;
const timestamp = date - offset;
const now = Date.now();
@ -35,11 +37,18 @@ const FormattedDate = ({date}) => {
}
}
const formatted = date !== 0 ? new SimpleDateFormat(format).format(timestamp) : '-';
return date !== 0 ? new SimpleDateFormat(format).format(timestamp) : '-'
}
return (
<>{formatted}</>
)
const FormattedDate = ({date}) => {
const {t} = useTranslation();
const {pattern, recentDays, recentDaysPattern, offset} = useDatePreferences();
if (!pattern || date === undefined || date === null) return <></>;
if (!isNumber(date)) return date;
return formatDate(date, offset, pattern, recentDays, recentDaysPattern, t);
};
export default FormattedDate

View File

@ -3,13 +3,12 @@ import {usePreferences} from "../../hooks/preferencesHook";
import {formatTimeAmount} from "../../util/format/TimeAmountFormat";
import {isNumber} from "../../util/isNumber.js";
const FormattedTime = ({timeMs}) => {
export const useTimePreferences = () => {
const {preferencesLoaded, timeFormat} = usePreferences();
if (!preferencesLoaded) return <></>;
if (!isNumber(timeMs)) return timeMs;
if (!preferencesLoaded) return undefined;
const options = {
return {
YEAR: timeFormat.year,
YEARS: timeFormat.years,
MONTH: timeFormat.month,
@ -21,11 +20,14 @@ const FormattedTime = ({timeMs}) => {
SECONDS: timeFormat.seconds,
ZERO: timeFormat.zero
}
const formatted = formatTimeAmount(options, timeMs);
}
return (
<>{formatted}</>
)
const FormattedTime = ({timeMs}) => {
const options = useTimePreferences()
if (!options) return <></>;
if (!isNumber(timeMs)) return timeMs;
return formatTimeAmount(options, timeMs);
};
export const formatTimeFunction = time => {

View File

@ -1782,6 +1782,11 @@ expand-template@^2.0.3:
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
export-to-csv@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-1.2.2.tgz#24f09a2dba34aff65f8f97402a869e88232b8f58"
integrity sha512-d9BxkLlyxeyFU1yhQKGjnnEQj2fh6PjyDVGyiI/uj7PD/oe7Lo1551B1lTaQeXHGPRX2DHuk6i3JMZB6ncJApQ==
fast-json-patch@^3.0.0-1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"