diff --git a/apps/platform/src/components/AssociationsToolkit/DataDownloader.jsx b/apps/platform/src/components/AssociationsToolkit/DataDownloader.jsx index 704940c9e..ee0561b65 100644 --- a/apps/platform/src/components/AssociationsToolkit/DataDownloader.jsx +++ b/apps/platform/src/components/AssociationsToolkit/DataDownloader.jsx @@ -1,266 +1,241 @@ -import FileSaver from 'file-saver'; -import { useState, useMemo } from 'react'; -import _ from 'lodash'; +import FileSaver from "file-saver"; +import { useState, useMemo, useEffect, useReducer } from "react"; +import _ from "lodash"; import { Button, Grid, Typography, Snackbar, Slide, - Popover, - Alert, CircularProgress, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; -import { Link } from 'ui'; -import { faCloudArrowDown } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import useBatchDownloader from './hooks/useBatchDownloader'; -import useAotfContext from './hooks/useAotfContext'; -import dataSources from './static_datasets/dataSourcesAssoc'; -import prioritizationCols from './static_datasets/prioritizationCols'; -import { isPartnerPreview } from './utils'; - -const PopoverContent = styled('div')({ - padding: '20px 25px', - width: '350px', -}); - -const DisclaimerContainer = styled('div')({ - marginTop: 12, + Dialog, + DialogTitle, + DialogContent, + FormGroup, + FormControlLabel, + Checkbox, + Accordion, + AccordionSummary, + AccordionDetails, + Divider, + FormControl, + InputLabel, + Select, + MenuItem, + ListItemText, + Box, + FormHelperText, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { makeStyles } from "@mui/styles"; +import { faCloudArrowDown, faLink, faCaretDown } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import useBatchDownloader from "./hooks/useBatchDownloader"; +import useAotfContext from "./hooks/useAotfContext"; +import OriginalDataSources from "./static_datasets/dataSourcesAssoc"; +import prioritizationCols from "./static_datasets/prioritizationCols"; +import { + getRowsQuerySelector, + getExportedColumns, + getExportedPrioritisationColumns, + createBlob, + getFilteredColumnArray, +} from "./utils/downloads"; +import config from "../../config"; + +const { isPartnerPreview } = config.profile; + +const dataSources = OriginalDataSources.filter(e => { + if (isPartnerPreview && e.isPrivate) { + return e; + } else if (!e.isPrivate) return e; + return; }); -const LabelContainer = styled('div')({ +const LabelContainer = styled("div")({ marginBottom: 12, }); -const targetName = { - id: 'symbol', - label: 'Symbol', - exportValue: data => data.target.approvedSymbol, -}; - -const diseaseName = { - id: 'symbol', - label: 'Symbol', - exportValue: data => data.disease.name, -}; - -const getRowsQuerySelector = entityToGet => - entityToGet === 'target' - ? 'data.disease.associatedTargets' - : 'data.target.associatedDiseases'; - -const getExportedColumns = entityToGet => { - const nameColumn = entityToGet === 'target' ? targetName : diseaseName; - let exportedColumns = []; - const sources = dataSources.map(({ id }) => ({ - id, - exportValue: data => { - const datatypeScore = data.datasourceScores.find( - datasourceScore => datasourceScore.componentId === id - ); - return datatypeScore ? parseFloat(datatypeScore.score) : 'No data'; - }, - })); - - exportedColumns = [...sources]; - - if (entityToGet === 'target' && isPartnerPreview) { - const prioritisationExportCols = prioritizationCols.map(({ id }) => ({ - id, - exportValue: data => { - const prioritisationScore = data.target.prioritisation.items.find( - prioritisationItem => prioritisationItem.key === id - ); - return prioritisationScore - ? parseFloat(prioritisationScore.value) - : 'No data'; - }, - })); - - exportedColumns = [...sources, ...prioritisationExportCols]; - } - - return [ - nameColumn, - { - id: 'score', - label: 'Score', - exportValue: data => data.score, - }, - ...exportedColumns, - ]; -}; - -const asJSON = (columns, rows) => { - const rowStrings = rows.map(row => - columns.reduce((accumulator, newKey) => { - if (newKey.exportValue === false) return accumulator; - - const newLabel = _.camelCase( - newKey.exportLabel || newKey.label || newKey.id - ); - - return { - ...accumulator, - [newLabel]: newKey.exportValue - ? newKey.exportValue(row) - : _.get(row, newKey.propertyPath || newKey.id, ''), - }; - }, {}) - ); - - return JSON.stringify(rowStrings); -}; - -const getHeaderString = ({ columns, quoteString, separator }) => - columns - .reduce((headerString, column) => { - if (column.exportValue === false) return headerString; - - const newLabel = quoteString( - _.camelCase(column.exportLabel || column.label || column.id) - ); - - return [...headerString, newLabel]; - }, []) - .join(separator); - -const asDSV = (columns, rows, separator = ',', quoteStrings = true) => { - const quoteString = d => { - let result = d; - // converts arrays to strings - if (Array.isArray(d)) { - result = d.join(','); - } - return quoteStrings && typeof result === 'string' ? `"${result}"` : result; - }; - - const lineSeparator = '\n'; - - const headerString = getHeaderString({ columns, quoteString, separator }); - - const rowStrings = rows - .map(row => - columns - .reduce((rowString, column) => { - if (column.exportValue === false) return rowString; - - const newValue = quoteString( - column.exportValue - ? column.exportValue(row) - : _.get(row, column.propertyPath || column.id, '') - ); - - return [...rowString, newValue]; - }, []) - .join(separator) - ) - .join(lineSeparator); - - return [headerString, rowStrings].join(lineSeparator); -}; - -const createBlob = format => - ({ - json: (columns, rows) => - new Blob([asJSON(columns, rows)], { - type: 'application/json;charset=utf-8', - }), - csv: (columns, rows) => - new Blob([asDSV(columns, rows)], { - type: 'text/csv;charset=utf-8', - }), - tsv: (columns, rows) => - new Blob([asDSV(columns, rows, '\t', false)], { - type: 'text/tab-separated-values;charset=utf-8', - }), - }[format]); +const BorderAccordion = styled(Accordion)(({ theme }) => ({ + boxShadow: "none", + border: `1px solid ${theme.palette.primary.light}`, + borderRadius: `${theme.spacing(1)} !important`, +})); const styles = makeStyles(theme => ({ messageProgress: { - marginRight: '1rem', - color: 'white !important', + marginRight: "1rem", + color: "white !important", }, snackbarContentMessage: { - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - padding: '.75rem 1rem', - width: '100%', + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + padding: ".75rem 1rem", + width: "100%", }, snackbarContentRoot: { padding: 0, }, backdrop: { - '& .MuiBackdrop-root': { - opacity: '0 !important', + "& .MuiBackdrop-root": { + opacity: "0 !important", }, }, container: { - width: '80%', + width: "80%", backgroundColor: theme.palette.grey[300], }, paper: { - margin: '1.5rem', - padding: '1rem', + margin: "1.5rem", + padding: "1rem", }, title: { - display: 'flex', - justifyContent: 'space-between', - backgroundColor: 'white', - borderBottom: '1px solid #ccc', - fontSize: '1.2rem', - fontWeight: 'bold', - padding: '1rem', + display: "flex", + justifyContent: "space-between", + backgroundColor: "white", + borderBottom: "1px solid #ccc", + fontSize: "1.2rem", + fontWeight: "bold", + padding: "1rem", }, playgroundContainer: { - margin: '0 1.5rem 1.5rem 1.5rem', - height: '100%', + margin: "0 1.5rem 1.5rem 1.5rem", + height: "100%", }, })); +const allAssociationsAggregation = [...new Set(dataSources.map(e => e.aggregation))]; +const allPrioritizationAggregation = [...new Set(prioritizationCols.map(e => e.aggregation))]; + +const initialState = { + associationAggregationSelectValue: allAssociationsAggregation, + prioritisationAggregationSelectValue: allPrioritizationAggregation, + selectedAssociationAggregationColumnObjectValue: [...dataSources], + selectedPrioritisationAggregationColumnObjectValue: [...prioritizationCols], +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case "UPDATE_ASSOCIATION_COLUMNS": + return { + ...state, + associationAggregationSelectValue: action.payload, + selectedAssociationAggregationColumnObjectValue: getFilteredColumnArray( + action.payload, + state.selectedAssociationAggregationColumnObjectValue + ), + }; + case "UPDATE_PRIORITISATION_COLUMNS": + return { + ...state, + prioritisationAggregationSelectValue: action.payload, + selectedPrioritisationAggregationColumnObjectValue: getFilteredColumnArray( + action.payload, + state.selectedPrioritisationAggregationColumnObjectValue + ), + }; + default: + return state; + } +}; + +const actions = { + UPDATE_ASSOCIATION_COLUMNS: payload => ({ + type: "UPDATE_ASSOCIATION_COLUMNS", + payload, + }), + UPDATE_PRIORITISATION_COLUMNS: payload => ({ + type: "UPDATE_PRIORITISATION_COLUMNS", + payload, + }), +}; + function DataDownloader({ fileStem }) { - const [downloading, setDownloading] = useState(false); + const [state, dispatch] = useReducer(reducer, initialState); + const classes = styles(); - const [anchorEl, setAnchorEl] = useState(null); const { id, query, searhFilter, sorting, enableIndirect, + entity, entityToGet, displayedTable, + modifiedSourcesDataControls, + pinnedEntries, + dataSourcesWeights, + dataSourcesRequired, } = useAotfContext(); + const [onlyPinnedCheckBox, setOnlyPinnedCheckBox] = useState(false); + const [weightControlCheckBox, setWeightControlCheckBox] = useState(modifiedSourcesDataControls); + const [requiredControlCheckBox, setRequiredControlCheckBox] = useState( + modifiedSourcesDataControls + ); + const [onlyTargetData, setOnlyTargetData] = useState(false); - const columns = useMemo(() => getExportedColumns(entityToGet), [entityToGet]); - const queryResponseSelector = useMemo( - () => getRowsQuerySelector(entityToGet), - [entityToGet] + const [downloading, setDownloading] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const [urlSnackbar, setUrlSnackbar] = useState(false); + const columns = useMemo( + () => + getExportedColumns( + entityToGet, + state.selectedAssociationAggregationColumnObjectValue, + state.selectedPrioritisationAggregationColumnObjectValue + ), + [ + entityToGet, + state.selectedAssociationAggregationColumnObjectValue, + state.selectedPrioritisationAggregationColumnObjectValue, + ] ); + const prioritisationColumns = useMemo( + () => + getExportedPrioritisationColumns(state.selectedPrioritisationAggregationColumnObjectValue), + [state.selectedPrioritisationAggregationColumnObjectValue] + ); + const queryResponseSelector = useMemo(() => getRowsQuerySelector(entityToGet), [entityToGet]); - const getAllAssociations = useBatchDownloader( + const allAssociationsVariable = { + id, + filter: searhFilter, + sortBy: sorting[0].id, + enableIndirect, + datasources: dataSourcesWeights, + ...(requiredControlCheckBox && { + aggregationFilters: dataSourcesRequired.map(el => ({ + name: el.name, + path: el.path, + })), + }), + }; + + const pinnedAssociationsVariable = { + ...allAssociationsVariable, + rowsFilter: pinnedEntries, + }; + + const getOnlyPinnedData = useBatchDownloader( query, - { - id, - filter: searhFilter, - sortBy: sorting[0].id, - enableIndirect, - }, + pinnedAssociationsVariable, queryResponseSelector ); - const isPrioritisation = displayedTable === 'prioritisations'; + const getAllAssociations = useBatchDownloader( + query, + allAssociationsVariable, + queryResponseSelector + ); const open = Boolean(anchorEl); - const popoverId = open ? 'dowloader-popover' : undefined; + const popoverId = open ? "downloader-popover" : undefined; const downloadData = async (format, dataColumns, rows, dataFileStem) => { let allRows = rows; - if (typeof rows === 'function') { + if (typeof rows === "function") { setDownloading(true); allRows = await rows(); setDownloading(false); @@ -269,7 +244,10 @@ function DataDownloader({ fileStem }) { return; } const blob = createBlob(format)(dataColumns, allRows); - FileSaver.saveAs(blob, `${dataFileStem}.${format}`, { autoBOM: false }); + const d = new Date().toLocaleDateString(); + FileSaver.saveAs(blob, `${dataFileStem}-${d}-v.${format}`, { + autoBOM: false, + }); }; const handleClickBTN = event => { @@ -281,13 +259,22 @@ function DataDownloader({ fileStem }) { }; const handleClickDownloadJSON = async () => { - downloadData('json', columns, getAllAssociations, fileStem); + const data = onlyPinnedCheckBox ? getOnlyPinnedData : getAllAssociations; + const columnToGet = onlyTargetData ? prioritisationColumns : columns; + downloadData("json", columnToGet, data, fileStem); }; const handleClickDownloadTSV = async () => { - downloadData('tsv', columns, getAllAssociations, fileStem); + const data = onlyPinnedCheckBox ? getOnlyPinnedData : getAllAssociations; + const columnToGet = onlyTargetData ? prioritisationColumns : columns; + downloadData("tsv", columnToGet, data, fileStem); }; + useEffect(() => { + setRequiredControlCheckBox(modifiedSourcesDataControls); + setWeightControlCheckBox(modifiedSourcesDataControls); + }, [modifiedSourcesDataControls]); + return (
- theme.spacing(1), + }, }} > - - - Download data as - - - - - - + Export: {fileStem} data + + + }> + Advanced export options: + + + + + + + Associations Aggregation + + + + Selected {state.associationAggregationSelectValue.length} of{" "} + {allAssociationsAggregation.length} + + + + {entity === "disease" && ( + + + Prioritization Aggregation + + + + Selected {state.prioritisationAggregationSelectValue.length} of{" "} + {allPrioritizationAggregation.length} + + + )} + + setWeightControlCheckBox(e.target.checked)} + /> + } + label="Include custom weight controls" + /> + setRequiredControlCheckBox(e.target.checked)} + /> + } + label="Include custom required control" + /> + setOnlyPinnedCheckBox(e.target.checked)} + /> + } + label="Only pinned / upload rows" + /> + + {entity === "disease" && ( + setOnlyTargetData(e.target.checked)} + /> + } + label="Only prioritisation data" + /> + )} + + + + + - - - - - Table with pre-set weights only. See{' '} - - here - {' '} - for more information. -
- {isPartnerPreview && - `The file will also include the target prioritisation data.`} -
-
-
-
+ + + + Download data as + + + + + + + + + + + + + } /> + + { + setUrlSnackbar(false); + }} + autoHideDuration={2000} + TransitionComponent={Slide} + ContentProps={{ + classes: { + root: classes.snackbarContentRoot, + message: classes.snackbarContentMessage, + }, + }} + message="URL copied" + />
); } diff --git a/apps/platform/src/components/AssociationsToolkit/utils/downloads.js b/apps/platform/src/components/AssociationsToolkit/utils/downloads.js new file mode 100644 index 000000000..1e3009d01 --- /dev/null +++ b/apps/platform/src/components/AssociationsToolkit/utils/downloads.js @@ -0,0 +1,181 @@ +import _ from 'lodash'; + +const targetName = { + id: 'symbol', + label: 'Symbol', + exportValue: data => data.target.approvedSymbol, +}; + +const diseaseName = { + id: 'symbol', + label: 'Symbol', + exportValue: data => data.disease.name, +}; + +const asJSON = (columns, rows) => { + const rowStrings = rows.map(row => + columns.reduce((accumulator, newKey) => { + if (newKey.exportValue === false) return accumulator; + + const newLabel = _.camelCase( + newKey.exportLabel || newKey.label || newKey.id + ); + + return { + ...accumulator, + [newLabel]: newKey.exportValue + ? newKey.exportValue(row) + : _.get(row, newKey.propertyPath || newKey.id, ''), + }; + }, {}) + ); + + return JSON.stringify(rowStrings); +}; + +const getHeaderString = ({ columns, quoteString, separator }) => + columns + .reduce((headerString, column) => { + if (column.exportValue === false) return headerString; + + const newLabel = quoteString( + _.camelCase(column.exportLabel || column.label || column.id) + ); + + return [...headerString, newLabel]; + }, []) + .join(separator); + +const asDSV = (columns, rows, separator = ',', quoteStrings = true) => { + const quoteString = d => { + let result = d; + // converts arrays to strings + if (Array.isArray(d)) { + result = d.join(','); + } + return quoteStrings && typeof result === 'string' ? `"${result}"` : result; + }; + + const lineSeparator = '\n'; + + const headerString = getHeaderString({ columns, quoteString, separator }); + + const rowStrings = rows + .map(row => + columns + .reduce((rowString, column) => { + if (column.exportValue === false) return rowString; + + const newValue = quoteString( + column.exportValue + ? column.exportValue(row) + : _.get(row, column.propertyPath || column.id, '') + ); + + return [...rowString, newValue]; + }, []) + .join(separator) + ) + .join(lineSeparator); + + return [headerString, rowStrings].join(lineSeparator); +}; + +export const getRowsQuerySelector = entityToGet => + entityToGet === 'target' + ? 'data.disease.associatedTargets' + : 'data.target.associatedDiseases'; + +export const getExportedColumns = (entityToGet, assocArr, prioArr) => { + const nameColumn = entityToGet === 'target' ? targetName : diseaseName; + let exportedColumns = []; + const sources = assocArr.map(({ id }) => ({ + id, + exportValue: data => { + const datatypeScore = data.datasourceScores.find( + datasourceScore => datasourceScore.componentId === id + ); + return datatypeScore ? parseFloat(datatypeScore.score) : 'No data'; + }, + })); + + exportedColumns = [...sources]; + + if (entityToGet === 'target') { + const prioritisationExportCols = prioArr.map(({ id }) => ({ + id, + exportValue: data => { + const prioritisationScore = data.target.prioritisation.items.find( + prioritisationItem => prioritisationItem.key === id + ); + return prioritisationScore + ? parseFloat(prioritisationScore.value) + : 'No data'; + }, + })); + + exportedColumns = [...sources, ...prioritisationExportCols]; + } + + return [ + nameColumn, + { + id: 'score', + label: 'Score', + exportValue: data => data.score, + }, + ...exportedColumns, + ]; +}; + +export const getExportedPrioritisationColumns = arr => { + let exportedColumns = []; + + const prioritisationExportCols = arr.map(({ id }) => ({ + id, + exportValue: data => { + const prioritisationScore = data.target.prioritisation.items.find( + prioritisationItem => prioritisationItem.key === id + ); + return prioritisationScore + ? parseFloat(prioritisationScore.value) + : 'No data'; + }, + })); + + exportedColumns = [...prioritisationExportCols]; + + return [ + targetName, + { + id: 'score', + label: 'Score', + exportValue: data => data.score, + }, + ...exportedColumns, + ]; +}; + +export const createBlob = format => + ({ + json: (columns, rows) => + new Blob([asJSON(columns, rows)], { + type: 'application/json;charset=utf-8', + }), + csv: (columns, rows) => + new Blob([asDSV(columns, rows)], { + type: 'text/csv;charset=utf-8', + }), + tsv: (columns, rows) => + new Blob([asDSV(columns, rows, '\t', false)], { + type: 'text/tab-separated-values;charset=utf-8', + }), + }[format]); + +export const getFilteredColumnArray = (selectArray, requestArray) => { + const arr = selectArray + .map(ag => requestArray.filter(e => e.aggregation === ag)) + .flat(1); + + return arr.length > 0 ? arr : []; +};