Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nav scope title + breadcrumbs #248

Merged
merged 33 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d9aa929
feat: experiment with nav scope breadcrumbs
davidlougheed Jan 23, 2025
6508f03
style
davidlougheed Jan 23, 2025
92592a0
styling work for breadcrumbs
davidlougheed Jan 23, 2025
51bf3a7
Merge remote-tracking branch 'origin/main' into feat/experiment-bread…
davidlougheed Jan 24, 2025
4a19587
Merge remote-tracking branch 'origin/main' into feat/experiment-bread…
davidlougheed Feb 6, 2025
dbc4ee3
style: consistent grid layout across application
davidlougheed Feb 7, 2025
e494405
feat: show sub-page and icon in title breadcrumbs
davidlougheed Feb 7, 2025
78de8c0
style: fix beacon network width consistency
davidlougheed Feb 8, 2025
1c4b7b1
chore: remove scope selector from SiteHeader
davidlougheed Feb 10, 2025
532c2f6
feat: scope title bar style tweaks + actions
davidlougheed Feb 10, 2025
d461f42
lint
davidlougheed Feb 10, 2025
8bbdfc1
style: responsiveness
davidlougheed Feb 10, 2025
6598a19
style: truncate project description in scope picker
davidlougheed Feb 10, 2025
4746c02
chore: show Clear instead of Cancel in scope picker with no proj
davidlougheed Feb 10, 2025
6e60791
style: better spacing + flex usage for multiple dataset provenances
davidlougheed Feb 10, 2025
3ad3906
chore: hide page help button if help text is not set
davidlougheed Feb 10, 2025
1a431d3
i18n: overview, provenance help
davidlougheed Feb 10, 2025
babf2b3
style: use small icon buttons for ScopeTitle extra
davidlougheed Feb 10, 2025
c033e3c
i18n: beacon / network page help
davidlougheed Feb 10, 2025
08cea21
i18n: better loading
davidlougheed Feb 10, 2025
2fcf7c3
style: better nested Loader
davidlougheed Feb 10, 2025
965cb8b
style: rm icon from page title to avoid confusion
davidlougheed Feb 11, 2025
bde0ed2
style: refactor container class to separate margin-auto
davidlougheed Feb 12, 2025
8c84b97
style: css variable for content max width
davidlougheed Feb 12, 2025
6f0a252
chore: fix comment wording
davidlougheed Feb 12, 2025
fbc980b
lint: rm dead css
davidlougheed Feb 12, 2025
acbb59f
refact: flip Loader param
davidlougheed Feb 12, 2025
4cb7ace
refact: use CSS variable for header height
davidlougheed Feb 12, 2025
bb86353
refact: content padding CSS variables
davidlougheed Feb 12, 2025
a558cc9
refact: consolidate layout styling into CSS
davidlougheed Feb 12, 2025
b9b9220
refact: rename ScopedTitle + CSS variable
davidlougheed Feb 12, 2025
3274f6d
lint
davidlougheed Feb 12, 2025
8e3890e
style: move site header h1 styling to CSS
davidlougheed Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const BeaconQueryFormUi = ({

// set assembly id options matching what's in gohan (for local beacon) or in network
form.setFieldsValue(formInitialValues);
}, [beaconAssemblyIds.length, form, formInitialValues, isAuthenticated, isNetworkQuery, launchEmptyQuery]);
}, [scopeSet, beaconAssemblyIds.length, form, formInitialValues, isAuthenticated, isNetworkQuery, launchEmptyQuery]);

