diff --git a/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetails.tsx b/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetails.tsx
index 7019286757..deca01089f 100644
--- a/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetails.tsx
+++ b/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetails.tsx
@@ -13,8 +13,20 @@ function AnalysisDetails({ dagListData }: AnalysisDetails) {
return (
- {ingestPipelines.length > 0 &&
}
- {cwlPipelines.length > 0 &&
}
+ {ingestPipelines.length > 0 && (
+
+ )}
+ {cwlPipelines.length > 0 && (
+
+ )}
);
}
diff --git a/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetailsList.tsx b/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetailsList.tsx
index ee479cadcb..cdd1918a45 100644
--- a/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetailsList.tsx
+++ b/context/app/static/js/components/detailPage/AnalysisDetails/AnalysisDetailsList.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
+import InfoTooltipIcon from 'js/shared-styles/icons/TooltipIcon';
import ProvAnalysisDetailsLink from './AnalysisDetailsLink';
interface ProvAnalysisDetailsListProps {
@@ -11,12 +12,15 @@ interface ProvAnalysisDetailsListProps {
origin: string;
}[];
pipelineType: string;
+ tooltip?: string;
}
-function ProvAnalysisDetailsList({ pipelines, pipelineType }: ProvAnalysisDetailsListProps) {
+function ProvAnalysisDetailsList({ pipelines, pipelineType, tooltip }: ProvAnalysisDetailsListProps) {
return (
<>
- {`${pipelineType} Pipelines`}
+
+ {`${pipelineType} Pipelines`}
+
{pipelines.map((item) => (
-
- Bulk Data Transfer
-
+ Bulk Data Transfer
+
+ This section explains how to download data in bulk from raw and processed datasets. Processed datasets have
+ separate download directories in Globus or dbGaP, distinct from the raw dataset.
+
diff --git a/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.tsx b/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.tsx
index 309b5a3cf3..b13a6e912a 100644
--- a/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.tsx
+++ b/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.tsx
@@ -2,10 +2,10 @@ import React from 'react';
import SectionHeader from 'js/shared-styles/sections/SectionHeader';
import PanelList from 'js/shared-styles/panels/PanelList';
-import { useFlaskDataContext } from 'js/components/Contexts';
import { DetailPageSection } from 'js/components/detailPage/style';
import { buildCollectionsPanelsProps } from 'js/pages/Collections/utils';
import { CollectionHit } from 'js/pages/Collections/types';
+import { SectionDescription } from '../ProcessedData/ProcessedDataset/SectionDescription';
interface CollectionsSectionProps {
collectionsData: CollectionHit[];
@@ -14,15 +14,13 @@ interface CollectionsSectionProps {
function CollectionsSection({ collectionsData }: CollectionsSectionProps) {
const panelsProps = buildCollectionsPanelsProps(collectionsData);
- const {
- entity: { entity_type },
- } = useFlaskDataContext();
-
return (
-
- Collections
-
+ Collections
+
+ Collections may contain references to either raw or processed datasets. If a processed dataset is not included
+ in any collection, there will be no corresponding tabs in the table below.
+
);
diff --git a/context/app/static/js/components/detailPage/DatasetRelationships/nodeTypes.tsx b/context/app/static/js/components/detailPage/DatasetRelationships/nodeTypes.tsx
index 48762cbc60..3af481f18c 100644
--- a/context/app/static/js/components/detailPage/DatasetRelationships/nodeTypes.tsx
+++ b/context/app/static/js/components/detailPage/DatasetRelationships/nodeTypes.tsx
@@ -6,6 +6,7 @@ import Typography from '@mui/material/Typography';
import { AccountTreeRounded, ExtensionRounded, SvgIconComponent } from '@mui/icons-material';
import Skeleton from '@mui/material/Skeleton';
import { Box } from '@mui/system';
+import { formatSectionHash } from 'js/shared-styles/sections/TableOfContents/utils';
import StatusIcon from '../StatusIcon';
import { usePipelineInfo } from './hooks';
@@ -126,7 +127,7 @@ function ProcessedDatasetNode({ data }: NodeProps) {
) {
rounded
target
icon={nodeIcons.componentDataset}
- href={`#${data.name}-section`}
+ href={formatSectionHash(`#section-${data.name}`)}
bgColor={nodeColors.componentDataset}
{...data}
>
diff --git a/context/app/static/js/components/detailPage/DetailLayout/DetailLayout.tsx b/context/app/static/js/components/detailPage/DetailLayout/DetailLayout.tsx
index f13361b9ae..5797219e5c 100644
--- a/context/app/static/js/components/detailPage/DetailLayout/DetailLayout.tsx
+++ b/context/app/static/js/components/detailPage/DetailLayout/DetailLayout.tsx
@@ -5,7 +5,7 @@ import Stack from '@mui/material/Stack';
import useEntityStore, { savedAlertStatus, editedAlertStatus, EntityStore } from 'js/stores/useEntityStore';
import TableOfContents from 'js/shared-styles/sections/TableOfContents';
import { TableOfContentsItems } from 'js/shared-styles/sections/TableOfContents/types';
-import { leftRouteBoundaryID } from 'js/components/Routes/Route/Route';
+import { leftRouteBoundaryID, rightRouteBoundaryID } from 'js/components/Routes/Route/Route';
import { SectionOrder, getSections } from 'js/shared-styles/sections/TableOfContents/utils';
import { StyledAlert } from './style';
@@ -33,6 +33,14 @@ function TableOfContentsPortal({ items, isLoading = false }: { items: TableOfCon
);
}
+export function HelperPanelPortal({ children }: PropsWithChildren) {
+ const element = document.getElementById(rightRouteBoundaryID);
+ if (!element) {
+ return null;
+ }
+ return createPortal(children, element);
+}
+
function DetailAlert() {
const { shouldDisplaySavedOrEditedAlert, setShouldDisplaySavedOrEditedAlert } = useEntityStore(entityStoreSelector);
diff --git a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx
new file mode 100644
index 0000000000..8b4f5bcb02
--- /dev/null
+++ b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx
@@ -0,0 +1,138 @@
+import React, { PropsWithChildren } from 'react';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { formatDate } from 'date-fns/format';
+import Divider from '@mui/material/Divider';
+import Box from '@mui/system/Box';
+import { useIsDesktop } from 'js/hooks/media-queries';
+import SchemaRounded from '@mui/icons-material/SchemaRounded';
+import { WorkspacesIcon } from 'js/shared-styles/icons';
+import { CloudDownloadRounded } from '@mui/icons-material';
+import { useAppContext } from 'js/components/Contexts';
+import { formatSectionHash } from 'js/shared-styles/sections/TableOfContents/utils';
+import { HelperPanelPortal } from '../../DetailLayout/DetailLayout';
+import useProcessedDataStore from '../store';
+import StatusIcon from '../../StatusIcon';
+import { getDateLabelAndValue } from '../../utils';
+import { HelperPanelButton } from './styles';
+
+function useCurrentDataset() {
+ return useProcessedDataStore((state) => state.currentDataset);
+}
+
+function HelperPanelHeader() {
+ const currentDataset = useCurrentDataset();
+ return (
+
+
+ {currentDataset?.hubmap_id}
+
+ );
+}
+
+function HelperPanelStatus() {
+ const currentDataset = useCurrentDataset();
+ if (!currentDataset) {
+ return null;
+ }
+ return (
+
+
+ {currentDataset.status}
+
+ );
+}
+
+interface HelperPanelBodyItemProps extends PropsWithChildren {
+ label: string;
+ noWrap?: boolean;
+}
+
+const noWrapStyles = {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+};
+
+function HelperPanelBodyItem({ label, children, noWrap }: HelperPanelBodyItemProps) {
+ const valueStyles = noWrap ? noWrapStyles : {};
+ return (
+
+ {label}
+
+ {children}
+
+
+ );
+}
+
+function HelperPanelBody() {
+ const currentDataset = useCurrentDataset();
+ if (!currentDataset) {
+ return null;
+ }
+ const [dateLabel, date] = getDateLabelAndValue(currentDataset);
+ return (
+ <>
+ {currentDataset.title && {currentDataset.title}}
+ {currentDataset.description && (
+
+ {currentDataset.description}
+
+ )}
+ {currentDataset.pipeline}
+ {currentDataset.group_name}
+ {date && formatDate(date, 'yyyy-MM-dd')}
+ >
+ );
+}
+
+function HelperPanelActions() {
+ const currentDataset = useCurrentDataset();
+ // TODO: Add workspace actions/dropdown menu
+ const { isWorkspacesUser } = useAppContext();
+ return (
+ <>
+ {isWorkspacesUser && }>Workspace}
+ }
+ href={`#bulk-data-transfer?bulk-data=${currentDataset?.hubmap_id}`}
+ >
+ Bulk Download
+
+ >
+ );
+}
+
+export default function HelperPanel() {
+ const currentDataset = useCurrentDataset();
+ // const panelOffset = useProcessedDataStore((state) => state.currentDatasetOffset);
+ const isDesktop = useIsDesktop();
+ if (!currentDataset || !isDesktop) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/index.ts b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/index.ts
new file mode 100644
index 0000000000..e1fb356fbc
--- /dev/null
+++ b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/index.ts
@@ -0,0 +1,3 @@
+import HelperPanel from './HelperPanel';
+
+export default HelperPanel;
diff --git a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/styles.tsx b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/styles.tsx
new file mode 100644
index 0000000000..75b5b8568d
--- /dev/null
+++ b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/styles.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import { styled } from '@mui/material/styles';
+import Button, { ButtonProps } from '@mui/material/Button';
+
+export const HelperPanelButton = styled((props: ButtonProps) => )(
+ ({ theme }) => ({
+ backgroundColor: 'white',
+ borderRadius: theme.spacing(0.5),
+ whiteSpace: 'nowrap',
+ }),
+);
diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedData.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedData.tsx
index 4b65c92742..b9fec621db 100644
--- a/context/app/static/js/components/detailPage/ProcessedData/ProcessedData.tsx
+++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedData.tsx
@@ -1,13 +1,11 @@
import React from 'react';
import { useProcessedDatasets } from 'js/pages/Dataset/hooks';
import SectionHeader from 'js/shared-styles/sections/SectionHeader';
-import { DetailSectionPaper } from 'js/shared-styles/surfaces';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { InfoIcon } from 'js/shared-styles/icons';
import LabelledSectionText from 'js/shared-styles/sections/LabelledSectionText';
import { DetailPageSection } from '../style';
import ProcessedDataset from './ProcessedDataset';
+import { SectionDescription } from './ProcessedDataset/SectionDescription';
+import HelperPanel from './HelperPanel';
function ProcessedDataSection() {
const processedDatasets = useProcessedDatasets();
@@ -18,20 +16,14 @@ function ProcessedDataSection() {
return (
Processed Data
-
-
-
-
-
- This section contains the results of any additional analyses performed on this dataset. Additional data
- may include visualizations and essential data files (data products). Analysis results could be generated
- from consortium standardized pipelines or by external groups, and may have been generated independently
- from the primary data submitted by the original group.
-
-
- {pipelines.join(', ')}
-
-
+ {pipelines.join(', ')}}
+ >
+ This section contains the results of any additional analyses performed on this dataset. Additional data may
+ include visualizations and essential data files (data products). Analysis results could be generated from
+ consortium standardized pipelines or by external groups, and may have been generated independently from the
+ primary data submitted by the original group.
+
{processedDatasets.searchHits.map((dataset) => (
))}
+
);
}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx
index 8d07b8e8ce..78c72168ea 100644
--- a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx
+++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx
@@ -8,6 +8,7 @@ import FactCheckRounded from '@mui/icons-material/FactCheckRounded';
import SummarizeRounded from '@mui/icons-material/SummarizeRounded';
import InsertDriveFileRounded from '@mui/icons-material/InsertDriveFileRounded';
import { VisualizationIcon } from 'js/shared-styles/icons';
+import { useInView } from 'react-intersection-observer';
import Files from '../../files/Files';
import DataProducts from '../../files/DataProducts';
import VisualizationWrapper from '../../visualization/VisualizationWrapper';
@@ -17,17 +18,16 @@ import { ProcessedDataVisualizationProps } from './types';
import { DatasetTitle } from './DatasetTitle';
import { ProcessedDatasetAccordion } from './ProcessedDatasetAccordion';
import { Subsection } from './Subsection';
+import { SectionDescription } from './SectionDescription';
+import useProcessedDataStore from '../store';
+import { getDateLabelAndValue } from '../../utils';
-function ProcessedDatasetDescription({
- description: _description,
- title,
-}: Pick) {
- const descriptionTitle = _description ? 'Description' : 'Title';
- const description = _description ?? title;
+function ProcessedDatasetDescription({ description, title }: Pick) {
if (!description) {
- return null;
+ return {title};
}
- return {description};
+
+ return {description};
}
function RegisteredBy({
@@ -43,6 +43,7 @@ function RegisteredBy({
}
function SummaryAccordion({ dataset }: Pick) {
+ const [dateLabel, dateValue] = getDateLabelAndValue(dataset);
return (
}>
@@ -51,18 +52,24 @@ function SummaryAccordion({ dataset }: Pick
-
- {dataset.published_timestamp ? formatDate(dataset.published_timestamp, 'yyyy-MM-dd') : 'N/A'}
+
+ {dateValue ? formatDate(new Date(dateValue), 'yyyy-MM-dd') : 'N/A'}
);
}
-function FilesAccordion({ files, id }: Pick & { id: string }) {
+function FilesAccordion({ dataset }: Pick) {
const [openTabIndex, setOpenTabIndex] = useState(0);
-
+ const { files, hubmap_id } = dataset;
+ const id = `files-${hubmap_id}`;
return (
}>
+
+ Files are available for this processed dataset. Essential data products are identified for your convenience, and
+ a comprehensive list of available files is displayed in the file browser. To download data in bulk from either
+ the processed or the primary dataset, navigate to the bulk data transfer section.
+
setOpenTabIndex(newValue as number)}>
@@ -77,20 +84,29 @@ function FilesAccordion({ files, id }: Pick & {
);
}
-function VisualizationAccordion({
- conf,
- dataset: { hubmap_id, uuid },
-}: Pick) {
+function VisualizationAccordion({ conf, dataset }: Pick) {
+ const { hubmap_id, uuid } = dataset;
+
+ const hasBeenSeen = useProcessedDataStore((state) => state.hasBeenSeen(hubmap_id));
+
+ if (!conf) {
+ return null;
+ }
+
return (
}>
-
+
+ This visualization includes various interactive elements such as scatter plots, spatial imaging plots, heat
+ maps, genome browser tracks, and more.
+
+
);
}
function AnalysisDetailsAccordion({ dataset }: Pick) {
return (
- }>
+ }>
{Boolean(dataset.protocol_url) && }
@@ -98,13 +114,28 @@ function AnalysisDetailsAccordion({ dataset }: Pick ({
+ setCurrentDataset: state.setCurrentDataset,
+ }));
+ const { ref } = useInView({
+ threshold: 0.1,
+ initialInView: false,
+ onChange: (visible) => {
+ if (visible) {
+ setCurrentDataset(dataset);
+ }
+ },
+ });
+
return (
-
-
-
-
-
-
-
+
);
}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDatasetAccordion.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDatasetAccordion.tsx
index 8ac65a45f1..336baef3e7 100644
--- a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDatasetAccordion.tsx
+++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDatasetAccordion.tsx
@@ -5,6 +5,7 @@ import Typography from '@mui/material/Typography';
import ArrowDropDownRounded from '@mui/icons-material/ArrowDropDownRounded';
import { VisualizationIcon } from 'js/shared-styles/icons';
import Skeleton from '@mui/material/Skeleton';
+import { formatSectionHash } from 'js/shared-styles/sections/TableOfContents/utils';
import StatusIcon from '../../StatusIcon';
import { ProcessedDataVisualizationProps } from './types';
import { ProcessedDatasetSectionAccordion } from './styles';
@@ -19,7 +20,7 @@ export function ProcessedDatasetAccordion({
}: PropsWithChildren) {
const visualizationIcon = conf ? : null;
return (
-
+
}>
{isLoading ? iconPlaceholder : visualizationIcon}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/SectionDescription.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/SectionDescription.tsx
new file mode 100644
index 0000000000..55eb085217
--- /dev/null
+++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/SectionDescription.tsx
@@ -0,0 +1,27 @@
+import React, { PropsWithChildren } from 'react';
+import { DetailSectionPaper } from 'js/shared-styles/surfaces';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { InfoIcon } from 'js/shared-styles/icons';
+
+interface SectionDescriptionProps extends PropsWithChildren {
+ addendum?: React.ReactNode;
+ subsection?: boolean;
+}
+
+export function SectionDescription({ addendum, children, subsection }: SectionDescriptionProps) {
+ const iconSize = subsection ? '1rem' : '1.5rem';
+ const contents = (
+
+
+
+ {children}
+
+ {addendum}
+
+ );
+ if (subsection) {
+ return contents;
+ }
+ return {contents};
+}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/Subsection.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/Subsection.tsx
index 4389ece5fe..0cef5e89a6 100644
--- a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/Subsection.tsx
+++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/Subsection.tsx
@@ -3,6 +3,7 @@ import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMore from '@mui/icons-material/ExpandMoreRounded';
import Typography from '@mui/material/Typography';
+import { formatSectionHash } from 'js/shared-styles/sections/TableOfContents/utils';
import { SubsectionAccordion } from './styles';
interface SubsectionProps extends PropsWithChildren {
@@ -13,7 +14,7 @@ interface SubsectionProps extends PropsWithChildren {
export function Subsection({ title, icon, id, children }: SubsectionProps) {
return (
-
+
}>
{icon}
diff --git a/context/app/static/js/components/detailPage/ProcessedData/store.ts b/context/app/static/js/components/detailPage/ProcessedData/store.ts
new file mode 100644
index 0000000000..1bc33158ad
--- /dev/null
+++ b/context/app/static/js/components/detailPage/ProcessedData/store.ts
@@ -0,0 +1,39 @@
+import { ProcessedDatasetTypes } from 'js/pages/Dataset/hooks';
+import { create } from 'zustand';
+
+interface ProcessedDataStoreState {
+ seenDatasets: Set;
+ currentDataset: ProcessedDatasetTypes | null;
+ datasetOffsets: Map;
+ currentDatasetOffset: number;
+}
+
+interface ProcessedDataStoreAction {
+ addDataset: (hubmapId: string) => void;
+ setCurrentDataset: (dataset: ProcessedDatasetTypes) => void;
+ hasBeenSeen: (hubmapId: string) => boolean;
+ setDatasetOffset: (hubmapId: string, offset: number) => void;
+}
+
+const defaultState = {
+ seenDatasets: new Set(),
+ currentDataset: null,
+ datasetOffsets: new Map(),
+ currentDatasetOffset: 0,
+};
+
+export type ProcessedDataStore = ProcessedDataStoreState & ProcessedDataStoreAction;
+
+export const useProcessedDataStore = create((set, get) => ({
+ ...defaultState,
+ addDataset: (hubmapId) => set((state) => ({ seenDatasets: new Set([...state.seenDatasets, hubmapId]) })),
+ setCurrentDataset: (dataset) => {
+ const { datasetOffsets, addDataset } = get();
+ addDataset(dataset.hubmap_id);
+ set({ currentDataset: dataset, currentDatasetOffset: datasetOffsets.get(dataset.hubmap_id) ?? 0 });
+ },
+ hasBeenSeen: (hubmapId) => Boolean(get().seenDatasets.has(hubmapId)),
+ setDatasetOffset: (hubmapId, offset) =>
+ set((state) => ({ datasetOffsets: new Map(state.datasetOffsets.set(hubmapId, offset)) })),
+}));
+export default useProcessedDataStore;
diff --git a/context/app/static/js/components/detailPage/provenance/ProvSection/ProvSection.tsx b/context/app/static/js/components/detailPage/provenance/ProvSection/ProvSection.tsx
index 35c90315f1..77f1365355 100644
--- a/context/app/static/js/components/detailPage/provenance/ProvSection/ProvSection.tsx
+++ b/context/app/static/js/components/detailPage/provenance/ProvSection/ProvSection.tsx
@@ -7,6 +7,7 @@ import { DetailPageSection } from 'js/components/detailPage/style';
import Skeleton from '@mui/material/Skeleton';
import useProvData from '../hooks';
import ProvTabs from '../ProvTabs';
+import { SectionDescription } from '../../ProcessedData/ProcessedDataset/SectionDescription';
const provenanceTooltipText = `The provenance shows the sequence of events and actions that led to this page creation.`;
@@ -54,6 +55,11 @@ function ProvSection() {
return (
+
+ The provenance displays the sequence of events and actions that led to the creation of this dataset. The table
+ view provides a basic overview of the dataset's origin, from donor to sample level, and any processing that
+ has been done to the dataset. The graph view offers a comprehensive overview of the data's provenance.
+
);
diff --git a/context/app/static/js/components/detailPage/utils.ts b/context/app/static/js/components/detailPage/utils.ts
index f5a3f6edf7..7c36b7c080 100644
--- a/context/app/static/js/components/detailPage/utils.ts
+++ b/context/app/static/js/components/detailPage/utils.ts
@@ -1,3 +1,5 @@
+import { ProcessedDatasetTypes } from 'js/pages/Dataset/hooks';
+
export function getSectionOrder(
possibleSections: string[],
optionalSectionsToInclude: Record,
@@ -10,3 +12,21 @@ export function getSectionOrder(
export function getCombinedDatasetStatus({ sub_status, status }: { sub_status?: string; status: string }) {
return sub_status ?? status;
}
+
+/**
+ * Helper function to handle different date labels for creation and publication dates
+ * depending on dataset status
+ * @param dataset
+ * @returns [label: string, value: number]
+ */
+export function getDateLabelAndValue(
+ dataset: Pick,
+): [string, number] {
+ const { published_timestamp, created_timestamp, status } = dataset;
+
+ if (status.toLowerCase() === 'published') {
+ return ['Publication Date', published_timestamp];
+ }
+
+ return ['Creation Date', created_timestamp];
+}
diff --git a/context/app/static/js/pages/Dataset/Dataset.tsx b/context/app/static/js/pages/Dataset/Dataset.tsx
index 17ea833313..9f555a7648 100644
--- a/context/app/static/js/pages/Dataset/Dataset.tsx
+++ b/context/app/static/js/pages/Dataset/Dataset.tsx
@@ -38,7 +38,7 @@ import MetadataSection from 'js/components/detailPage/MetadataSection';
import { Dataset, Entity, isDataset, isSupport, Sample, Support } from 'js/components/types';
import DatasetRelationships from 'js/components/detailPage/DatasetRelationships';
import ProcessedDataSection from 'js/components/detailPage/ProcessedData';
-import useDatasetLabel, { useProcessedDatasetsSections } from './hooks';
+import useDatasetLabel, { useLazyLoadedHashHandler, useProcessedDatasetsSections } from './hooks';
function NotebookButton({ disabled, ...props }: { disabled: boolean } & ButtonProps) {
return (
@@ -265,6 +265,7 @@ function DatasetDetail({ assayMetadata, vitData, hasNotebook }: EntityDetailProp
} = assayMetadata;
const isLatest = !('next_revision_uuid' in assayMetadata);
+ useLazyLoadedHashHandler();
const origin_sample = origin_samples[0];
const { mapped_organ } = origin_sample;
diff --git a/context/app/static/js/pages/Dataset/hooks.ts b/context/app/static/js/pages/Dataset/hooks.ts
index 8d563632ab..9c7acbb81f 100644
--- a/context/app/static/js/pages/Dataset/hooks.ts
+++ b/context/app/static/js/pages/Dataset/hooks.ts
@@ -5,9 +5,10 @@ import { useFlaskDataContext } from 'js/components/Contexts';
import { useSearchHits } from 'js/hooks/useSearchData';
import { excludeComponentDatasetsClause, excludeSupportEntitiesClause, getIDsQuery } from 'js/helpers/queries';
import { Dataset, isDataset } from 'js/components/types';
-import { getSectionFromString } from 'js/shared-styles/sections/TableOfContents/utils';
+import { formatSectionHash, getSectionFromString } from 'js/shared-styles/sections/TableOfContents/utils';
import { partialMultiFetcher } from 'js/helpers/swr';
import { TableOfContentsItem } from 'js/shared-styles/sections/TableOfContents/types';
+import { useLayoutEffect } from 'react';
function useDatasetLabelPrefix() {
const {
@@ -49,8 +50,10 @@ export type ProcessedDatasetTypes = Pick<
| 'created_by_user_email'
| 'title'
| 'published_timestamp'
+ | 'created_timestamp'
| 'metadata'
| 'protocol_url' // TODO: This is present for non-dataset entities, but not for datasets.
+ | 'dataset_type'
>;
type VitessceConf = object | null;
@@ -96,9 +99,11 @@ function useProcessedDatasets() {
'created_by_user_email',
'title',
'published_timestamp',
+ 'created_timestamp',
'metadata.dag_provenance_list',
'metadata.metadata',
'protocol_url',
+ 'dataset_type',
],
size: 10000,
};
@@ -129,14 +134,18 @@ function getProcessedDatasetSection({
summary: true,
visualization: Boolean(conf),
files: Boolean(hit?._source?.files),
+ analysis: Boolean(hit?._source?.metadata?.dag_provenance_list),
};
const sectionsToDisplay = Object.entries(shouldDisplaySection).filter(([_k, v]) => v === true);
return {
// TODO: Improve the lookup for descendants to exclude anything with a missing pipeline name
- ...getSectionFromString(pipeline ?? hubmap_id, `${hubmap_id}-section`),
- items: sectionsToDisplay.map(([s]) => ({ ...getSectionFromString(s), hash: `${s}-${hubmap_id}` })),
+ ...getSectionFromString(pipeline ?? hubmap_id, `section-${hubmap_id}`),
+ items: sectionsToDisplay.map(([s]) => ({
+ ...getSectionFromString(s),
+ hash: formatSectionHash(`${s}-${hubmap_id}`),
+ })),
};
}
@@ -157,5 +166,27 @@ function useProcessedDatasetsSections(): { sections: TableOfContentsItem | false
};
}
-export { useProcessedDatasets, useProcessedDatasetsSections };
+function useLazyLoadedHashHandler() {
+ const { isLoading } = useProcessedDatasets();
+ useLayoutEffect(() => {
+ if (!isLoading && window.location.hash) {
+ const { hash } = window.location;
+ const decodedHash = decodeURIComponent(hash);
+
+ if (decodedHash) {
+ // Since `document.querySelector` can fail if the hash is invalid, wrap it in a try-catch block.
+ try {
+ const element = document.querySelector(decodedHash);
+ if (element) {
+ element.scrollIntoView();
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }, [isLoading]);
+}
+
+export { useProcessedDatasets, useProcessedDatasetsSections, useLazyLoadedHashHandler };
export default useDatasetLabel;
diff --git a/context/app/static/js/shared-styles/sections/TableOfContents/utils.ts b/context/app/static/js/shared-styles/sections/TableOfContents/utils.ts
index 29f670aca8..b760d72abb 100644
--- a/context/app/static/js/shared-styles/sections/TableOfContents/utils.ts
+++ b/context/app/static/js/shared-styles/sections/TableOfContents/utils.ts
@@ -2,8 +2,17 @@ import { capitalizeAndReplaceDashes } from 'js/helpers/functions';
import { sectionIconMap } from 'js/shared-styles/icons/sectionIconMap';
import { TableOfContentsItem, TableOfContentsItemWithNode, TableOfContentsItems } from './types';
+function formatSectionHash(hash: string) {
+ const hashWithoutDots = hash.replace(/\./g, '-').toLowerCase();
+ return encodeURIComponent(hashWithoutDots);
+}
+
function getSectionFromString(s: string, hash: string = s): TableOfContentsItem {
- return { text: capitalizeAndReplaceDashes(s), hash: encodeURIComponent(hash), icon: sectionIconMap?.[s] };
+ return {
+ text: capitalizeAndReplaceDashes(s),
+ hash: formatSectionHash(hash),
+ icon: sectionIconMap?.[s],
+ };
}
export type SectionOrder = Record;
@@ -29,4 +38,4 @@ function getItemsClient(items: TableOfContentsItems): TableOfContentsItems props.theme.spacing(2)};
-`;
-
-export { DetailSectionPaper };
diff --git a/context/app/static/js/shared-styles/surfaces/index.ts b/context/app/static/js/shared-styles/surfaces/index.ts
new file mode 100644
index 0000000000..21c2bc4069
--- /dev/null
+++ b/context/app/static/js/shared-styles/surfaces/index.ts
@@ -0,0 +1,8 @@
+import { styled } from '@mui/material/styles';
+import Paper from '@mui/material/Paper';
+
+const DetailSectionPaper = styled(Paper)(({ theme }) => ({
+ padding: theme.spacing(2),
+})) as typeof Paper;
+
+export { DetailSectionPaper };