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: UI selector for project/dataset #171

Merged
merged 29 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ffef748
migrated previous code
SanjeevLakhwani Jul 3, 2024
354e897
removed redundant state variables
SanjeevLakhwani Jul 3, 2024
7549508
reorganized redux state
SanjeevLakhwani Jul 15, 2024
b15000f
corrected app load sequence
SanjeevLakhwani Jul 17, 2024
3eb72db
added routing for projects and datasets
SanjeevLakhwani Jul 22, 2024
df495da
improved index.tsx
SanjeevLakhwani Jul 25, 2024
0561f84
provided correct navigation for chart clicks
SanjeevLakhwani Jul 25, 2024
829df07
corrected allow clear button
SanjeevLakhwani Jul 25, 2024
9854b2e
removed redundant log
SanjeevLakhwani Jul 25, 2024
be24708
modified UI
SanjeevLakhwani Jul 29, 2024
b7b039a
Added dataset title
SanjeevLakhwani Jul 29, 2024
a55cd51
corrected token refresh import url
SanjeevLakhwani Aug 5, 2024
4600d38
removed React.FC
SanjeevLakhwani Aug 5, 2024
6e035c4
updated modal UI
SanjeevLakhwani Aug 5, 2024
922b637
changed db icon to svg
SanjeevLakhwani Aug 5, 2024
5e40e99
fixed compile issues with rebase
SanjeevLakhwani Aug 8, 2024
7233115
fixed index url redirect
SanjeevLakhwani Aug 8, 2024
772afed
Added transitions to on hover effects to match antd design
SanjeevLakhwani Aug 8, 2024
73adcdb
PR css changes
SanjeevLakhwani Aug 8, 2024
9200f5d
added translations
SanjeevLakhwani Aug 8, 2024
a58619c
removed stray )
SanjeevLakhwani Aug 19, 2024
1194e93
added handling for null edge case
SanjeevLakhwani Aug 19, 2024
d7a7c6e
Added type for selectedProject useState
SanjeevLakhwani Aug 19, 2024
ab86cfa
Typed Discovery
SanjeevLakhwani Aug 22, 2024
ed3101e
Removed React.FC
SanjeevLakhwani Aug 22, 2024
cee5e06
provide null option for type
SanjeevLakhwani Aug 22, 2024
86a634d
Removed React.useState
SanjeevLakhwani Aug 22, 2024
923d2e0
organized imports
SanjeevLakhwani Aug 22, 2024
514fae4
enabled same page routing for ChooseProjectModal.tsx
SanjeevLakhwani Aug 26, 2024
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
79 changes: 70 additions & 9 deletions src/js/components/BentoAppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { Routes, Route } from 'react-router-dom';
import { Routes, Route, useNavigate, useParams, Outlet } from 'react-router-dom';
import { useAutoAuthenticate, useIsAuthenticated } from 'bento-auth-js';
import { useAppDispatch } from '@/hooks';
import { useAppDispatch, useAppSelector } from '@/hooks';

import { makeGetConfigRequest, makeGetServiceInfoRequest } from '@/features/config/config.store';
import { makeGetAboutRequest } from '@/features/content/content.store';
Expand All @@ -11,19 +11,56 @@ import { makeGetProvenanceRequest } from '@/features/provenance/provenance.store
import { getBeaconConfig } from '@/features/beacon/beaconConfig.store';
import { fetchGohanData, fetchKatsuData } from '@/features/ingestion/lastIngestion.store';
import { makeGetDataTypes } from '@/features/dataTypes/dataTypes.store';
import { getProjects, selectScope } from '@/features/metadata/metadata.store';

import PublicOverview from './Overview/PublicOverview';
import Search from './Search/Search';
import ProvenanceTab from './Provenance/ProvenanceTab';
import BeaconQueryUi from './Beacon/BeaconQueryUi';
import { BentoRoute } from '@/types/routes';
import Loader from '@/components/Loader';
import { validProjectDataset } from '@/utils/router';
import DefaultLayout from '@/components/Util/DefaultLayout';

