diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx
index 67ab9e5d..300b1fba 100644
--- a/src/js/components/BentoAppRouter.tsx
+++ b/src/js/components/BentoAppRouter.tsx
@@ -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';
@@ -11,6 +11,7 @@ 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';
@@ -18,12 +19,48 @@ 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 ;
+};
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()));
@@ -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 ;
}
return (
- } />
- } />
- } />
- } />
- } />
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
);
};
diff --git a/src/js/components/ChooseProjectModal.tsx b/src/js/components/ChooseProjectModal.tsx
new file mode 100644
index 00000000..0f77f076
--- /dev/null
+++ b/src/js/components/ChooseProjectModal.tsx
@@ -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(
+ selectedScope.project ?? projects[0]?.identifier ?? undefined
+ );
+
+ const baseURL = '/' + location.pathname.split('/')[1];
+ const page = getCurrentPage();
+
+ const closeModal = () => setIsModalOpen(false);
+
+ return (
+
+ setSelectedProject(key)}
+ tabBarExtraContent={
+
+
+
+ }
+ items={projects.map(({ identifier, title, datasets, description }) => {
+ return {
+ key: identifier,
+ label: t(title),
+ children: (
+
+
+
+ {td('About')} {t(title)}
+
+
+ {td('Select')}
+
+
+ {t(description)}
+
+ {td('Datasets')}
+
+ (
+
+
+ } />}
+ title={
+ {t(item.title)}
+ }
+ description={t(item.description)}
+ />
+
+
+ )}
+ />
+
+ ),
+ };
+ })}
+ />
+
+ );
+};
+
+interface ChooseProjectModalProps {
+ isModalOpen: boolean;
+ setIsModalOpen: (isOpen: boolean) => void;
+}
+
+export default ChooseProjectModal;
diff --git a/src/js/components/Overview/Chart.tsx b/src/js/components/Overview/Chart.tsx
index 18e54561..f8a729c9 100644
--- a/src/js/components/Overview/Chart.tsx
+++ b/src/js/components/Overview/Chart.tsx
@@ -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;
@@ -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: {
diff --git a/src/js/components/SiteHeader.tsx b/src/js/components/SiteHeader.tsx
index 1534869a..3105c4f8 100644
--- a/src/js/components/SiteHeader.tsx
+++ b/src/js/components/SiteHeader.tsx
@@ -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');
@@ -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();
@@ -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)}`)}
/>
-
{CLIENT_NAME}
+ setIsModalOpen(true)}
+ >
+
+
+ {selectedScope.project && scopeProps.projectTitle}
+ {scopeProps.datasetTitle ? ` / ${scopeProps.datasetTitle}` : ''}
+
+
diff --git a/src/js/components/SiteSider.tsx b/src/js/components/SiteSider.tsx
index 76f91679..8de43c5b 100644
--- a/src/js/components/SiteSider.tsx
+++ b/src/js/components/SiteSider.tsx
@@ -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]
);
diff --git a/src/js/components/Util/AuthOutlet.tsx b/src/js/components/Util/AuthOutlet.tsx
new file mode 100644
index 00000000..f464bf33
--- /dev/null
+++ b/src/js/components/Util/AuthOutlet.tsx
@@ -0,0 +1,74 @@
+import React, { useRef, useState } from 'react';
+import { useQueryWithAuthIfAllowed, useTranslationDefault } from '@/hooks';
+import {
+ checkIsInAuthPopup,
+ nop,
+ useHandleCallback,
+ useOpenSignInWindowCallback,
+ usePopupOpenerAuthCallback,
+ useSessionWorkerTokenRefresh,
+ useSignInPopupTokenHandoff,
+} from 'bento-auth-js';
+import { PUBLIC_URL_NO_TRAILING_SLASH } from '@/config';
+import { Button, message, Modal } from 'antd';
+import { Outlet } from 'react-router-dom';
+
+const SIGN_IN_WINDOW_FEATURES = 'scrollbars=no, toolbar=no, menubar=no, width=800, height=600';
+const CALLBACK_PATH = '/callback';
+
+const createSessionWorker = () => new Worker(new URL('../../workers/tokenRefresh.ts', import.meta.url));
+
+const AuthOutlet = () => {
+ const t = useTranslationDefault();
+
+ // AUTHENTICATION
+ const [signedOutModal, setSignedOutModal] = useState(false);
+
+ const sessionWorker = useRef(null);
+ const signInWindow = useRef(null);
+ const windowMessageHandler = useRef<((event: MessageEvent) => void) | null>(null);
+
+ const openSignInWindow = useOpenSignInWindowCallback(signInWindow, SIGN_IN_WINDOW_FEATURES);
+
+ const popupOpenerAuthCallback = usePopupOpenerAuthCallback();
+ const isInAuthPopup = checkIsInAuthPopup(PUBLIC_URL_NO_TRAILING_SLASH);
+
+ useHandleCallback(
+ CALLBACK_PATH,
+ () => {
+ console.debug('authenticated');
+ },
+ isInAuthPopup ? popupOpenerAuthCallback : undefined,
+ (msg) => message.error(msg)
+ );
+
+ // Set up message handling from sign-in popup
+ useSignInPopupTokenHandoff(windowMessageHandler);
+
+ useSessionWorkerTokenRefresh(sessionWorker, createSessionWorker, nop);
+
+ useQueryWithAuthIfAllowed();
+
+ return (
+ <>
+ {
+ setSignedOutModal(false);
+ }}
+ open={signedOutModal}
+ footer={[
+ ,
+ ]}
+ >
+ {t('Please sign in to the research portal.')}
+
+
+
+ >
+ );
+};
+
+export default AuthOutlet;
diff --git a/src/js/components/Util/DefaultLayout.tsx b/src/js/components/Util/DefaultLayout.tsx
new file mode 100644
index 00000000..37d0fe8a
--- /dev/null
+++ b/src/js/components/Util/DefaultLayout.tsx
@@ -0,0 +1,35 @@
+import React, { useState } from 'react';
+import { Outlet } from 'react-router-dom';
+import { Layout } from 'antd';
+import SiteHeader from '@/components/SiteHeader';
+import SiteSider from '@/components/SiteSider';
+import SiteFooter from '@/components/SiteFooter';
+
+const { Content } = Layout;
+
+const DefaultLayout = () => {
+ const [collapsed, setCollapsed] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DefaultLayout;
diff --git a/src/js/components/Util/LanguageHandler.tsx b/src/js/components/Util/LanguageHandler.tsx
new file mode 100644
index 00000000..b478040e
--- /dev/null
+++ b/src/js/components/Util/LanguageHandler.tsx
@@ -0,0 +1,33 @@
+import React, { useEffect } from 'react';
+import { Outlet, useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { DEFAULT_TRANSLATION, SUPPORTED_LNGS } from '@/constants/configConstants';
+
+const LNGS_ARRAY = Object.values(SUPPORTED_LNGS);
+
+const LanguageHandler = () => {
+ const { lang } = useParams<{ lang?: string }>();
+ const navigate = useNavigate();
+ const { i18n } = useTranslation(DEFAULT_TRANSLATION);
+
+ useEffect(() => {
+ const setLanguage = (newLang: string) => {
+ i18n.changeLanguage(newLang);
+ if (lang !== newLang) {
+ navigate(`/${newLang}/`, { replace: true });
+ }
+ };
+
+ if (lang && LNGS_ARRAY.includes(lang)) {
+ setLanguage(lang);
+ } else {
+ const browserLang = navigator.language.split('-')[0];
+ const defaultLang = LNGS_ARRAY.includes(browserLang) ? browserLang : SUPPORTED_LNGS.ENGLISH;
+ setLanguage(defaultLang);
+ }
+ }, [lang, i18n, navigate]);
+
+ return ;
+};
+
+export default LanguageHandler;
diff --git a/src/js/constants/configConstants.ts b/src/js/constants/configConstants.ts
index 2d3cb501..c304ddd1 100644
--- a/src/js/constants/configConstants.ts
+++ b/src/js/constants/configConstants.ts
@@ -7,6 +7,7 @@ export const katsuPublicRulesUrl = `${PORTAL_URL}/api/metadata/api/public_rules`
export const searchFieldsUrl = `${PORTAL_URL}/api/metadata/api/public_search_fields`;
export const katsuUrl = `${PORTAL_URL}/api/metadata/api/public`;
export const provenanceUrl = `${PORTAL_URL}/api/metadata/api/public_dataset`;
+export const projectsUrl = `${PORTAL_URL}/api/metadata/api/projects`;
export const katsuLastIngestionsUrl = '/katsu/data-types';
export const gohanLastIngestionsUrl = '/gohan/data-types';
diff --git a/src/js/features/config/config.store.ts b/src/js/features/config/config.store.ts
index 6fdbd6eb..6b6a5311 100644
--- a/src/js/features/config/config.store.ts
+++ b/src/js/features/config/config.store.ts
@@ -7,14 +7,14 @@ import { RootState } from '@/store';
import { PUBLIC_URL } from '@/config';
import { DiscoveryRules } from '@/types/configResponse';
-export const makeGetConfigRequest = createAsyncThunk(
+export const makeGetConfigRequest = createAsyncThunk(
'config/getConfigData',
- (_, { rejectWithValue }) =>
- // TODO: should be project/dataset scoped with url params
- axios
- .get(katsuPublicRulesUrl)
+ (_, { rejectWithValue, getState }) => {
+ return axios
+ .get(katsuPublicRulesUrl, { params: getState().metadata.selectedScope })
.then((res) => res.data)
- .catch(printAPIError(rejectWithValue))
+ .catch(printAPIError(rejectWithValue));
+ }
);
export const makeGetServiceInfoRequest = createAsyncThunk<
diff --git a/src/js/features/data/makeGetDataRequest.thunk.ts b/src/js/features/data/makeGetDataRequest.thunk.ts
index 9e5022a0..327b2747 100644
--- a/src/js/features/data/makeGetDataRequest.thunk.ts
+++ b/src/js/features/data/makeGetDataRequest.thunk.ts
@@ -10,14 +10,15 @@ import { ChartConfig } from '@/types/chartConfig';
import { ChartDataField, LocalStorageData, Sections } from '@/types/data';
import { Counts, OverviewResponse } from '@/types/overviewResponse';
import { printAPIError } from '@/utils/error.util';
+import { RootState } from '@/store';
export const makeGetDataRequestThunk = createAsyncThunk<
{ sectionData: Sections; counts: Counts; defaultData: Sections },
void,
- { rejectValue: string }
->('data/makeGetDataRequest', async (_, { rejectWithValue }) => {
+ { rejectValue: string; state: RootState }
+>('data/makeGetDataRequest', async (_, { rejectWithValue, getState }) => {
const overviewResponse = (await axios
- .get(katsuPublicOverviewUrl)
+ .get(katsuPublicOverviewUrl, { params: getState().metadata.selectedScope })
.then((res) => res.data)
.catch(printAPIError(rejectWithValue))) as OverviewResponse['overview'];
diff --git a/src/js/features/metadata/metadata.store.ts b/src/js/features/metadata/metadata.store.ts
new file mode 100644
index 00000000..1261dcf0
--- /dev/null
+++ b/src/js/features/metadata/metadata.store.ts
@@ -0,0 +1,61 @@
+import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
+import axios from 'axios';
+import { PaginatedResponse, Project } from '@/types/metadata';
+import { RootState } from '@/store';
+import { printAPIError } from '@/utils/error.util';
+import { validProjectDataset } from '@/utils/router';
+import { projectsUrl } from '@/constants/configConstants';
+
+export interface MetadataState {
+ projects: Project[];
+ isFetching: boolean;
+ selectedScope: {
+ project: string | undefined;
+ dataset: string | undefined;
+ };
+}
+
+const initialState: MetadataState = {
+ projects: [],
+ isFetching: true,
+ selectedScope: {
+ project: undefined,
+ dataset: undefined,
+ },
+};
+
+export const getProjects = createAsyncThunk<
+ PaginatedResponse,
+ void,
+ { state: RootState; rejectValue: string }
+>('metadata/getProjects', (_, { rejectWithValue }) => {
+ return axios
+ .get(projectsUrl)
+ .then((res) => res.data)
+ .catch(printAPIError(rejectWithValue));
+});
+
+const metadata = createSlice({
+ name: 'metadata',
+ initialState,
+ reducers: {
+ selectScope: (state, { payload }: PayloadAction<{ project?: string; dataset?: string }>) => {
+ state.selectedScope = validProjectDataset(state.projects, payload.project, payload.dataset);
+ },
+ },
+ extraReducers(builder) {
+ builder.addCase(getProjects.pending, (state) => {
+ state.isFetching = true;
+ });
+ builder.addCase(getProjects.fulfilled, (state, { payload }) => {
+ state.projects = payload?.results ?? [];
+ state.isFetching = false;
+ });
+ builder.addCase(getProjects.rejected, (state) => {
+ state.isFetching = false;
+ });
+ },
+});
+
+export const { selectScope } = metadata.actions;
+export default metadata.reducer;
diff --git a/src/js/features/search/makeGetSearchFields.thunk.ts b/src/js/features/search/makeGetSearchFields.thunk.ts
index 6d9135d1..81a21a2a 100644
--- a/src/js/features/search/makeGetSearchFields.thunk.ts
+++ b/src/js/features/search/makeGetSearchFields.thunk.ts
@@ -3,12 +3,15 @@ import axios from 'axios';
import { searchFieldsUrl } from '@/constants/configConstants';
import { printAPIError } from '@/utils/error.util';
import { SearchFieldResponse } from '@/types/search';
+import { RootState } from '@/store';
-export const makeGetSearchFields = createAsyncThunk(
- 'query/makeGetSearchFields',
- (_, { rejectWithValue }) =>
- axios
- .get(searchFieldsUrl)
- .then((res) => res.data)
- .catch(printAPIError(rejectWithValue))
-);
+export const makeGetSearchFields = createAsyncThunk<
+ SearchFieldResponse,
+ void,
+ { rejectValue: string; state: RootState }
+>('query/makeGetSearchFields', async (_, { rejectWithValue, getState }) => {
+ return await axios
+ .get(searchFieldsUrl, { params: getState().metadata.selectedScope })
+ .then((res) => res.data)
+ .catch(printAPIError(rejectWithValue));
+});
diff --git a/src/js/index.tsx b/src/js/index.tsx
index 21a556cc..5fadcb20 100644
--- a/src/js/index.tsx
+++ b/src/js/index.tsx
@@ -1,37 +1,26 @@
// React and ReactDOM imports
-import React, { Suspense, useEffect, useRef, useState } from 'react';
+import React from 'react';
import ReactDOM from 'react-dom/client';
// Redux and routing imports
import { Provider } from 'react-redux';
-import { Routes, Route, useParams, useNavigate, BrowserRouter } from 'react-router-dom';
+import { Routes, Route, BrowserRouter, Navigate } from 'react-router-dom';
// i18n and constants imports
import { useTranslation } from 'react-i18next';
import { NEW_BENTO_PUBLIC_THEME } from '@/constants/overviewConstants';
-import { DEFAULT_TRANSLATION, SUPPORTED_LNGS } from '@/constants/configConstants';
+import { SUPPORTED_LNGS } from '@/constants/configConstants';
// Component imports
-import { Button, ConfigProvider, Layout, Modal, message } from 'antd';
+import { ConfigProvider } from 'antd';
import { ChartConfigProvider } from 'bento-charts';
-import SiteHeader from '@/components/SiteHeader';
-import SiteFooter from '@/components/SiteFooter';
-import SiteSider from '@/components/SiteSider';
import Loader from '@/components/Loader';
import BentoAppRouter from '@/components/BentoAppRouter';
+import LanguageHandler from '@/components/Util/LanguageHandler';
+import AuthOutlet from '@/components/Util/AuthOutlet';
// Hooks and utilities imports
-import {
- useHandleCallback,
- checkIsInAuthPopup,
- useOpenSignInWindowCallback,
- usePopupOpenerAuthCallback,
- useSignInPopupTokenHandoff,
- useSessionWorkerTokenRefresh,
- BentoAuthContextProvider,
- nop,
-} from 'bento-auth-js';
-import { useQueryWithAuthIfAllowed } from '@/hooks';
+import { BentoAuthContextProvider } from 'bento-auth-js';
// Store and configuration imports
import { store } from './store';
@@ -43,135 +32,46 @@ import 'bento-charts/src/styles.css';
import './i18n';
import '../styles.css';
-const SIGN_IN_WINDOW_FEATURES = 'scrollbars=no, toolbar=no, menubar=no, width=800, height=600';
-const CALLBACK_PATH = '/callback';
-
-const LNGS_ARRAY = Object.values(SUPPORTED_LNGS);
-const { Content } = Layout;
-
-const createSessionWorker = () => new Worker(new URL('./workers/tokenRefresh.ts', import.meta.url));
-
-const App = () => {
- const navigate = useNavigate();
- const [collapsed, setCollapsed] = useState(false);
-
- // TRANSLATION
- const { lang } = useParams<{ lang?: string }>();
- const { t, i18n } = useTranslation(DEFAULT_TRANSLATION);
-
- useEffect(() => {
- console.debug('lang', lang);
- if (lang && lang == 'callback') return;
- if (lang && LNGS_ARRAY.includes(lang)) {
- i18n.changeLanguage(lang);
- } else if (i18n.language) {
- navigate(`/${i18n.language}/`, { replace: true });
- } else {
- navigate(`/${SUPPORTED_LNGS.ENGLISH}/`, { replace: true });
- }
- }, [lang, i18n.language, navigate]);
-
- // AUTHENTICATION
- const [signedOutModal, setSignedOutModal] = useState(false);
-
- const sessionWorker = useRef(null);
- const signInWindow = useRef(null);
- const windowMessageHandler = useRef<((event: MessageEvent) => void) | null>(null);
-
- const openSignInWindow = useOpenSignInWindowCallback(signInWindow, SIGN_IN_WINDOW_FEATURES);
-
- const popupOpenerAuthCallback = usePopupOpenerAuthCallback();
- const isInAuthPopup = checkIsInAuthPopup(PUBLIC_URL_NO_TRAILING_SLASH);
-
- useHandleCallback(
- CALLBACK_PATH,
- () => {
- console.debug('authenticated');
- },
- isInAuthPopup ? popupOpenerAuthCallback : undefined,
- (msg) => message.error(msg)
- );
-
- // Set up message handling from sign-in popup
- useSignInPopupTokenHandoff(windowMessageHandler);
-
- useSessionWorkerTokenRefresh(sessionWorker, createSessionWorker, nop);
-
- useQueryWithAuthIfAllowed();
-
+const BaseRoutes = () => {
return (
-
- {
- setSignedOutModal(false);
- }}
- open={signedOutModal}
- footer={[
- ,
- ]}
- >
- {t('Please sign in to the research portal.')}
-
-
-
-
-
-
-
-
- }>
-
- } />
- } />
-
-
-
-
-
-
-
-
+
+ }>
+ } />
+ }>
+ } />
+ } />
+
+
+
);
};
-const BentoApp = () => {
+const RootApp = () => {
const { i18n } = useTranslation();
- console.log('i18n.language', i18n.language);
return (
-
-
- } />
-
-
+
+
+
+
+
+
+
+
+
+
+
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
-root.render(
-
-
-
-
-
-
-
-);
+root.render();
diff --git a/src/js/store.ts b/src/js/store.ts
index a92d517b..5642f05c 100644
--- a/src/js/store.ts
+++ b/src/js/store.ts
@@ -14,8 +14,9 @@ import dataReducer from '@/features/data/data.store';
import queryReducer from '@/features/search/query.store';
import lastIngestionDataReducer from '@/features/ingestion/lastIngestion.store';
import provenanceReducer from '@/features/provenance/provenance.store';
-import beaconConfigReducer from './features/beacon/beaconConfig.store';
-import beaconQueryReducer from './features/beacon/beaconQuery.store';
+import beaconConfigReducer from '@/features/beacon/beaconConfig.store';
+import beaconQueryReducer from '@/features/beacon/beaconQuery.store';
+import metadataReducer from '@/features/metadata/metadata.store';
import { getValue, saveValue } from './utils/localStorage';
interface PersistedState {
@@ -41,6 +42,7 @@ export const store = configureStore({
lastIngestionData: lastIngestionDataReducer,
beaconConfig: beaconConfigReducer,
beaconQuery: beaconQueryReducer,
+ metadata: metadataReducer,
},
preloadedState: persistedState,
});
diff --git a/src/js/types/metadata.ts b/src/js/types/metadata.ts
new file mode 100644
index 00000000..66a8e3c1
--- /dev/null
+++ b/src/js/types/metadata.ts
@@ -0,0 +1,35 @@
+import { Layout as DiscoveryOverview, Fields as DiscoveryFields } from '@/types/overviewResponse';
+import { Section as DiscoverySearch } from '@/types/search';
+import { DiscoveryRules } from '@/types/configResponse';
+
+export interface Discovery {
+ overview: DiscoveryOverview[];
+ search: DiscoverySearch[];
+ fields: DiscoveryFields;
+ rules: DiscoveryRules;
+}
+
+export interface Project {
+ identifier: string;
+ title: string;
+ description: string;
+ discovery: Discovery | null;
+ datasets: Dataset[];
+}
+
+export interface Dataset {
+ identifier: string;
+ title: string;
+ description: string;
+ discovery: Discovery | null;
+ dats_file: object;
+}
+
+export interface PaginatedResponse {
+ count: number;
+ next: T;
+ previous: T;
+ results: T[];
+}
+
+export type ProjectsResponse = PaginatedResponse;
diff --git a/src/js/types/overviewResponse.ts b/src/js/types/overviewResponse.ts
index 999a2c4d..e1755685 100644
--- a/src/js/types/overviewResponse.ts
+++ b/src/js/types/overviewResponse.ts
@@ -48,7 +48,7 @@ export interface Datum {
value: number;
}
-interface Layout {
+export interface Layout {
charts: ChartConfig[];
section_title: string;
}
diff --git a/src/js/utils/router.ts b/src/js/utils/router.ts
index 534c8a6d..6c12f392 100644
--- a/src/js/utils/router.ts
+++ b/src/js/utils/router.ts
@@ -1,11 +1,47 @@
import { BentoRoute } from '@/types/routes';
+import { MetadataState } from '@/features/metadata/metadata.store';
export const getCurrentPage = (): BentoRoute => {
const pathArray = window.location.pathname.split('/');
const validPages = Object.values(BentoRoute);
- if (pathArray.length > 2 && validPages.includes(pathArray[2] as BentoRoute)) {
- return pathArray[2] as BentoRoute;
+ if (validPages.includes(pathArray[pathArray.length - 1] as BentoRoute)) {
+ return pathArray[pathArray.length - 1] as BentoRoute;
} else {
return BentoRoute.Overview;
}
};
+
+export const validProjectDataset = (
+ projects: MetadataState['projects'],
+ projectId?: string,
+ datasetId?: string
+): MetadataState['selectedScope'] => {
+ const valid: MetadataState['selectedScope'] = {
+ project: undefined,
+ dataset: undefined,
+ };
+
+ if (projectId && projects.find(({ identifier }) => identifier === projectId)) {
+ valid.project = projectId;
+ if (datasetId) {
+ if (
+ projects
+ .find(({ identifier }) => identifier === projectId)!
+ .datasets.find(({ identifier }) => identifier === datasetId)
+ ) {
+ valid.dataset = datasetId;
+ }
+ }
+ }
+ return valid;
+};
+
+export const scopeToUrl = (scope: MetadataState['selectedScope']): string => {
+ if (scope.project && scope.dataset) {
+ return `/p/${scope.project}/d/${scope.dataset}`;
+ } else if (scope.project) {
+ return `/p/${scope.project}`;
+ } else {
+ return '';
+ }
+};
diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json
index c7d0db78..f1f519a7 100644
--- a/src/public/locales/en/default_translation_en.json
+++ b/src/public/locales/en/default_translation_en.json
@@ -91,5 +91,10 @@
"You have been signed out": "You have been signed out",
"Please sign in to the research portal.": "Please sign in to the research portal.",
"Sign In": "Sign In",
- "Sign Out": "Sign Out"
+ "Sign Out": "Sign Out",
+ "Select Scope": "Select Scope",
+ "About": "About",
+ "Datasets": "Datasets",
+ "Clear": "Clear",
+ "Select": "Select"
}
diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json
index 852a5060..37a2abdf 100644
--- a/src/public/locales/fr/default_translation_fr.json
+++ b/src/public/locales/fr/default_translation_fr.json
@@ -91,5 +91,10 @@
"You have been signed out": "Vous avez été déconnecté",
"Please sign in to the research portal.": "Veuillez vous connecter au portail de recherche.",
"Sign In": "Se connecter",
- "Sign Out": "Se déconnecter"
+ "Sign Out": "Se déconnecter",
+ "Select Scope": "Sélectionner un périmètre",
+ "About": "À propos",
+ "Datasets": "Jeux de données",
+ "Clear": "Effacer",
+ "Select": "Sélectionner"
}
diff --git a/src/styles.css b/src/styles.css
index 994e396b..a9faaa22 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -5,6 +5,10 @@ body {
margin: 0;
}
+.no-margin-top {
+ margin-top: 0 !important;
+}
+
.container {
width: 100%;
max-width: 1325px;
@@ -22,3 +26,21 @@ body {
.ant-layout-sider-trigger {
border-right: 1px solid #f0f0f0;
}
+
+.select-project-title {
+ transition: all 0.2s;
+}
+
+.select-project-title:hover {
+ color: white !important;
+ cursor: pointer;
+}
+
+.select-dataset-hover {
+ transition: all 0.2s;
+ border-radius: 7px;
+}
+
+.select-dataset-hover:hover {
+ background-color: aliceblue !important;
+}