// Disables max query param if user is authenticated and authorized
useQueryWithAuthIfAllowed();
Expand Down Expand Up @@ -232,12 +232,8 @@ const BeaconQueryFormUi = ({
const searchButtonText = t(`beacon.${isNetworkQuery ? 'search_network' : 'search_beacon'}`);

return (
<div style={{ paddingBottom: 8, display: 'flex', justifyContent: 'center', width: '100%' }}>
<Card
title={t('Search')}
style={{ borderRadius: '10px', maxWidth: '1200px', width: '100%', ...BOX_SHADOW }}
styles={CARD_STYLES}
>
<div className="container" style={{ paddingBottom: 8 }}>
<Card title={t('Search')} style={{ borderRadius: '10px', width: '100%', ...BOX_SHADOW }} styles={CARD_STYLES}>
<p style={{ margin: '-8px 0 8px 0', padding: '0', color: 'grey' }}>{t(uiInstructions)}</p>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Row gutter={FORM_ROW_GUTTERS}>
Expand Down
8 changes: 8 additions & 0 deletions src/js/components/Beacon/BeaconLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ComponentProps } from 'react';
import Icon from '@ant-design/icons';
import BeaconSvg from './BeaconSvg';

type CustomIconComponentProps = ComponentProps<typeof Icon>;

const BeaconLogo = (props: Partial<CustomIconComponentProps>) => <Icon component={BeaconSvg} {...props} />;
export default BeaconLogo;
4 changes: 2 additions & 2 deletions src/js/components/Overview/AboutBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BOX_SHADOW } from '@/constants/overviewConstants';
import { useAppSelector } from '@/hooks';
import { RequestStatus } from '@/types/requests';

const ABOUT_CARD_STYLE: CSSProperties = { width: '100%', maxWidth: '1390px', borderRadius: '11px', ...BOX_SHADOW };
const ABOUT_CARD_STYLE: CSSProperties = { width: '100%', maxWidth: 1320, borderRadius: '11px', ...BOX_SHADOW };

const AboutBox = ({ style, bottomDivider }: { style?: CSSProperties; bottomDivider?: boolean }) => {
const { i18n } = useTranslation();
Expand All @@ -24,7 +24,7 @@ const AboutBox = ({ style, bottomDivider }: { style?: CSSProperties; bottomDivid
<div className="about-content" dangerouslySetInnerHTML={{ __html: aboutContent }} />
)}
</Card>
{bottomDivider && <Divider style={{ maxWidth: 1310, minWidth: 'auto', margin: '32px auto' }} />}
{bottomDivider && <Divider style={{ maxWidth: 1280, minWidth: 'auto', margin: '32px auto' }} />}
</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/js/components/Overview/OverviewChartDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const OverviewChartDashboard = () => {
return WAITING_STATES.includes(overviewDataStatus) ? (
<Loader />
) : (
<>
<div className="container">
<AboutBox />

<Row>
Expand Down Expand Up @@ -102,7 +102,7 @@ const OverviewChartDashboard = () => {
style={MANAGE_CHARTS_BUTTON_STYLE}
onClick={onManageChartsOpen}
/>
</>
</div>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/js/components/Overview/PublicOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const PublicOverview = () => {
// In which case this can be reverted.
return showCatalogue ? (
<>
<AboutBox style={{ maxWidth: 1325, margin: 'auto' }} bottomDivider={true} />
<AboutBox style={{ margin: 'auto' }} bottomDivider={true} />
<Catalogue />
</>
) : (
Expand Down
92 changes: 92 additions & 0 deletions src/js/components/Scope/ScopeTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Breadcrumb, type BreadcrumbProps } from 'antd';
import type { BreadcrumbItemType } from 'antd/es/breadcrumb/Breadcrumb';

import { useSelectedScope, useSelectedScopeTitles } from '@/features/metadata/hooks';
import { getCurrentPage } from '@/utils/router';
import { useGetRouteTitleAndIcon } from '@/hooks/navigation';
import { useTranslationFn } from '@/hooks';
import { BentoRoute } from '@/types/routes';

const breadcrumbRender: BreadcrumbProps['itemRender'] = (route, _params, routes, _paths) => {
const isLast = route?.path === routes[routes.length - 1]?.path;
return isLast || !route.path ? <span>{route.title}</span> : <Link to={{ pathname: route.path }}>{route.title}</Link>;
};

const ScopeTitle = () => {
const { i18n } = useTranslation();
const t = useTranslationFn();
const { scope, fixedProject, fixedDataset } = useSelectedScope();
const { projectTitle, datasetTitle } = useSelectedScopeTitles();

const getRouteTitleAndIcon = useGetRouteTitleAndIcon();
const currentPage = getCurrentPage();

const breadcrumbItems: BreadcrumbItemType[] = useMemo(() => {
const currentTitleAndIcon = getRouteTitleAndIcon(currentPage);
const currentPageTitle = (
<>
{currentTitleAndIcon[1]} {t(currentTitleAndIcon[0])}
</>
);

const items: BreadcrumbItemType[] = [];

if (scope.project && !fixedProject) {
items.push({
title: projectTitle,
path: `/${i18n.language}/p/${scope.project}`,
});

if (scope.dataset && !fixedDataset) {
items.push({
title: datasetTitle,
path: `/${i18n.language}/p/${scope.project}/d/${scope.dataset}`,
});
}
}

if (currentPage !== BentoRoute.Overview) {
items.push({ title: currentPageTitle });
}

return items;
}, [
i18n.language,
t,
projectTitle,
datasetTitle,
scope,
fixedProject,
fixedDataset,
currentPage,
getRouteTitleAndIcon,
]);

useEffect(() => {
if (!breadcrumbItems.length) return;

const observer = new IntersectionObserver(
([e]) => {
return e.target.toggleAttribute('data-stuck', e.intersectionRatio < 1);
},
{ threshold: [1], root: document.getElementById('content-layout') }
);

const sb = document.querySelector('.scope-breadcrumb');

if (sb) {
observer.observe(sb);
}
}, [breadcrumbItems]);

if (breadcrumbItems.length) {
return <Breadcrumb className="scope-breadcrumb" items={breadcrumbItems} itemRender={breadcrumbRender} />;
}

return null;
};

export default ScopeTitle;
2 changes: 1 addition & 1 deletion src/js/components/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const RoutedSearch = () => {
const WIDTH_100P_STYLE = { width: '100%' };
const SEARCH_SPACE_ITEM_STYLE = { item: WIDTH_100P_STYLE };
const SEARCH_SECTION_SPACE_ITEM_STYLE = { item: { display: 'flex', justifyContent: 'center' } };
const SEARCH_SECTION_STYLE = { maxWidth: 1200 };
const SEARCH_SECTION_STYLE = { maxWidth: 1320 };

const Search = () => {
const t = useTranslationFn();
Expand Down
7 changes: 3 additions & 4 deletions src/js/components/Search/SearchResultsPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ const SearchResultsPane = ({
);

return (
<div className="search-results-pane">
<div className="container search-results-pane">
<Card
style={{
borderRadius: '10px',
maxWidth: '1200px',
width: '100%',
// Set a minimum height (i.e., an expected final height, which can be exceeded) to prevent this component from
// suddenly increasing in height after it loads. This is calculated from the sum of the following parts:
Expand Down Expand Up @@ -87,7 +86,7 @@ const SearchResultsPane = ({
{panePage === 'charts' ? (
<>
<Col xs={24} lg={10}>
<Typography.Title level={5} style={{ marginTop: 0 }}>
<Typography.Title level={5} style={{ marginTop: 0, textAlign: 'center' }}>
{t('entities.biosample', T_PLURAL_COUNT)}
</Typography.Title>
{!hasInsufficientData && biosampleChartData.length ? (
Expand All @@ -97,7 +96,7 @@ const SearchResultsPane = ({
)}
</Col>
<Col xs={24} lg={10}>
<Typography.Title level={5} style={{ marginTop: 0 }}>
<Typography.Title level={5} style={{ marginTop: 0, textAlign: 'center' }}>
{t('entities.experiment', T_PLURAL_COUNT)}
</Typography.Title>
{!hasInsufficientData && experimentChartData.length ? (
Expand Down
18 changes: 9 additions & 9 deletions src/js/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ const SiteHeader = () => {
>
{CLIENT_NAME}
</Typography.Title>
{scopeSelectionEnabled && (
<Typography.Title className="select-project-title" level={2} onClick={() => setIsModalOpen(true)}>
<ProfileOutlined style={{ marginRight: '5px', fontSize: '16px' }} />

{scopeObj.project && scopeProps.projectTitle}
{scopeProps.datasetTitle ? ` / ${scopeProps.datasetTitle}` : ''}
</Typography.Title>
)}
<ScopePickerModal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
{/*{scopeSelectionEnabled && (*/}
{/* <Typography.Title className="select-project-title" level={2} onClick={() => setIsModalOpen(true)}>*/}
{/* <ProfileOutlined style={{ marginRight: '5px', fontSize: '16px' }} />*/}

{/* {scopeObj.project && scopeProps.projectTitle}*/}
{/* {scopeProps.datasetTitle ? ` / ${scopeProps.datasetTitle}` : ''}*/}
{/* </Typography.Title>*/}
{/*)}*/}
{/*<ScopePickerModal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />*/}
</Space>

<Space size={isSmallScreen ? 0 : 'small'}>
Expand Down
59 changes: 22 additions & 37 deletions src/js/components/SiteSider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,21 @@ import { useLocation, useNavigate } from 'react-router-dom';

import type { MenuProps, SiderProps } from 'antd';
import { Button, Divider, Layout, Menu } from 'antd';
import Icon, {
ArrowLeftOutlined,
BookOutlined,
PieChartOutlined,
SearchOutlined,
ShareAltOutlined,
SolutionOutlined,
} from '@ant-design/icons';

import BeaconSvg from '@/components/Beacon/BeaconSvg';
import { ArrowLeftOutlined } from '@ant-design/icons';

import { useSelectedScope } from '@/features/metadata/hooks';
import { useSearchQuery } from '@/features/search/hooks';
import { useTranslationFn } from '@/hooks';
import { useIsInCatalogueMode, useNavigateToRoot } from '@/hooks/navigation';
import { useGetRouteTitleAndIcon, useIsInCatalogueMode, useNavigateToRoot } from '@/hooks/navigation';
import { BentoRoute, TOP_LEVEL_ONLY_ROUTES } from '@/types/routes';
import { buildQueryParamsUrl } from '@/utils/search';
import { getCurrentPage } from '@/utils/router';

const { Sider } = Layout;

type CustomIconComponentProps = React.ComponentProps<typeof Icon>;
type MenuItem = Required<MenuProps>['items'][number];
type OnClick = MenuProps['onClick'];

const BeaconLogo = (props: Partial<CustomIconComponentProps>) => <Icon component={BeaconSvg} {...props} />;

const SiteSider = ({ collapsed, setCollapsed }: { collapsed: boolean; setCollapsed: SiderProps['onCollapse'] }) => {
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -40,10 +29,6 @@ const SiteSider = ({ collapsed, setCollapsed }: { collapsed: boolean; setCollaps
const catalogueMode = useIsInCatalogueMode();
const currentPage = getCurrentPage();

// Use location for catalogue page detection instead of selectedProject, since it gives us faster UI rendering at the
// cost of only being wrong with a redirect edge case (and being slightly more brittle).
const overviewIsCatalogue = !location.pathname.includes('/p/') && catalogueMode;

const navigateToRoot = useNavigateToRoot();
const { fixedProject, scope, scopeSet } = useSelectedScope();

Expand All @@ -68,7 +53,7 @@ const SiteSider = ({ collapsed, setCollapsed }: { collapsed: boolean; setCollaps
);

const createMenuItem = useCallback(
(label: string, key: string, icon?: React.ReactNode, children?: MenuItem[]): MenuItem => ({
(key: string, label: string, icon?: React.ReactNode, children?: MenuItem[]): MenuItem => ({
key,
icon,
children,
Expand All @@ -77,31 +62,29 @@ const SiteSider = ({ collapsed, setCollapsed }: { collapsed: boolean; setCollaps
[t]
);

const getRouteTitleAndIcon = useGetRouteTitleAndIcon();

const menuItems: MenuItem[] = useMemo(() => {
const items = [
createMenuItem(
overviewIsCatalogue ? 'Catalogue' : 'Overview',
BentoRoute.Overview,
overviewIsCatalogue ? <BookOutlined /> : <PieChartOutlined />
),
createMenuItem('Search', BentoRoute.Search, <SearchOutlined />),
createMenuItem(BentoRoute.Overview, ...getRouteTitleAndIcon(BentoRoute.Overview)),
createMenuItem(BentoRoute.Search, ...getRouteTitleAndIcon(BentoRoute.Search)),
];

if (scope.project) {
// Only show provenance if we're not at the top level, since the giant list of context-less datasets is confusing.
items.push(createMenuItem('Provenance', BentoRoute.Provenance, <SolutionOutlined />));
items.push(createMenuItem(BentoRoute.Provenance, ...getRouteTitleAndIcon(BentoRoute.Provenance)));
}

if (BentoRoute.Beacon) {
items.push(createMenuItem('Beacon', BentoRoute.Beacon, <BeaconLogo />));
items.push(createMenuItem(BentoRoute.Beacon, ...getRouteTitleAndIcon(BentoRoute.Beacon)));
}

if (BentoRoute.BeaconNetwork && (!scope.project || (scope.project && fixedProject))) {
items.push(createMenuItem('Beacon Network', BentoRoute.BeaconNetwork, <ShareAltOutlined />));
items.push(createMenuItem(BentoRoute.BeaconNetwork, ...getRouteTitleAndIcon(BentoRoute.BeaconNetwork)));
}

return items;
}, [createMenuItem, scope, fixedProject, overviewIsCatalogue]);
}, [getRouteTitleAndIcon, createMenuItem, scope, fixedProject]);

return (
<Sider
Expand All @@ -122,14 +105,16 @@ const SiteSider = ({ collapsed, setCollapsed }: { collapsed: boolean; setCollaps
>
{scope.project && catalogueMode && (
<>
<Button
type="text"
icon={<ArrowLeftOutlined />}
style={{ margin: 4, width: 'calc(100% - 8px)' }}
onClick={scope.dataset ? () => navigate(`/${i18n.language}/p/${scope.project}`) : navigateToRoot}
>
{collapsed || !scopeSet ? null : t(scope.dataset ? 'Back to project' : 'Back to catalogue')}
</Button>
<div style={{ backgroundColor: '#FAFAFA' }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
style={{ margin: 4, width: 'calc(100% - 8px)' }}
onClick={scope.dataset ? () => navigate(`/${i18n.language}/p/${scope.project}`) : navigateToRoot}
>
{collapsed || !scopeSet ? null : t(scope.dataset ? 'Back to project' : 'Back to catalogue')}
</Button>
</div>
<Divider style={{ margin: 0 }} />
</>
)}
Expand Down
17 changes: 14 additions & 3 deletions src/js/components/Util/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,36 @@ import { Layout } from 'antd';
import SiteHeader from '@/components/SiteHeader';
import SiteSider from '@/components/SiteSider';
import SiteFooter from '@/components/SiteFooter';
import ScopeTitle from '@/components/Scope/ScopeTitle';

const { Content } = Layout;

const DefaultLayout = () => {
const [collapsed, setCollapsed] = useState(false);

const sidebarWidth = collapsed ? '80px' : '200px';

return (
<Layout style={{ minHeight: '100vh' }}>
<SiteHeader />
<Layout>
<SiteSider collapsed={collapsed} setCollapsed={setCollapsed} />
<Layout
id="content-layout"
style={{
marginLeft: collapsed ? '80px' : '200px',
transition: 'margin-left 0.3s',
marginTop: '64px',
position: 'fixed',
top: 64,
right: 0,
bottom: 0,
left: sidebarWidth,
transition: 'left 0.3s',
width: `calc(100% - ${sidebarWidth})`,
overflow: 'auto',
display: 'block',
}}
>
<Content style={{ padding: '32px 64px' }}>
<ScopeTitle />
<Outlet />
</Content>
<SiteFooter />
Expand Down
Loading
Loading