diff --git a/src/components/CollapsibleTable.jsx b/src/components/CollapsibleTable.jsx
deleted file mode 100644
index 179d6478e..000000000
--- a/src/components/CollapsibleTable.jsx
+++ /dev/null
@@ -1,409 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import _ from 'lodash';
-import Box from '@mui/material/Box';
-import CircularProgress from '@mui/material/CircularProgress';
-import Collapse from '@mui/material/Collapse';
-import IconButton from '@mui/material/IconButton';
-import { styled } from '@mui/material/styles';
-import Table from '@mui/material/Table';
-import TableBody from '@mui/material/TableBody';
-import TableCell, { tableCellClasses } from '@mui/material/TableCell';
-import TableContainer from '@mui/material/TableContainer';
-import TableHead from '@mui/material/TableHead';
-import TableRow, { tableRowClasses } from '@mui/material/TableRow';
-import Paper from '@mui/material/Paper';
-import Popover from '@mui/material/Popover';
-import Typography from '@mui/material/Typography';
-import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
-import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
-import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
-import { isEmpty } from 'lodash';
-import { Checkbox } from '@mui/material';
-
-/*
-The data should follow the following format:
-
-const table = {
- headers: [
- {
- value: 'Header 1',
- },
- {
- value: () => 'Header 2',
- },
- {
- value: 'Header 3',
- },
- ],
- rows: [
- {
- id: 'Row 1',
- data: [
- {
- value: 'Row 1, Cell 1',
- truncate: true,
- },
- {
- value: 'Row 1, Cell 2',
- truncate: false,
- },
- {
- value: 'Row 1, Cell 3',
- truncate: false,
- }
- ],
- subtable: {
- headers: [
- {
- value: 'Sub header 1',
- },
- {
- value: 'Sub header 2',
- },
- ],
- rows: [
- {
- id: 'Sub Row 1',
- data: [
- {
- value: 'Sub Row 1, Cell 1',
- },
- {
- value: 'Sub Row 1, Cell 2',
- },
- ],
- },
- {
- id: 'Sub Row 2',
- data: [
- {
- value: 'Sub Row 2, Cell 1',
- },
- {
- value: 'Sub Row 2, Cell 2',
- },
- ],
- },
- ],
- }
- },
- {
- id: 'Row 2',
- data: [
- {
- value: 'Row 2, Cell 1',
- },
- {
- value: 'Row 2, Cell 2',
- },
- {
- value: 'Row 2, Cell 3',
- }
- ],
- },
- ],
-};
-*/
-
-const SubtableRow = styled(TableRow)(() => ({
- [`&.${tableRowClasses.root}`]: {
- border: '1px solid rgba(224, 224, 224, 1)'
- }
-}));
-
-const StyledTableCell = styled(TableCell)(() => ({
- paddingTop: '0px',
- paddingBottom: '0px',
- height: '57px',
- [`&.${tableCellClasses.head}`]: {
- color: '#333F52',
- fontFamily: 'Montserrat',
- fontSize: '14px',
- fontWeight: 'bold',
- lineHeight: '16px',
- backgroundColor: '#e2e8f4',
- textTransform: 'uppercase',
- },
- [`&.${tableCellClasses.body}`]: {
- fontSize: '14px',
- lineHeight: '24px',
- fontFamily: 'Montserrat',
- fontWeight: 400,
- color: '#333F52',
- letterSpacing: 0,
- }
-}));
-
-const HeaderCell = styled(StyledTableCell)(() => ({
- [`&.${tableCellClasses.root}`]: {
- height: '40px'
- }
-}));
-
-const TruncatedTableCell = styled(StyledTableCell)(() => ({
- [`&.${tableCellClasses.root}`]: {
- padding: '8px',
- maxWidth: '20ch',
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- transition: 'max-width 0.5s',
- '&:hover': {
- whiteSpace: 'normal',
- overflow: 'normal',
- wordBreak: 'break-all'
- },
- },
-}));
-
-const renderValue = (data) => {
- if (_.isEmpty(data)) {
- return null;
- }
- if (_.isFunction(data.value)) {
- return data.value();
- }
- return data.value;
-};
-
-const CollapsibleRow = (props) => {
- const { row, row: { subtable: { rows: subrows } }, selected, selectHandler, expandHandler, collapseHandler } = props;
-
- const [open, setOpen] = useState(false);
- const [toggling, setToggling] = useState(false);
-
- const isSelected = (id) => selected.indexOf(id) !== -1;
- const allSelected = subrows.every((row) => isSelected(row.id));
- const someSelected = subrows.some((row) => isSelected(row.id));
-
- const openHandler = async (event, data) => {
- if (_.isFunction(expandHandler)) {
- await expandHandler(event, data);
- }
- setOpen(true);
- };
-
- const closeHandler = async (event, data) => {
- if (_.isFunction(collapseHandler)) {
- await collapseHandler(event, data);
- }
- setOpen(false);
- };
-
- const toggleHandler = async (event, data) => {
- setToggling(true);
- if (open) {
- await closeHandler(event, data);
- } else {
- await openHandler(event, data);
- }
- setToggling(false);
- };
-
- return (
-
- {/* main table row */}
-
-
- selectHandler(event, row, 'row')}
- checked={allSelected}
- indeterminate={someSelected && !allSelected}
- />
-
-
- toggleHandler(event, row)}
- disabled={toggling}
- >
- {toggling && }
- {!toggling ? (open ? : ) : null}
-
-
- {row.data.map((cell, i) => {
- return ;
- })}
-
- {/* subtable */}
-
-
-
-
-
- {/* subtable header */}
-
-
-
- {row.subtable.headers.map((header, i) => (
- {renderValue(header)}
- ))}
-
-
- {/* subtable rows */}
-
- {subrows.map((subRow, j) => (
-
-
- selectHandler(event, subRow, 'subrow')}
- checked={isSelected(subRow.id)}
- />
-
- {subRow.data.map((cell, k) => {
- return ;
- })}
-
- ))}
-
-
-
-
-
-
-
- );
-};
-
-export const CollapsibleTable = (props) => {
- const { data, summary, selected, selectHandler, expandHandler } = props;
-
- const [allSelected, setAllSelected] = useState(false);
- const [someSelected, setSomeSelected] = useState(false);
-
- useEffect(() => {
- const isSelected = (id) => selected.indexOf(id) !== -1;
- if (!isEmpty(data) && !isEmpty(data.rows)) {
- setAllSelected(data.rows.every((row) => {
- return row.subtable.rows.every((subRow) => isSelected(subRow.id));
- }));
- setSomeSelected(data.rows.some((row) => {
- return row.subtable.rows.some((subRow) => isSelected(subRow.id));
- }));
- }
- }, [data, selected]);
-
- return !isEmpty(data) && (
-
-
- {/* main table header */}
-
-
-
- selectHandler(event, data, 'all')}
- checked={allSelected}
- indeterminate={someSelected && !allSelected}
- />
-
-
- {data.headers.map((header, i) => (
- {renderValue(header)}
- ))}
-
-
- {/* main table rows */}
-
- {data.rows.map((row) => (
-
- ))}
-
-
-
- );
-};
-
-export default CollapsibleTable;
-
-const TableCellRenderer = ({ cell }) => {
-
- const [anchorEl, setAnchorEl] = React.useState(null);
-
- const handlePopoverOpen = (event) => {
- setAnchorEl(event.currentTarget);
- };
-
- const handlePopoverClose = () => {
- setAnchorEl(null);
- };
-
- const popoverOpen = Boolean(anchorEl);
-
- if (cell?.hideUnderIcon) {
- return (
-
-
-
-
- {renderValue(cell)}
-
-
-
- );
- }
-
- if (cell?.truncate && cell?.increaseWidth) {
- return
- {renderValue(cell)}
- ;
- }
-
- if (cell?.truncate) {
- return
- {renderValue(cell)}
- ;
- }
-
- if (cell?.increaseWidth) {
- return
- {renderValue(cell)}
- ;
- }
-
- // Default case:
- return
- {renderValue(cell)}
- ;
-};
-
-const SubtableCellRenderer = ({ cell }) => {
-
- if (cell?.increaseWidth) {
- return
- {renderValue(cell)}
- ;
- }
-
- // Default case:
- return
- {renderValue(cell)}
- ;
-
-};
diff --git a/src/components/SimpleTable.jsx b/src/components/SimpleTable.jsx
index 989bfba08..069735599 100644
--- a/src/components/SimpleTable.jsx
+++ b/src/components/SimpleTable.jsx
@@ -104,7 +104,7 @@ const DataRows = ({rowData, baseStyle, columnHeaders, rowWrapper = ({renderedRow
let output;
//columnHeaders determine width of the columns,
//therefore extract width from columnHeader and apply to cell style
- const columnWidthStyle = columnHeaders[cellIndex].cellStyle;
+ const columnWidthStyle = { width: columnHeaders[cellIndex].cellStyle.width };
const appliedStyle = Object.assign({}, style, columnWidthStyle);
//assume component is in hyperscript format
//wrap component in dive with columnWidth applied
diff --git a/src/components/Tooltips.tsx b/src/components/Tooltips.tsx
new file mode 100644
index 000000000..392526fba
--- /dev/null
+++ b/src/components/Tooltips.tsx
@@ -0,0 +1,36 @@
+import ReactTooltip from 'react-tooltip';
+import * as React from 'react';
+import {useEffect, useRef, useState} from 'react';
+
+// Copied from ReactTooltip Types
+type Place = 'top' | 'right' | 'bottom' | 'left';
+
+interface TooltipProps {
+ tooltipText: string;
+ children: React.ReactNode | React.ReactNode[];
+ id: string;
+ place: Place
+}
+export const OverflowTooltip = (props: TooltipProps) => {
+ const ref = useRef(null);
+ const [isOverflown, setIsOverflown] = useState(false);
+ useEffect(() => {
+ const element = ref.current!;
+ setIsOverflown(element.offsetWidth < element.scrollWidth);
+ }, []);
+
+ return <>
+ {props.children}
+ {props.tooltipText}
+ >;
+};
diff --git a/src/components/data_search/DatasetSearchTable.jsx b/src/components/data_search/DatasetSearchTable.jsx
index a3d911fd4..e57d18a78 100644
--- a/src/components/data_search/DatasetSearchTable.jsx
+++ b/src/components/data_search/DatasetSearchTable.jsx
@@ -1,66 +1,67 @@
+import Tab from '@mui/material/Tab';
+import Tabs from '@mui/material/Tabs';
import * as React from 'react';
-import _ from 'lodash';
-import { Box, Button, Link } from '@mui/material';
+import { Box, Button } from '@mui/material';
import { useEffect, useState, useRef } from 'react';
-import { groupBy, isEmpty, concat, compact, map } from 'lodash';
-import CollapsibleTable from '../CollapsibleTable';
+import { isEmpty } from 'lodash';
+import { TerraDataRepo } from '../../libs/ajax/TerraDataRepo';
+import { DatasetSearchTableDisplay } from './DatasetSearchTableDisplay';
+import { datasetSearchTableTabs } from './DatasetSearchTableConstants';
import TableHeaderSection from '../TableHeaderSection';
-import DatasetExportButton from './DatasetExportButton';
import { DataSet } from '../../libs/ajax/DataSet';
import { DAR } from '../../libs/ajax/DAR';
-import eventList from '../../libs/events';
-import { Config } from '../../libs/config';
import DatasetFilterList from './DatasetFilterList';
-import { Metrics } from '../../libs/ajax/Metrics';
import { Notifications } from '../../libs/utils';
import { Styles } from '../../libs/theme';
-import { TerraDataRepo } from '../../libs/ajax/TerraDataRepo';
-import isEqual from 'lodash/isEqual';
-import TranslatedDulModal from '../modals/TranslatedDulModal';
-
+import * as _ from 'lodash';
+
+const styles = {
+ subTab: {
+ padding: '0 25px',
+ fontSize: '15px',
+ textTransform: 'none',
+ fontFamily: 'Montserrat, sans-serif',
+ color: '#00609f',
+ },
+ subTabActive: {
+ fontWeight: 'bold',
+ borderBottom: `5px solid #00609f`
+ },
+};
-const studyTableHeader = [
- 'Study Name',
- 'Description',
- 'Datasets',
- 'Participants',
- 'Phenotype',
- 'Species',
- 'PI Name',
- 'Data Custodian',
-];
-const datasetTableHeader = [
- 'DUOS ID',
- 'Dataset Name',
- 'Data Use',
- 'Data Types',
- 'Participants',
- 'Access Type',
- 'Data Location',
- 'Export to Terra',
-];
const defaultFilters = {
accessManagement: [],
dataUse: [],
-}
+};
export const DatasetSearchTable = (props) => {
const { datasets, history, icon, title } = props;
+ const [exportableDatasets, setExportableDatasets] = useState();
const [filters, setFilters] = useState(defaultFilters);
const [filtered, setFiltered] = useState([]);
- const [tableData, setTableData] = useState({});
const [selected, setSelected] = useState([]);
- const [exportableDatasets, setExportableDatasets] = useState({}); // datasetId -> snapshot
- const [tdrApiUrl, setTdrApiUrl] = useState('');
- const [showTranslatedDULModal, setShowTranslatedDULModal] = useState(false);
- const [dataUse, setDataUse] = useState();
+ const [selectedTable, setSelectedTable] = useState(datasetSearchTableTabs.study);
const searchRef = useRef('');
const isFiltered = (filter, category) => (filters[category]).indexOf(filter) > -1;
const numSelectedFilters = (filters) => Object.values(filters).reduce((sum, array) => sum + array.length, 0);
+ const getExportableDatasets = async (datasets) => {
+ // Note the dataset identifier is in each sub-table row.
+ const datasetIdentifiers = datasets.map((row) => row.datasetIdentifier);
+ const snapshots = await TerraDataRepo.listSnapshotsByDatasetIds(datasetIdentifiers);
+ if (snapshots.filteredTotal > 0) {
+ const datasetIdToSnapshot = _.chain(snapshots.items)
+ // Ignore any snapshots that a user does not have export (steward or reader) to
+ .filter((snapshot) => _.intersection(snapshots.roleMap[snapshot.id], ['steward', 'reader']).length > 0)
+ .groupBy('duosId')
+ .value();
+ setExportableDatasets(datasetIdToSnapshot);
+ }
+ };
+
const assembleFullQuery = (searchTerm, filters) => {
const queryChunks = [
{
@@ -171,7 +172,7 @@ export const DatasetSearchTable = (props) => {
const search = async () => {
try {
await DataSet.searchDatasetIndex(fullQuery).then((filteredDatasets) => {
- var newFiltered = datasets.filter(value => filteredDatasets.some(item => isEqual(item, value)));
+ var newFiltered = datasets.filter(value => filteredDatasets.some(item => _.isEqual(item, value)));
setFiltered(newFiltered);
});
} catch (error) {
@@ -180,74 +181,8 @@ export const DatasetSearchTable = (props) => {
};
search();
};
-
-
-
- const selectHandler = async (event, data, selector) => {
- let idsToModify = [];
- if (selector === 'all') {
- data.rows.forEach((row) => {
- row.subtable.rows.forEach((subRow) => {
- idsToModify.push(subRow.id);
- });
- });
- } else if (selector === 'row') {
- const rowIds = data.subtable.rows.map(row => row.id);
- const isRowSelected = rowIds.every(id => selected.includes(id));
- isRowSelected ?
- await Metrics.captureEvent(eventList.dataLibrary, {'action': 'study-unselected'}) :
- await Metrics.captureEvent(eventList.dataLibrary, {'action': 'study-selected'});
- data.subtable.rows.forEach((row) => {
- idsToModify.push(row.id);
- });
- } else if (selector === 'subrow') {
- selected.includes(data.id) ?
- await Metrics.captureEvent(eventList.dataLibrary, {'action': 'dataset-unselected'}) :
- await Metrics.captureEvent(eventList.dataLibrary,{'action': 'dataset-selected'});
- idsToModify.push(data.id);
- }
-
- let newSelected = [];
- const allSelected = idsToModify.every((id) => selected.includes(id));
- if (allSelected) {
- newSelected = selected.filter((id) => !idsToModify.includes(id));
- } else {
- newSelected = selected.concat(idsToModify);
- }
-
- setSelected(newSelected);
- };
-
- const getExportableDatasets = async (event, data) => {
- setTdrApiUrl(await Config.getTdrApiUrl());
- // Note the dataset identifier is in each sub-table row.
- const datasetIdentifiers = data.subtable.rows.map((row) => row.datasetIdentifier);
- const snapshots = await TerraDataRepo.listSnapshotsByDatasetIds(datasetIdentifiers);
- if (snapshots.filteredTotal > 0) {
- const datasetIdToSnapshot = _.chain(snapshots.items)
- // Ignore any snapshots that a user does not have export (steward or reader) to
- .filter((snapshot) => _.intersection(snapshots.roleMap[snapshot.id], ['steward', 'reader']).length > 0)
- .groupBy('duosId')
- .value();
- setExportableDatasets(datasetIdToSnapshot);
- }
- };
-
- const expandHandler = async (event, data) => {
- try {
- getExportableDatasets(event, data);
- } catch {
- Notifications.showError({ text: 'Unable to retrieve exportable datasets from Terra' });
- }
- };
-
- const collapseHandler = () => {
- setExportableDatasets({});
- };
-
const applyForAccess = async () => {
- const draftDatasets = selected.map((id) => parseInt(id.replace('dataset-', '')));
- const darDraft = await DAR.postDarDraft({ datasetId: draftDatasets });
+ const darDraft = await DAR.postDarDraft({ datasetId: selected });
history.push(`/dar_application/${darDraft.referenceId}`);
};
@@ -256,146 +191,12 @@ export const DatasetSearchTable = (props) => {
filterHandler(null, datasets, '', '');
};
- const openTranslatedDUL = (dataUse) => {
- const mergedPrimaryAndSecondary = concat(dataUse.primary, dataUse.secondary);
- setDataUse(mergedPrimaryAndSecondary);
- setShowTranslatedDULModal(true);
- };
-
useEffect(() => {
if (isEmpty(filtered)) {
return;
}
-
- const studies = groupBy(filtered, 'study.studyId');
- const table = {
- id: 'study-table',
- headers: studyTableHeader.map((header) => ({ value: header })),
- rows: Object.values(studies).map((entry, index) => {
- const sum = entry.reduce((acc, dataset) => {
- return acc + dataset.participantCount;
- }, 0);
- return {
- id: 'study-' + entry[0].study.studyId,
- data: [
- {
- value: entry[0].study.studyName,
- truncate: true,
- increaseWidth: true,
- },
- {
- value: entry[0].study.description,
- hideUnderIcon: true,
- },
- {
- value: entry.length,
- truncate: true,
- },
- {
- value: isNaN(sum) ? undefined : sum,
- truncate: true,
- },
- {
- value: entry[0].study.phenotype,
- truncate: true,
- },
- {
- value: entry[0].study.species,
- truncate: true,
- },
- {
- value: entry[0].study.piName,
- truncate: true,
- },
- {
- value: entry[0].study.dataCustodianEmail?.join(', '),
- truncate: true,
- },
- ],
- subtable: {
- headers: datasetTableHeader.map((header) => ({ value: header })),
- rows: entry.map((dataset) => {
- return {
- id: 'dataset-' + dataset.datasetId,
- datasetIdentifier: dataset.datasetIdentifier,
- data: [
- {
- value: {dataset.datasetIdentifier},
- increaseWidth: true,
- },
- {
- value: dataset.datasetName,
- truncate: true,
- },
- {
- value: () => {
- const dataUse = dataset.dataUse;
- const primaryDataUse = map(dataUse?.primary, 'code').join(', ');
- const secondaryDataUse = map(dataUse?.secondary, 'code').join(', ');
- const mergedDataUse = compact(concat(primaryDataUse, secondaryDataUse)).join(', ');
- return openTranslatedDUL(dataUse)}
- >{mergedDataUse};
- }
- },
- {
- value: dataset.dataTypes,
- },
- {
- value: dataset.participantCount,
- },
- {
- value: () => {
- let accessType;
- if (dataset.accessManagement === 'external') {
- accessType = dataset.url ? External to DUOS : 'External to DUOS';
- } else if (dataset.accessManagement === 'open') {
- accessType = dataset.url ? Open Access : 'Open Access';
- } else {
- accessType = dataset.dac?.dacEmail ? {dataset.dac?.dacName} : dataset.dac?.dacName;
- }
- return accessType;
- }
- },
- {
- value: () => {
- const exportableSnapshots = exportableDatasets[dataset.datasetIdentifier] || [];
- if (exportableSnapshots.length === 0) {
- return dataset.dataLocation;
- }
- return exportableSnapshots.map((snapshot, i) =>
-
- {snapshot.name}{'\n'}
- );
- }
- },
- {
- value: () => {
- const exportableSnapshots = exportableDatasets[dataset.datasetIdentifier] || [];
- return exportableSnapshots
- .map((snapshot, i) =>
- );
- }
- },
- ],
- };
- }),
- }
- };
- }),
- };
-
- setTableData(table);
- }, [filtered, exportableDatasets, tdrApiUrl]);
+ getExportableDatasets(filtered);
+ }, [filtered]);
useEffect(() => {
setFiltered(datasets);
@@ -432,6 +233,24 @@ export const DatasetSearchTable = (props) => {
+
+
+ {Object.values(datasetSearchTableTabs).map((tab) => setSelectedTable(tab)}
+ component={Button}
+ />)}
+
+
@@ -446,25 +265,8 @@ export const DatasetSearchTable = (props) => {
No datasets registered for this library.
);
- } else if (isEmpty(filtered)) {
- return (
-
- There are no datasets that fit these criteria.
-
- );
} else {
- return (
-
- );
+ return ;
}
})()}
@@ -478,14 +280,6 @@ export const DatasetSearchTable = (props) => {
}
- {
- showTranslatedDULModal &&
- setShowTranslatedDULModal(false)}
- />
- }
>
);
};
diff --git a/src/components/data_search/DatasetSearchTableConstants.tsx b/src/components/data_search/DatasetSearchTableConstants.tsx
new file mode 100644
index 000000000..822f20780
--- /dev/null
+++ b/src/components/data_search/DatasetSearchTableConstants.tsx
@@ -0,0 +1,428 @@
+import {DatasetTerm} from 'src/types/model';
+import _, {groupBy} from 'lodash';
+import {Checkbox, Link} from '@mui/material';
+import * as React from 'react';
+import {OverflowTooltip} from '../Tooltips';
+import {SnapshotSummaryModel} from 'src/types/tdrModel';
+import DatasetExportButton from './DatasetExportButton';
+
+export interface DatasetSearchTableTab {
+ key: string;
+ label: string;
+ makeHeaders: (datasets: DatasetTerm[], selected: number[], onSelect: (datasetIds: number[]) => void, exportableDatasets: { [duosId: string]: SnapshotSummaryModel[] }) => HeaderData[];
+ makeRows: (datasets: DatasetTerm[], headers: HeaderData[]) => CellData[][];
+}
+
+interface DatasetSearchTableTabs {
+ study: DatasetSearchTableTab;
+ dataset: DatasetSearchTableTab;
+}
+
+
+
+const makeHeaderStyle = (width: string | number): React.CSSProperties => ({
+ width,
+});
+
+const makeRowStyle = (width: string | number): React.CSSProperties => ({
+ width,
+ textOverflow: 'ellipsis',
+ textWrap: 'nowrap',
+ overflow: 'hidden',
+ paddingRight: 5
+});
+
+const trimNewlineCharacters = (str: string): string => str?.replace( /[\r\n]+/gm, '');
+
+interface CellData{
+ data: string | React.ReactElement;
+ value: string | React.ReactElement;
+ id: string;
+ style?: React.CSSProperties;
+ label: string;
+}
+
+interface HeaderData{
+ label: string | React.ReactElement;
+ sortable: boolean;
+ cellStyle: React.CSSProperties;
+ cellDataFn: (datasets: T) => CellData;
+}
+
+// eslint-disable-next-line no-unused-vars
+export const makeStudyTableHeaders = (datasets: DatasetTerm[], selected: number[], onSelect: (datasetIds: number[]) => void, _exportableDatasets: { [duosId: string]: SnapshotSummaryModel[] }): HeaderData[] => {
+ interface StudyCellWidths{
+ selected: number;
+ studyName: string;
+ description: string;
+ participants: string;
+ phenotype: string;
+ species: string;
+ piName: string;
+ dataCustodians: string;
+ }
+ const studyCellWidths: StudyCellWidths = {
+ selected: 50,
+ // subtract the selected column width
+ studyName: 'calc(25% - 50px)',
+ description: '20%',
+ participants: '10%',
+ phenotype: '15%',
+ species: '10%',
+ piName: '10%',
+ dataCustodians: '20%'
+ };
+ const datasetIds = datasets.map(dataset => dataset.datasetId);
+ return [
+ {
+ label: 0 && selected.length < datasetIds.length}
+ onClick={() => onSelect(datasetIds.length === selected.length ? [] : datasetIds)}/>,
+ sortable: false,
+ cellStyle: makeHeaderStyle(studyCellWidths.selected),
+ cellDataFn: datasets => {
+ const studyDatasetIds = datasets.map(dataset => dataset.datasetId);
+ const numberSelected = _.intersection(studyDatasetIds, selected).length;
+ const fullySelected = numberSelected === studyDatasetIds.length;
+ const indeterminate = numberSelected > 0 && numberSelected < studyDatasetIds.length;
+ return {
+ data: onSelect(fullySelected ? _.without(selected, ...studyDatasetIds) : indeterminate ? _.xor(_.without(selected, ...studyDatasetIds), studyDatasetIds) : [...selected, ...studyDatasetIds])}/>,
+ value: fullySelected ? 'Selected' : indeterminate ? 'Partially Selected' : 'Not Selected',
+ id: `${datasets[0].study.studyId}-is-selected`,
+ style: makeRowStyle(studyCellWidths.selected),
+ label: `Study ${datasets[0].study.studyId} is ${fullySelected ? '' : indeterminate ? 'partially ' : 'not '} selected`
+ };
+ }
+ },
+ {
+ label: 'Study Name',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.studyName),
+ cellDataFn: (datasets) => ({
+ data:
+ {trimNewlineCharacters(datasets[0].study.studyName)}
+ ,
+ value: datasets[0].study.studyName,
+ id: `${datasets[0].study.studyId}-study-name`,
+ style: makeRowStyle(studyCellWidths.selected),
+ label: datasets[0].study.studyName
+ })
+ },
+ {
+ label: 'Description',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.description),
+ cellDataFn: (datasets) => ({
+ data:
+ {trimNewlineCharacters(datasets[0].study.description)}
+ ,
+ value: datasets[0].study.description,
+ id: `${datasets[0].study.studyId}-study-description`,
+ style: makeRowStyle(studyCellWidths.description),
+ label: datasets[0].study.description
+ })
+ },
+ {
+ label: 'Participants',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.participants),
+ cellDataFn: (datasets) => {
+ const participantCount = datasets.map(dataset => dataset.participantCount).reduce((partialSum, participants) => partialSum + participants, 0);
+ return {
+ data: isNaN(participantCount) ? '' : participantCount.toString(),
+ value: participantCount.toString(),
+ id: `${datasets[0].study.studyId}-participant-count`,
+ style: makeRowStyle(studyCellWidths.participants),
+ label: `Participant Count for study ${datasets[0].study.studyId}: ${participantCount}`
+ };
+ }
+ },
+ {
+ label: 'Phenotype',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.phenotype),
+ cellDataFn: (datasets) => ({
+ data:
+ {trimNewlineCharacters(datasets[0].study.phenotype)}
+ ,
+ value: datasets[0].study.phenotype,
+ id: `${datasets[0].study.studyId}-phenotype`,
+ style: makeRowStyle(studyCellWidths.phenotype),
+ label: `Phenotype for study ${datasets[0].study.studyId}: ${datasets[0].study.phenotype}`
+ })
+ },
+ {
+ label: 'Species',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.species),
+ cellDataFn: (datasets) => ({
+ data:
+ {trimNewlineCharacters(datasets[0].study.species)}
+ ,
+ style: makeRowStyle(studyCellWidths.species),
+ value: datasets[0].study.species,
+ id: `${datasets[0].study.studyId}-species`,
+ label: `Species for study ${datasets[0].study.studyId}: ${datasets[0].study.species}`
+ })
+ },
+ {
+ label: 'PI Name',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.piName),
+ cellDataFn: (datasets) => ({
+ data:
+ {trimNewlineCharacters(datasets[0].study.piName)}
+ ,
+ value: datasets[0].study.piName,
+ id: `${datasets[0].study.studyId}-pi-name`,
+ style: makeRowStyle(studyCellWidths.piName),
+ label: `PI for study ${datasets[0].study.studyId}: ${datasets[0].study.piName}`
+ })
+ },
+ {
+ label: 'Data Custodian',
+ sortable: true,
+ cellStyle: makeHeaderStyle(studyCellWidths.dataCustodians),
+ cellDataFn: (datasets) => {
+ const custodians = datasets[0].study.dataCustodianEmail.join(', ');
+ return {
+ data:
+ {custodians}
+ ,
+ value: custodians,
+ id: `${datasets[0].study.studyId}-custodians`,
+ style: makeRowStyle(studyCellWidths.dataCustodians),
+ label: `Data Custodians for study ${datasets[0].study.studyId}: ${custodians}`
+ };
+ }
+ },
+ ];
+};
+
+export const makeStudyTableRowData = (datasets: DatasetTerm[], headers: HeaderData[]) => {
+ const studies = groupBy(datasets, 'study.studyId');
+ return Object.values(studies).map((datasets: DatasetTerm[]) => headers.map(header => header.cellDataFn(datasets)));
+};
+
+export const makeDatasetTableHeader = (datasets: DatasetTerm[], selected: number[], onSelect: (datasetIds: number[]) => void, exportableDatasets: { [duosId: string]: SnapshotSummaryModel[] }): HeaderData[] => {
+ interface CellWidths{
+ selected: number;
+ datasetName: string;
+ studyName: string;
+ duosId: string;
+ accessType: string;
+ dataType: string;
+ donorSize: string;
+ dataLocation: string;
+ dac: string;
+ exportToTerra: number;
+ }
+ const cellWidths: CellWidths = {
+ selected: 50,
+ // subtract the selected column width
+ datasetName: 'calc(25% - 150px)',
+ studyName: '10%',
+ duosId: '10%',
+ accessType: '10%',
+ dataType: '15%',
+ donorSize: '7%',
+ dataLocation: '13%',
+ dac: '10%',
+ exportToTerra: 100,
+ };
+ const datasetIds = datasets.map(dataset => dataset.datasetId);
+ return [
+ {
+ label: 0 && selected.length < datasets.length}
+ onClick={() => onSelect(datasetIds.length === selected.length ? [] : datasetIds)}/>,
+ sortable: false,
+ cellStyle: makeHeaderStyle(cellWidths.selected),
+ cellDataFn: (dataset: DatasetTerm) => {
+ const isSelected = selected.includes(dataset.datasetId);
+ return {
+ data: onSelect(_.xor([dataset.datasetId], selected))}/>,
+ value: isSelected ? 'Selected' : 'Not Selected',
+ id: `${dataset.datasetId}-is-selected`,
+ style: makeRowStyle(cellWidths.selected),
+ label: `Dataset ${dataset.datasetId} is ${isSelected ? '' : 'not '} selected`
+ };
+ }
+ },
+ {
+ label: 'Dataset Name',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.datasetName),
+ cellDataFn: (dataset: DatasetTerm) => ({
+ data:
+ {trimNewlineCharacters(dataset.datasetName)}
+ ,
+ value: dataset.datasetName,
+ id: `${dataset.datasetId}-dataset-name`,
+ style: makeRowStyle(cellWidths.datasetName),
+ label: dataset.datasetName
+ })
+ },
+ {
+ label: 'Study Name',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.studyName),
+ cellDataFn: (dataset: DatasetTerm) => ({
+ data:
+ {trimNewlineCharacters(dataset.study.studyName)}
+ ,
+ value: dataset.study.studyName,
+ id: `${dataset.datasetId}-study-name`,
+ style: makeRowStyle(cellWidths.studyName),
+ label: dataset.study.studyName,
+ })
+ },
+ {
+ label: 'DUOS Id',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.duosId),
+ cellDataFn: (dataset: DatasetTerm) => ({
+ data: {dataset.datasetIdentifier},
+ value: dataset.datasetIdentifier,
+ id: `${dataset.datasetId}-study-description`,
+ style: makeRowStyle(cellWidths.duosId),
+ label: dataset.datasetIdentifier
+ })
+ },
+ {
+ label: 'Access Type',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.accessType),
+ cellDataFn: (dataset: DatasetTerm) => {
+ let accessType;
+ if (dataset.accessManagement === 'external') {
+ accessType = dataset.url ?
+ External to DUOS : 'External to DUOS';
+ } else if (dataset.accessManagement === 'open') {
+ accessType = dataset.url ? Open Access : 'Open Access';
+ } else {
+ accessType = dataset.dac?.dacEmail ? {dataset.dac?.dacName} : dataset.dac?.dacName;
+ }
+ return {
+ data: accessType,
+ value: dataset.accessManagement,
+ id: `${dataset.datasetId}-participant-count`,
+ style: makeRowStyle(cellWidths.accessType),
+ label: `Access Type for dataset ${dataset.datasetId}: ${dataset.accessManagement}`
+ };
+ }
+ },
+ {
+ label: 'Data Type',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.dataType),
+ cellDataFn: (dataset: DatasetTerm) => ({
+ data:
+ {dataset.study.dataTypes?.join(', ')}
+ ,
+ value: dataset.study.dataTypes?.join(', '),
+ id: `${dataset.datasetId}-data-types`,
+ style: makeRowStyle(cellWidths.dataType),
+ label: `Data Types for dataset ${dataset.datasetId}: ${dataset.study.dataTypes?.join(', ')}`
+ })
+ },
+ {
+ label: 'Donor Size',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.donorSize),
+ cellDataFn: (dataset: DatasetTerm) => {
+ const donorSize = isNaN(dataset.participantCount) ? '' : dataset.participantCount.toString();
+ return {
+ data: donorSize,
+ style: makeRowStyle(cellWidths.donorSize),
+ value: donorSize,
+ id: `${dataset.datasetId}-participant-count`,
+ label: `Participant Count for dataset ${dataset.datasetId}: ${donorSize}`
+ };
+ }
+ },
+ {
+ label: 'Data Location',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.dataLocation),
+ cellDataFn: (dataset: DatasetTerm) => {
+ let dataLocation;
+ if (dataset.dataLocation === 'TDR Location') {
+ dataLocation = dataset.url ?
+ Terra Data Repo : 'Terra Data Repo';
+ } else if (dataset.dataLocation === 'Terra Workspace') {
+ dataLocation = dataset.url ?
+ Terra Workspace : 'Terra Data Repo';
+ } else if (dataset.dataLocation === 'Not Determined') {
+ dataLocation = 'Not Determined';
+ } else {
+ dataLocation = dataset.url ?
+ External to DUOS : 'External Location';
+ }
+ return {
+ data: dataLocation,
+ value: dataset.accessManagement,
+ id: `${dataset.datasetId}-participant-count`,
+ style: makeRowStyle(cellWidths.accessType),
+ label: `Access Type for dataset ${dataset.datasetId}: ${dataset.dataLocation}`
+ };
+ }
+ },
+ {
+ label: 'DAC',
+ sortable: true,
+ cellStyle: makeHeaderStyle(cellWidths.dac),
+ cellDataFn: (dataset: DatasetTerm) => ({
+ data:
+ {dataset.dac?.dacName}
+ ,
+ value: dataset.dac?.dacName,
+ id: `${dataset.datasetId}-dac`,
+ style: makeRowStyle(cellWidths.dac),
+ label: `DAC for dataset ${dataset.datasetId}: ${dataset.dac?.dacName}`
+ })
+ },
+ {
+ label: 'Export to Terra',
+ sortable: false,
+ cellStyle: makeHeaderStyle(cellWidths.exportToTerra),
+ cellDataFn: (dataset: DatasetTerm) => {
+ const exportableSnapshots = exportableDatasets[dataset.datasetIdentifier] || [];
+ return {
+ data: <>{exportableSnapshots
+ .map((snapshot, i) =>
+ )}>,
+ value: 'Export to Workspace',
+ id: `${dataset.datasetId}-export-to-terra`,
+ style: makeRowStyle(cellWidths.exportToTerra),
+ label: `Export to Terra for dataset ${dataset.datasetId}`
+ };
+ }
+ }
+ ];
+};
+
+export const makeDatasetTableRows = (datasets: DatasetTerm[], headers: HeaderData[]): CellData[][] => datasets.map(dataset => headers.map(header => header.cellDataFn(dataset)));
+
+
+export const datasetSearchTableTabs: DatasetSearchTableTabs = {
+ study: {
+ key: 'study-table-tab',
+ label: 'View By Studies',
+ makeHeaders: makeStudyTableHeaders,
+ makeRows: makeStudyTableRowData,
+ },
+ dataset: {
+ key: 'datasets-table-tab',
+ label: 'View By Datasets',
+ makeHeaders: makeDatasetTableHeader,
+ makeRows: makeDatasetTableRows,
+ }
+};
diff --git a/src/components/data_search/DatasetSearchTableDisplay.tsx b/src/components/data_search/DatasetSearchTableDisplay.tsx
new file mode 100644
index 000000000..7a420768c
--- /dev/null
+++ b/src/components/data_search/DatasetSearchTableDisplay.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import {Box} from '@mui/material';
+import {isEmpty} from 'lodash';
+import {DatasetTerm} from 'src/types/model';
+import SimpleTable from '../SimpleTable';
+import {Styles} from '../../libs/theme';
+import {
+ DatasetSearchTableTab,
+} from './DatasetSearchTableConstants';
+import {SnapshotSummaryModel} from "src/types/tdrModel";
+
+const styles = {
+ baseStyle: {
+ fontFamily: 'Montserrat',
+ fontSize: '1.4rem',
+ fontWeight: 400,
+ display: 'flex',
+ padding: '1rem 2%',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ whiteSpace: 'pre-wrap',
+ backgroundColor: 'white',
+ border: '1px solid #DEDEDE',
+ borderRadius: '4px',
+ textOverflow: 'ellipsis',
+ height: '4rem',
+ marginTop: 5,
+ },
+ columnStyle: Object.assign({}, Styles.TABLE.HEADER_ROW, {
+ justifyContent: 'space-between',
+ fontFamily: 'Montserrat',
+ fontSize: '1.2rem',
+ fontWeight: 'bold',
+ letterSpacing: '0.2px',
+ backgroundColor: '#E2E8F4',
+ border: 'none',
+ textTransform: 'uppercase',
+ lineHeight: '16px',
+ }),
+ containerOverride: {}
+};
+
+
+interface DatasetSearchTableDisplayProps{
+ onSelect: (newSelectedIds: number[]) => void;
+ filteredData: DatasetTerm[];
+ selected: number[];
+ exportableDatasets: { [duosId: string]: SnapshotSummaryModel[] }
+ tab: DatasetSearchTableTab
+}
+
+export const DatasetSearchTableDisplay = (props: DatasetSearchTableDisplayProps) => {
+ const { onSelect, exportableDatasets, filteredData, selected, tab } = props;
+ const headers = tab.makeHeaders(filteredData, selected, onSelect, exportableDatasets);
+ const rowData = tab.makeRows(filteredData, headers);
+
+ return isEmpty(filteredData) ? (
+
+ There are no datasets that fit these criteria.
+
+ )
+ : (
+
+ );
+};
diff --git a/src/components/modals/TranslatedDulModal.jsx b/src/components/modals/TranslatedDulModal.jsx
deleted file mode 100644
index 3927fc591..000000000
--- a/src/components/modals/TranslatedDulModal.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react';
-import {
- Dialog, DialogTitle, DialogContent, DialogActions, Button, IconButton
-} from '@mui/material';
-import { Close as CloseIcon } from '@mui/icons-material';
-import { useState, useEffect } from 'react';
-import isEmpty from 'lodash/fp/isEmpty';
-
-const listStyle = {
- listStyle: 'none',
- padding: '0',
- fontSize:'1rem'
-};
-
-//NOTE: li partial can be used in components that only need the list
-async function GenerateUseRestrictionStatements(dataUse) {
- if (!dataUse || isEmpty(dataUse)) {
- return (
-
- None
-
- );
- }
-
- return dataUse.map((restriction) => {
- return (
-
- {restriction.code}:
- {restriction.description}
-
- );
- });
-}
-
-export default function TranslatedDulModal(props) {
- const { showModal, onCloseRequest, dataUse } = props;
- const [translatedDulList, setTranslatedDulList] = useState([]);
-
- const closeHandler = () => {
- onCloseRequest();
- };
-
- useEffect(() => {
- const getTranslatedDulList = async () => {
- const dulList = await GenerateUseRestrictionStatements(dataUse || []);
- setTranslatedDulList(dulList);
- };
-
- getTranslatedDulList();
- }, [dataUse]);
-
- return (
-
- );
-}
diff --git a/src/types/model.ts b/src/types/model.ts
index 75eca8992..e320adc5f 100644
--- a/src/types/model.ts
+++ b/src/types/model.ts
@@ -113,6 +113,69 @@ export interface Dataset {
alternativeDataSharingPlanFile: FileStorageObject;
}
+interface DacTerm {
+ dacId: number;
+ dacName: string;
+ dacEmail: string;
+}
+
+interface InstitutionTerm {
+ id: number;
+ name: string;
+}
+
+interface UserTerm {
+ userId: number;
+ displayName: string;
+ institution: InstitutionTerm;
+}
+
+export interface StudyTerm {
+ description: string;
+ studyName: string;
+ studyId: number;
+ phsId: string;
+ phenotype: string;
+ species: string;
+ piName: string;
+ dataSubmitterEmail: string;
+ dataSubmitterId: number;
+ dataCustodianEmail: string[];
+ publicVisibility: boolean;
+ dataTypes: string[];
+}
+
+interface DataUseTerm {
+ code: string;
+ description: string;
+}
+
+interface DataUseSummary {
+ primary: DataUseTerm[];
+ secondary: DataUseTerm[];
+}
+
+export interface DatasetTerm {
+ datasetId: number;
+ createUserId: number;
+ createUserDisplayName: string;
+ datasetIdentifier: string;
+ deletable: boolean;
+ datasetName: string;
+ participantCount: number;
+ dataUse: DataUseSummary;
+ dataLocation: string;
+ url: string;
+ dacId: number;
+ dacApproval: boolean;
+ accessManagement: string;
+ approvedUserIds: number[];
+ study: StudyTerm;
+ submitter: UserTerm;
+ updateUser: UserTerm;
+ dac: DacTerm;
+}
+
interface DataUseRequirements {
required: string[];
}
diff --git a/src/types/tdrModel.ts b/src/types/tdrModel.ts
new file mode 100644
index 000000000..8778ab065
--- /dev/null
+++ b/src/types/tdrModel.ts
@@ -0,0 +1,31 @@
+type CloudPlatform = 'gcp' | 'azure';
+interface StorageResourceModel {
+ region: string;
+ cloudResource: string;
+ cloudPlatform: CloudPlatform;
+}
+
+interface ResourceLocks {
+ exclusive?: string;
+ shared?: string[];
+}
+
+export interface SnapshotSummaryModel {
+ id: string;
+ name: string;
+ description?: string;
+ createdDate?: string;
+ profileId?: string;
+ storage?: StorageResourceModel[];
+ secureMonitoringEnabled?: boolean;
+ consentCode?: string;
+ phsId?: string;
+ cloudPlatform: CloudPlatform;
+ dataProject?: string;
+ storageAccount?: string;
+ selfHosted?: boolean;
+ globalFileIds?: boolean;
+ tags?: string[];
+ resourceLocks: ResourceLocks;
+ duosId: string;
+}