Skip to content

Commit

Permalink
NickAkhmetov/Unified Views Fixes (CAT-847, CAT-849, CAT-853, CAT-850,…
Browse files Browse the repository at this point in the history
… CAT-857, CAT-855, CAT-848, CAT-851) (#3519)

Co-authored-by: John Conroy <[email protected]>
  • Loading branch information
NickAkhmetov and john-conroy authored Aug 28, 2024
1 parent cb41e15 commit b662d76
Show file tree
Hide file tree
Showing 21 changed files with 134 additions and 67 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG-unified-views-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- Prevent helper panel overflow from long titles/descriptions.
- Prevent lifting of publication ancillary data on dataset pages.
- Remove excessive margin on section descriptions.
- Indicate disabled state on entity header items.
- Add tooltip to bulk download shortcut link.
- Restore assay graphs for datasets with no defined search terms but with present datasets.
- Remove duplicate HuBMAP ID from bulk data transfer panel links.
- Fix single-tab table styles so the single tab appears selected.
- Add tooltips to unlabeled status icons.
- Remove link from helper panel dataset ID display.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import NoAccess from './NoAccess';
import { usePanelSet } from './usePanelSet';
import { useStudyURLsQuery } from './hooks';
import GlobusLink from './GlobusLink';
import { sanitizeLabel } from './utils';

interface BulkDataTransferPanelProps {
uuid: string;
Expand All @@ -26,11 +27,7 @@ function BulkDataTransferPanels({ uuid, label }: BulkDataTransferPanelProps) {

const { showDbGaP, showGlobus, showSRA, panels } = panelsToUse;

// This is a logical OR and should remain as such.
// If `showDbGaP` is false, we still want to check if `showGlobus` or `showSRA` are true.
// If we use `??` instead of `||`, the expression would only check if `showDbGaP` is false.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasLinks = Boolean(showDbGaP || showGlobus || showSRA);
const hasLinks = Boolean(showDbGaP ?? showGlobus ?? showSRA);

return (
<Stack gap={1}>
Expand All @@ -39,7 +36,7 @@ function BulkDataTransferPanels({ uuid, label }: BulkDataTransferPanelProps) {
))}
{hasLinks && (
<Paper>
{showGlobus && <GlobusLink uuid={uuid} hubmap_id={hubmap_id} label={label} />}
{showGlobus && <GlobusLink uuid={uuid} hubmap_id={hubmap_id} label={sanitizeLabel(label)} />}
{showDbGaP && <Link href={dbgap_study_url} title="dbGaP" />}
{showSRA && <Link href={dbgap_sra_experiment_url} title="SRA Experiment" />}
</Paper>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { sanitizeLabel } from './utils';

describe('sanitizeLabel', () => {
it('should not change any labels that do not contain square brackets', () => {
const label = 'Some label';
expect(sanitizeLabel(label)).toBe(label);
});
it('should remove the HuBMAP ID from the label', () => {
const label = 'Some label [HuBMAP-123]';
expect(sanitizeLabel(label)).toBe('Some label');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ function getDUAText(mapped_data_access_level: string) {
}
}

export { getDUAText };
// Removes the HuBMAP ID from the label (present if there is more than one dataset with the same pipeline and status)
function sanitizeLabel(label: string) {
return label.split(' [')[0];
}

export { getDUAText, sanitizeLabel };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';

import PanelList from 'js/shared-styles/panels/PanelList';
import { useFlaskDataContext } from 'js/components/Contexts';
Expand All @@ -9,6 +9,7 @@ import { Tabs, Tab, TabPanel } from 'js/shared-styles/tables/TableTabs';
import { OutlinedAlert } from 'js/shared-styles/alerts/OutlinedAlert.stories';
import withShouldDisplay from 'js/helpers/withShouldDisplay';
import { sectionIconMap } from 'js/shared-styles/icons/sectionIconMap';
import { useTabs } from 'js/shared-styles/tabs';
import { useProcessedDatasetTabs } from '../ProcessedData/ProcessedDataset/hooks';
import { SectionDescription } from '../ProcessedData/ProcessedDataset/SectionDescription';
import CollectionsSectionProvider, { useCollectionsSectionContext } from './CollectionsSectionContext';
Expand Down Expand Up @@ -45,7 +46,7 @@ function CollectionTab({ label, uuid, index, icon: Icon }: CollectionTabProps) {
);
}

function CollectionPanel({ uuid, index }: { uuid: string; index: number }) {
function CollectionPanel({ uuid, index, value }: { uuid: string; index: number; value: number }) {
const collectionsData = useDatasetsCollections([uuid]);
const { setProcessedDatasetHasCollections } = useCollectionsSectionContext();
const {
Expand All @@ -64,15 +65,15 @@ function CollectionPanel({ uuid, index }: { uuid: string; index: number }) {
if (panelsProps.length === 0) {
if (uuid === primaryDatasetId) {
return (
<TabPanel value={index} index={index}>
<TabPanel value={value} index={index}>
<OutlinedAlert severity="info">The raw dataset is not referenced in any existing collections.</OutlinedAlert>
</TabPanel>
);
}
return null;
}
return (
<TabPanel value={index} index={index}>
<TabPanel value={value} index={index}>
<PanelList panelsProps={panelsProps} key={index} />
</TabPanel>
);
Expand All @@ -84,19 +85,19 @@ const collectionsSectionDescription =
function CollectionsSection() {
const processedDatasetTabs = useProcessedDatasetTabs();

const [selectedTab, setSelectedTab] = useState(0);
const { openTabIndex, handleTabChange } = useTabs();

return (
<CollapsibleDetailPageSection id="collections" title="Collections" icon={sectionIconMap.collections}>
<CollectionsSectionProvider>
<SectionDescription>{collectionsSectionDescription}</SectionDescription>
<Tabs value={selectedTab} onChange={(_, tabIndex) => setSelectedTab(tabIndex as number)}>
<Tabs value={openTabIndex} onChange={handleTabChange} aria-label="Dataset collections">
{processedDatasetTabs.map(({ label, uuid, icon }, index) => (
<CollectionTab key={uuid} label={label} uuid={uuid} index={index} icon={icon} />
))}
</Tabs>
{processedDatasetTabs.map(({ uuid }, index) => (
<CollectionPanel key={uuid} uuid={uuid} index={index} />
<CollectionPanel key={uuid} uuid={uuid} index={index} value={openTabIndex} />
))}
</CollectionsSectionProvider>
</CollapsibleDetailPageSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function NodeTemplate({
<Stack direction="row" gap={1} my="auto" alignItems="center">
{Icon && <Icon color="primary" fontSize="1.5rem" width="1.5rem" height="1.5rem" />}
<Typography variant="subtitle2">{isLoading ? <Skeleton variant="text" width="10rem" /> : name}</Typography>
{status && <StatusIcon status={status} />}
{status && <StatusIcon status={status} tooltip />}
</Stack>
{children && <Typography variant="body2">{children}</Typography>}
{target && <Handle style={{ opacity: 0 }} type="target" position={Position.Left} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import SchemaRounded from '@mui/icons-material/SchemaRounded';
import { WorkspacesIcon } from 'js/shared-styles/icons';
import CloudDownloadRounded from '@mui/icons-material/CloudDownloadRounded';
import { useAppContext } from 'js/components/Contexts';
import { formatSectionHash } from 'js/shared-styles/sections/TableOfContents/utils';
import { useAnimatedSidebarPosition } from 'js/shared-styles/sections/TableOfContents/hooks';
import { animated } from '@react-spring/web';
import { useEventCallback } from '@mui/material/utils';
Expand All @@ -23,6 +22,7 @@ import SelectableTableProvider from 'js/shared-styles/tables/SelectableTableProv
import AddDatasetsFromSearchDialog from 'js/components/workspaces/AddDatasetsFromSearchDialog';
import { LineClamp } from 'js/shared-styles/text';
import Fade from '@mui/material/Fade';
import { SecondaryBackgroundTooltip } from 'js/shared-styles/tooltips';
import { HelperPanelPortal } from '../../DetailLayout/DetailLayout';
import useProcessedDataStore from '../store';
import StatusIcon from '../../StatusIcon';
Expand All @@ -42,7 +42,7 @@ function HelperPanelHeader() {
return (
<Typography variant="subtitle2" display="flex" alignItems="center" gap={0.5} whiteSpace="nowrap">
<SchemaRounded fontSize="small" />
<a href={`#${formatSectionHash(`section-${currentDataset?.hubmap_id}`)}`}>{currentDataset?.hubmap_id}</a>
{currentDataset?.hubmap_id}
</Typography>
);
}
Expand All @@ -66,7 +66,7 @@ interface HelperPanelBodyItemProps extends PropsWithChildren {
}

function HelperPanelBodyItem({ label, children, noWrap }: HelperPanelBodyItemProps) {
const body = noWrap ? children : <LineClamp lines={3}>{children}</LineClamp>;
const body = noWrap ? <LineClamp lines={3}>{children}</LineClamp> : children;
return (
<Stack direction="column">
<Typography variant="overline">{label}</Typography>
Expand Down Expand Up @@ -202,18 +202,20 @@ function HelperPanelActions() {
return (
<>
<WorkspaceButton />
<HelperPanelButton
startIcon={<CloudDownloadRounded />}
href="#bulk-data-transfer"
onClick={() => {
track({
action: 'Navigate to Bulk Download',
label: currentDataset?.hubmap_id,
});
}}
>
Bulk Download
</HelperPanelButton>
<SecondaryBackgroundTooltip title="Scroll down to the Bulk Data Transfer Section.">
<HelperPanelButton
startIcon={<CloudDownloadRounded />}
href="#bulk-data-transfer"
onClick={() => {
track({
action: 'Navigate to Bulk Download',
label: currentDataset?.hubmap_id,
});
}}
>
Bulk Download
</HelperPanelButton>
</SecondaryBackgroundTooltip>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';
import React, { ForwardedRef, forwardRef } from 'react';

import { styled } from '@mui/material/styles';
import Button, { ButtonProps } from '@mui/material/Button';

export const HelperPanelButton = styled((props: ButtonProps) => <Button {...props} variant="outlined" />)(
({ theme }) => ({
backgroundColor: 'white',
borderRadius: theme.spacing(0.5),
whiteSpace: 'nowrap',
}),
);
const OutlinedButton = forwardRef((props: ButtonProps, ref: ForwardedRef<HTMLButtonElement>) => (
<Button ref={ref} {...props} variant="outlined" />
));

export const HelperPanelButton = styled(OutlinedButton)(({ theme }) => ({
backgroundColor: 'white',
borderRadius: theme.spacing(0.5),
whiteSpace: 'nowrap',
}));
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function DatasetTitle() {
const track = useTrackEntityPageEvent();
return (
<Typography variant="h5" display="flex" alignItems="center" gap={0.5}>
<StatusIcon status={status} />
<StatusIcon status={status} tooltip />
{hubmap_id}
<SecondaryBackgroundTooltip title="Copy HuBMAP ID">
<IconButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function ProcessedDatasetAccordion({ children }: PropsWithChildren) {
{sectionDataset.pipeline}
</Typography>
<Typography variant="body1" ml="auto" component="div" display="flex" alignItems="center" gap={1}>
<StatusIcon status={sectionDataset.status} noColor sx={{ fontSize: 16 }} />
<StatusIcon status={sectionDataset.status} noColor tooltip />
{dataset?.hubmap_id}
</Typography>
</AccordionSummary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface SectionDescriptionProps extends PropsWithChildren {
export function SectionDescription({ addendum, children, subsection }: SectionDescriptionProps) {
const iconSize = subsection ? '1rem' : '1.5rem';
const contents = (
<Stack direction="column" gap={1} marginBottom={2}>
<Stack direction="column" gap={1} marginBottom={subsection ? 2 : 0}>
<Stack direction="row" gap={1} alignItems="start">
<InfoIcon color="primary" fontSize={iconSize} />
<Typography variant="body1">{children}</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ function ColoredStatusIcon({
return (
<SeverityIcon
status={status}
sx={{ fontSize: 16, marginRight: '3px', alignSelf: 'center', color: noColor ? 'white' : undefined }}
noColor={noColor}
sx={({ spacing }) => ({ fontSize: '1rem', marginRight: spacing(1), alignSelf: 'center' })}
{...props}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import React, { forwardRef } from 'react';
import { SvgIconProps } from '@mui/material/SvgIcon';
import { SecondaryBackgroundTooltip } from 'js/shared-styles/tooltips';
import Box from '@mui/material/Box';
import ColoredStatusIcon from './ColoredStatusIcon';

function getColor(status: string) {
Expand All @@ -26,13 +28,30 @@ function getColor(status: string) {
interface StatusIconProps extends SvgIconProps {
status: string;
noColor?: boolean;
tooltip?: boolean;
}

function StatusIcon({ status: irregularCaseStatus, noColor, ...props }: StatusIconProps) {
const StatusIcon = forwardRef(function StatusIcon(
{ status: irregularCaseStatus, noColor, tooltip, ...props }: StatusIconProps,
ref: React.Ref<SVGSVGElement>,
) {
const status = irregularCaseStatus.toUpperCase();
const color = getColor(status);

return <ColoredStatusIcon status={color} noColor={noColor} data-testid="status-svg-icon" {...props} />;
}
const content = (
<ColoredStatusIcon ref={ref} status={color} noColor={noColor} data-testid="status-svg-icon" {...props} />
);

if (tooltip) {
return (
<SecondaryBackgroundTooltip title={irregularCaseStatus}>
{/* The wrapper is required for the tooltip to work */}
<Box display="flex">{content}</Box>
</SecondaryBackgroundTooltip>
);
}

return content;
});

export default StatusIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function ActionButton<E extends ElementType = IconButtonTypeMap['defaultComponen
}: { icon: typeof SvgIcon | React.ComponentType<SvgIconProps> } & TooltipButtonProps<E>) {
return (
<TooltipIconButton {...rest}>
<Icon color="primary" fontSize="1.5rem" />
<Icon color={rest.disabled ? 'disabled' : 'primary'} fontSize="1.5rem" />
</TooltipIconButton>
);
}
Expand Down Expand Up @@ -85,8 +85,8 @@ function EditSavedEntityButton({ entity_type, uuid }: Pick<Entity, 'uuid'> & { e
);
}

function WorkspaceSVGIcon(props: SvgIconProps) {
return <SvgIcon component={WorkspacesIcon} color="primary" {...props} />;
function WorkspaceSVGIcon({ color = 'primary', ...props }: SvgIconProps) {
return <SvgIcon component={WorkspacesIcon} color={color} {...props} />;
}

function CreateWorkspaceButton({
Expand All @@ -108,7 +108,7 @@ function CreateWorkspaceButton({
setDialogIsOpen(true);
}}
icon={WorkspaceSVGIcon}
tooltip={disabled ? 'Protected datasets are not available in Workspaces.' : 'Launch a new workspace.'}
tooltip={disabled ? 'Protected datasets are not available in workspaces.' : 'Launch a new workspace.'}
disabled={disabled}
/>
<NewWorkspaceDialog {...rest} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { includeOnlyDatasetsClause } from 'js/helpers/queries';

export const getAncestorsQuery = (uuid: string) => ({
query: {
bool: {
must: [
{
term: {
'entity_type.keyword': 'Dataset',
},
},
includeOnlyDatasetsClause,
{
term: {
'processing.keyword': 'raw',
Expand Down
12 changes: 12 additions & 0 deletions context/app/static/js/helpers/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ export const excludeSupportEntitiesClause: QueryDslQueryContainer = {
},
};

export const includeDatasetsAndImageSupports: QueryDslQueryContainer = {
bool: {
should: [
{
terms: { entity_type: ['Dataset', 'Support'] },
},
{ terms: { 'vitessce-hints': ['pyramid', 'is_image'] } },
],
minimum_should_match: 1,
},
};

export const excludeComponentDatasetsClause: QueryDslQueryContainer = {
bool: {
must_not: {
Expand Down
6 changes: 3 additions & 3 deletions context/app/static/js/pages/Dataset/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import useSWR, { useSWRConfig } from 'swr';

import { useAppContext, useFlaskDataContext } from 'js/components/Contexts';
import { useSearchHits } from 'js/hooks/useSearchData';
import { excludeComponentDatasetsClause, getIDsQuery } from 'js/helpers/queries';
import { excludeComponentDatasetsClause, includeDatasetsAndImageSupports, getIDsQuery } from 'js/helpers/queries';
import { Dataset, isDataset } from 'js/components/types';
import { getSectionFromString } from 'js/shared-styles/sections/TableOfContents/utils';
import { fetcher } from 'js/helpers/swr';
Expand Down Expand Up @@ -87,8 +87,8 @@ function useProcessedDatasets(includeComponents?: boolean) {
bool: {
// TODO: Futher narrow once we understand EPICs.
must: includeComponents
? [getIDsQuery(descendant_ids)]
: [getIDsQuery(descendant_ids), excludeComponentDatasetsClause],
? [getIDsQuery(descendant_ids), includeDatasetsAndImageSupports]
: [getIDsQuery(descendant_ids), includeDatasetsAndImageSupports, excludeComponentDatasetsClause],
},
},
_source: [
Expand Down
Loading

0 comments on commit b662d76

Please sign in to comment.