diff --git a/web/gui-v2/package-lock.json b/web/gui-v2/package-lock.json index 3695168e..0ac7d362 100644 --- a/web/gui-v2/package-lock.json +++ b/web/gui-v2/package-lock.json @@ -23,6 +23,7 @@ "luxon": "^3.3.0", "plotly.js": "^2.24.3", "react": "^18.2.0", + "react-csv": "^2.2.2", "react-dom": "^18.2.0", "react-plotly.js": "^2.6.0", "react-use-query-param-string": "^2.0.10", @@ -20003,6 +20004,11 @@ "node": ">=0.10.0" } }, + "node_modules/react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -38372,6 +38378,11 @@ "loose-envify": "^1.1.0" } }, + "react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/web/gui-v2/package.json b/web/gui-v2/package.json index 5a032cf3..1a424609 100644 --- a/web/gui-v2/package.json +++ b/web/gui-v2/package.json @@ -33,6 +33,7 @@ "luxon": "^3.3.0", "plotly.js": "^2.24.3", "react": "^18.2.0", + "react-csv": "^2.2.2", "react-dom": "^18.2.0", "react-plotly.js": "^2.6.0", "react-use-query-param-string": "^2.0.10", diff --git a/web/gui-v2/src/components/ListViewTable.jsx b/web/gui-v2/src/components/ListViewTable.jsx index 241781c7..010bc203 100644 --- a/web/gui-v2/src/components/ListViewTable.jsx +++ b/web/gui-v2/src/components/ListViewTable.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { CSVLink } from 'react-csv'; import { getQueryParams, useQueryParamString } from 'react-use-query-param-string'; import { css } from '@emotion/react'; import { @@ -541,6 +542,57 @@ const ListViewTable = ({ .map(([key, data]) => [key, <>Total: {commas(data)}]) ); + + const exportHeaders = columns.flatMap((colDef) => { + const headers = [ { key: colDef.key, label: colDef.title } ]; + // Derived columns aren't able to calculate a rank + if ( colDef.type === "slider" && !colDef?.isDerived ) { + headers.push({ key: `${colDef.key}_rank`, label: `${colDef.title} rank` }); + } + return headers; + }); + const exportData = useMemo(() => { + return dataForTable + .map((row) => { + const entry = {}; + for ( const colDef of columns ) { + if ( Object.hasOwn(colDef, "dataKey") && Object.hasOwn(colDef, "dataSubkey") ) { + const value = row[colDef.dataKey][colDef.dataSubkey]; + if ( colDef.type === "slider" && !colDef?.isDerived ) { + entry[colDef.key] = value.total; + entry[`${colDef.key}_rank`] = value.rank; + } else if ( Object.hasOwn(colDef, "extract") ) { + entry[colDef.key] = colDef.extract(value, row); + } + } else if ( Object.hasOwn(colDef, "dataKey") ) { + entry[colDef.key] = row[colDef.dataKey]; + } else { + entry[colDef.key] = row[colDef.key]; + } + } + return entry; + }) + .sort((a, b) => { + const direction = sortDir ? -1 : 1; + if ( Object.hasOwn(a, sortKey) && Object.hasOwn(b, sortKey) ) { + if ( SLIDER_COLUMNS.includes(sortKey) ) { + if ( a[sortKey] < b[sortKey] ) { + return -1 * direction; + } else if ( b[sortKey] < a[sortKey] ) { + return 1 * direction; + } else { + return 0; + } + } else { + return a[sortKey].localeCompare(b[sortKey]) * direction; + } + } + // If the sorting column isn't visible, default to sorting by company + // name (which is always visible). + return a.name.localeCompare(b.name) * direction; + }); + }, [dataForTable]); + return (
@@ -560,13 +612,17 @@ const ListViewTable = ({ Reset filters - {/* TODO: enable once downloads are possible - see #96 and #166 */} - + + +