Skip to content

Commit

Permalink
Merge pull request #171 from bento-platform/features/new-discovery
Browse files Browse the repository at this point in the history
feat: UI selector for project/dataset
  • Loading branch information
SanjeevLakhwani authored Aug 26, 2024
2 parents eb79d1e + 514fae4 commit 289ee8d
Show file tree
Hide file tree
Showing 21 changed files with 590 additions and 189 deletions.
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 }) => {
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';

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 = {
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

0 comments on commit 289ee8d

Please sign in to comment.