const ScopedRoute = () => {
const { projectId, datasetId } = useParams();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { projects } = useAppSelector((state) => state.metadata);

useEffect(() => {
// Update selectedScope based on URL parameters
const valid = validProjectDataset(projects, projectId, datasetId);
if (datasetId === valid.dataset && projectId === valid.project) {
dispatch(selectScope(valid));
} else {
const oldpath = location.pathname.split('/').filter(Boolean);
const newPath = [oldpath[0]];

if (valid.dataset) {
newPath.push('p', valid.project as string, 'd', valid.dataset);
} else if (valid.project) {
newPath.push('p', valid.project);
}

const oldPathLength = oldpath.length;
if (oldpath[oldPathLength - 3] === 'p' || oldpath[oldPathLength - 3] === 'd') {
newPath.push(oldpath[oldPathLength - 1]);
}
const newPathString = '/' + newPath.join('/');
navigate(newPathString, { replace: true });
}
}, [projects, projectId, datasetId, dispatch]);

return <Outlet />;
};

const BentoAppRouter = () => {
const dispatch = useAppDispatch();

const { isAutoAuthenticating } = useAutoAuthenticate();
const isAuthenticated = useIsAuthenticated();
const { selectedScope, isFetching: isFetchingProjects } = useAppSelector((state) => state.metadata);

useEffect(() => {
dispatch(makeGetConfigRequest()).then(() => dispatch(getBeaconConfig()));
Expand All @@ -33,25 +70,49 @@ const BentoAppRouter = () => {
dispatch(makeGetProvenanceRequest());
dispatch(makeGetKatsuPublic());
dispatch(fetchKatsuData());
}, [selectedScope]);

useEffect(() => {
dispatch(getProjects());
dispatch(makeGetAboutRequest());
dispatch(fetchGohanData());
dispatch(makeGetServiceInfoRequest());

if (isAuthenticated) {
dispatch(makeGetDataTypes());
}
}, [isAuthenticated]);

if (isAutoAuthenticating) {
if (isAutoAuthenticating || isFetchingProjects) {
return <Loader />;
}

return (
<Routes>
<Route path={`/${BentoRoute.Overview}`} element={<PublicOverview />} />
<Route path={`/${BentoRoute.Search}/*`} element={<Search />} />
<Route path={`/${BentoRoute.Beacon}/*`} element={<BeaconQueryUi />} />
<Route path={`/${BentoRoute.Provenance}/*`} element={<ProvenanceTab />} />
<Route path="/*" element={<PublicOverview />} />
<Route element={<DefaultLayout />}>
<Route path="/" element={<ScopedRoute />}>
<Route index element={<PublicOverview />} />
<Route path={BentoRoute.Overview} element={<PublicOverview />} />
<Route path={BentoRoute.Search} element={<Search />} />
<Route path={BentoRoute.Beacon} element={<BeaconQueryUi />} />
<Route path={BentoRoute.Provenance} element={<ProvenanceTab />} />
</Route>

<Route path="/p/:projectId" element={<ScopedRoute />}>
<Route index element={<PublicOverview />} />
<Route path={BentoRoute.Overview} element={<PublicOverview />} />
<Route path={BentoRoute.Search} element={<Search />} />
<Route path={BentoRoute.Beacon} element={<BeaconQueryUi />} />
<Route path={BentoRoute.Provenance} element={<ProvenanceTab />} />
</Route>

<Route path="/p/:projectId/d/:datasetId" element={<ScopedRoute />}>
<Route index element={<PublicOverview />} />
<Route path={BentoRoute.Overview} element={<PublicOverview />} />
<Route path={BentoRoute.Search} element={<Search />} />
<Route path={BentoRoute.Beacon} element={<BeaconQueryUi />} />
<Route path={BentoRoute.Provenance} element={<ProvenanceTab />} />
</Route>
</Route>
</Routes>
);
};
Expand Down
82 changes: 82 additions & 0 deletions src/js/components/ChooseProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Tabs, List, Avatar, Modal, Button, Space, Typography } from 'antd';
import { useAppSelector, useTranslationCustom, useTranslationDefault } from '@/hooks';
import { Link, useLocation } from 'react-router-dom';
import { FaDatabase } from 'react-icons/fa';
import { getCurrentPage } from '@/utils/router';

const ChooseProjectModal = ({ isModalOpen, setIsModalOpen }: ChooseProjectModalProps) => {
const td = useTranslationDefault();
const t = useTranslationCustom();
const location = useLocation();
const { projects, selectedScope } = useAppSelector((state) => state.metadata);
const [selectedProject, setSelectedProject] = useState<string | undefined>(
selectedScope.project ?? projects[0]?.identifier ?? undefined
);

const baseURL = '/' + location.pathname.split('/')[1];
const page = getCurrentPage();

const closeModal = () => setIsModalOpen(false);

return (
<Modal title={td('Select Scope')} open={isModalOpen} onCancel={closeModal} footer={null} width={800}>
<Tabs
tabPosition="left"
activeKey={selectedProject}
onChange={(key) => setSelectedProject(key)}
tabBarExtraContent={
<Link to={baseURL}>
<Button>{td('Clear')}</Button>
</Link>
}
items={projects.map(({ identifier, title, datasets, description }) => {
SanjeevLakhwani marked this conversation as resolved.
Show resolved Hide resolved
return {
key: identifier,
label: t(title),
children: (
<Space direction="vertical">
<Space align="baseline" size="large">
<Typography.Title level={4} className="no-margin-top">
{td('About')} {t(title)}
</Typography.Title>
<Link to={`${baseURL}/p/${selectedProject}/${page}`} key="3">
<Typography.Link>{td('Select')}</Typography.Link>
</Link>
</Space>
<Typography.Text>{t(description)}</Typography.Text>
<Typography.Title level={5} className="no-margin-top">
{td('Datasets')}
</Typography.Title>
<List
dataSource={datasets}
bordered
renderItem={(item) => (
<Link to={`${baseURL}/p/${identifier}/d/${item.identifier}/${page}`}>
<List.Item className="select-dataset-hover" key={item.identifier}>
<List.Item.Meta
avatar={<Avatar style={{ backgroundColor: '#33ccff' }} icon={<FaDatabase />} />}
title={
<Link to={`${baseURL}/p/${identifier}/d/${item.identifier}/${page}`}>{t(item.title)}</Link>
}
description={t(item.description)}
/>
</List.Item>
</Link>
)}
/>
</Space>
),
};
})}
/>
</Modal>
);
};

interface ChooseProjectModalProps {
isModalOpen: boolean;
setIsModalOpen: (isOpen: boolean) => void;
}

export default ChooseProjectModal;
9 changes: 6 additions & 3 deletions src/js/components/Overview/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import {
CHART_TYPE_PIE,
ChartConfig,
} from '@/types/chartConfig';
import { useAppSelector } from '@/hooks';
import { scopeToUrl } from '@/utils/router';

const Chart = memo(({ chartConfig, data, units, id, isClickable }: ChartProps) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { selectedScope } = useAppSelector((state) => state.metadata);
const translateMap = ({ x, y }: { x: string; y: number }) => ({ x: t(x), y });
const removeMissing = ({ x }: { x: string }) => x !== 'missing';
const barChartOnClickHandler = (d: { payload: { x: string } }) => {
navigate(`/${i18n.language}/search?${id}=${d.payload.x}`);
navigate(`/${i18n.language}${scopeToUrl(selectedScope)}/search?${id}=${d.payload.x}`);
};
const pieChartOnClickHandler = (d: { name: string }) => {
navigate(`/${i18n.language}/search?${id}=${d.name}`);
navigate(`/${i18n.language}${scopeToUrl(selectedScope)}/search?${id}=${d.name}`);
};

const { chart_type: type } = chartConfig;
Expand Down Expand Up @@ -58,7 +61,7 @@ const Chart = memo(({ chartConfig, data, units, id, isClickable }: ChartProps) =
height={PIE_CHART_HEIGHT}
preFilter={removeMissing}
dataMap={translateMap}
{...(isClickable ? { onClick: pieChartOnClickHandler } : {})}
onClick={pieChartOnClickHandler}
/>
);
case CHART_TYPE_CHOROPLETH: {
Expand Down
55 changes: 45 additions & 10 deletions src/js/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';

import { Button, Flex, Layout, Typography, Space } from 'antd';
const { Header } = Layout;
import { useTranslation } from 'react-i18next';
import { DEFAULT_TRANSLATION, LNG_CHANGE, LNGS_FULL_NAMES } from '@/constants/configConstants';
import { useAppSelector } from '@/hooks';
import { useNavigate, useLocation } from 'react-router-dom';
import { useIsAuthenticated, usePerformAuth, usePerformSignOut } from 'bento-auth-js';
import { CLIENT_NAME, PORTAL_URL, TRANSLATED } from '@/config';

import { RiTranslate } from 'react-icons/ri';
import { ExportOutlined, LinkOutlined, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { ExportOutlined, LinkOutlined, LoginOutlined, LogoutOutlined, ProfileOutlined } from '@ant-design/icons';
SanjeevLakhwani marked this conversation as resolved.
Show resolved Hide resolved

import { useAppSelector } from '@/hooks';
import { scopeToUrl } from '@/utils/router';

import { DEFAULT_TRANSLATION, LNG_CHANGE, LNGS_FULL_NAMES } from '@/constants/configConstants';
import { CLIENT_NAME, PORTAL_URL, TRANSLATED } from '@/config';

import ChooseProjectModal from '@/components/ChooseProjectModal';

const { Header } = Layout;

const openPortalWindow = () => window.open(PORTAL_URL, '_blank');

Expand All @@ -19,6 +27,22 @@ const SiteHeader = () => {

const { isFetching: openIdConfigFetching } = useAppSelector((state) => state.openIdConfiguration);
const { isHandingOffCodeForToken } = useAppSelector((state) => state.auth);
const { projects, selectedScope } = useAppSelector((state) => state.metadata);

const scopeProps = {
davidlougheed marked this conversation as resolved.
Show resolved Hide resolved
projectTitle: projects.find((project) => project.identifier === selectedScope.project)?.title,
datasetTitle: selectedScope.dataset
? projects
.find((project) => project.identifier === selectedScope.project)
?.datasets.find((dataset) => dataset.identifier === selectedScope.dataset)?.title
: null,
};

const [isModalOpen, setIsModalOpen] = useState(false);

useEffect(() => {
setIsModalOpen(false);
}, [location]);

const isAuthenticated = useIsAuthenticated();
const performSignOut = usePerformSignOut();
Expand All @@ -40,16 +64,27 @@ const SiteHeader = () => {
src="/public/assets/branding.png"
alt="logo"
style={{ height: '32px', verticalAlign: 'middle', transform: 'translateY(-3px)' }}
onClick={() => navigate('/')}
onClick={() => navigate(`/${i18n.language}${scopeToUrl(selectedScope)}`)}
/>

<Typography.Title
level={1}
style={{ fontSize: '18px', margin: 0, lineHeight: '64px', color: 'white' }}
style={{ fontSize: '24px', margin: 0, lineHeight: '64px', color: 'white' }}
type="secondary"
>
{CLIENT_NAME}
</Typography.Title>
<Typography.Title
className="select-project-title"
level={5}
style={{ fontSize: '16px', margin: 0, lineHeight: '64px', color: 'lightgray' }}
onClick={() => setIsModalOpen(true)}
>
<ProfileOutlined style={{ marginRight: '5px', fontSize: '16px' }} />

{selectedScope.project && scopeProps.projectTitle}
{scopeProps.datasetTitle ? ` / ${scopeProps.datasetTitle}` : ''}
</Typography.Title>
<ChooseProjectModal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
</Space>

<Space size="small">
Expand Down
15 changes: 11 additions & 4 deletions src/js/components/SiteSider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ const SiteSider: React.FC<{

const handleMenuClick: OnClick = useCallback(
({ key }: { key: string }) => {
const currentPath = location.pathname.split('/');
const currentLang = currentPath[1];
const newPath = `/${currentLang}/${key === BentoRoute.Overview ? '' : key}`;
navigate(key === BentoRoute.Search ? buildQueryParamsUrl(newPath, queryParams) : newPath);
const currentPath = location.pathname.split('/').filter(Boolean);
const newPath = [currentPath[0]];
if (currentPath[1] == 'p') {
newPath.push('p', currentPath[2]);
}
if (currentPath[3] == 'd') {
newPath.push('d', currentPath[4]);
}
newPath.push(key);
const newPathString = '/' + newPath.join('/');
navigate(key === BentoRoute.Search ? buildQueryParamsUrl(newPathString, queryParams) : newPathString);
},
[navigate, queryParams, location.pathname]
);
Expand Down
Loading