-
{props.config.tagSelector.title}
-
- {
- props.config.tagCategories.map((category) => {
- if (category.display === false) {
- return null;
+const DiscoveryTagViewer: React.FunctionComponent
= (props: DiscoveryTagViewerProps) => {
+ const [tagsColumnExpansionStatus, setTagsColumnExpansionStatus] = useState({});
+
+ // getTagsInCategory returns a list of the unique tags in studies which belong
+ // to the specified category.
+ const getTagsInCategory = (category: any, studies: any[] | null):React.ReactNode => {
+ if (!studies) {
+ return ;
+ }
+ const tagMap = {};
+ studies.forEach((study) => {
+ const tagField = props.config.minimalFieldMapping.tagsListFieldName;
+ study[tagField].forEach((tag) => {
+ if (tag.category === category.name) {
+ if (tagMap[tag.name] === undefined) {
+ tagMap[tag.name] = 1;
}
- const tags = getTagsInCategory(category.name, props.studies, props.config);
+ tagMap[tag.name] += 1;
+ }
+ });
+ });
+ const tagArray = Object.keys(tagMap).sort((a, b) => tagMap[b] - tagMap[a]);
- // Capitalize category name
- const categoryWords = category.name.split('_').map((x) => x.toLowerCase());
- categoryWords[0] = categoryWords[0].charAt(0).toUpperCase()
- + categoryWords[0].slice(1);
- const capitalizedCategoryName = categoryWords.join(' ');
+ return (
+
+
+ { tagArray.map((tag) => (
+ {
+ props.setSelectedTags({
+ ...props.selectedTags,
+ [tag]: props.selectedTags[tag] ? undefined : true,
+ });
+ }}
+ onClick={() => {
+ props.setSelectedTags({
+ ...props.selectedTags,
+ [tag]: props.selectedTags[tag] ? undefined : true,
+ });
+ }}
+ >
+ {tag}
+
+ ),
+ )}
+
+ {(tagArray.length > TAG_LIST_LIMIT)
+ ? (
+
{
+ const tagColumn = document.getElementById(`discovery-tag-column--${category.name}`);
+ if (tagColumn) {
+ tagColumn.scrollTop = 0;
+ }
+ setTagsColumnExpansionStatus({
+ ...tagsColumnExpansionStatus,
+ [category.name]: !tagsColumnExpansionStatus[category.name],
+ });
+ }}
+ onKeyPress={() => {
+ const tagColumn = document.getElementById(`discovery-tag-column--${category.name}`);
+ if (tagColumn) {
+ tagColumn.scrollTop = 0;
+ }
+ setTagsColumnExpansionStatus({
+ ...tagsColumnExpansionStatus,
+ [category.name]: !tagsColumnExpansionStatus[category.name],
+ });
+ }}
+ >
+ {`${(tagsColumnExpansionStatus[category.name]) ? 'Hide' : 'Show'} ${tagArray.length - TAG_LIST_LIMIT} more tags`}
+
+ )
+ : null}
+
+ );
+ };
+
+ return (
+
+ {props.config.tagSelector.title
+ &&
{props.config.tagSelector.title} }
+
+ {
+ props.config.tagCategories.map((category) => {
+ if (category.display === false) {
+ return null;
+ }
+ const tags = getTagsInCategory(category, props.studies);
- let tagsClone = [...tags];
- tagsClone.sort((a, b) => a.length - b.length);
- const uniqueTags = new Set(tagsClone);
- tagsClone = [...uniqueTags];
+ let categoryDisplayName = category.displayName;
+ if (!categoryDisplayName) {
+ // Capitalize category name
+ const categoryWords = category.name.split('_').map((x) => x.toLowerCase());
+ categoryWords[0] = categoryWords[0].charAt(0).toUpperCase()
+ + categoryWords[0].slice(1);
+ categoryDisplayName = categoryWords.join(' ');
+ }
- return (
-
-
{capitalizedCategoryName}
- { tagsClone.map((tag) => (
- {
- props.setSelectedTags({
- ...props.selectedTags,
- [tag]: props.selectedTags[tag] ? undefined : true,
- });
- }}
- onClick={() => {
- props.setSelectedTags({
- ...props.selectedTags,
- [tag]: props.selectedTags[tag] ? undefined : true,
- });
- }}
- >
- {tag}
-
- ),
- )}
-
- );
- })
- }
+ return (
+
+ {categoryDisplayName}
+ { tags }
+
+ );
+ })
+ }
+
-
-);
+ );
+};
DiscoveryTagViewer.defaultProps = {
studies: null,
diff --git a/src/Discovery/__mocks__/mock_config.json b/src/Discovery/__mocks__/mock_config.json
index df66c5e9f1..666be5f4d0 100644
--- a/src/Discovery/__mocks__/mock_config.json
+++ b/src/Discovery/__mocks__/mock_config.json
@@ -23,16 +23,16 @@
}
},
"aggregations": [
- {
- "name": "Studies",
- "field": "study_id",
- "type": "count"
- },
- {
- "name": "Total Subjects",
- "field": "_subjects_count",
- "type": "sum"
- }
+ {
+ "name": "Studies",
+ "field": "study_id",
+ "type": "count"
+ },
+ {
+ "name": "Total Subjects",
+ "field": "_subjects_count",
+ "type": "sum"
+ }
],
"tagSelector": {
"title": "Associated tags organized by category"
diff --git a/src/Discovery/index.tsx b/src/Discovery/index.tsx
index b4a255b16f..701b837519 100644
--- a/src/Discovery/index.tsx
+++ b/src/Discovery/index.tsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
-import Discovery from './Discovery';
+import Discovery, { AccessLevel } from './Discovery';
import { DiscoveryConfig } from './DiscoveryConfig';
import { userHasMethodForServiceOnResource } from '../authMappingUtils';
import { hostname, discoveryConfig, useArboristUI } from '../localconf';
@@ -69,15 +69,27 @@ const DiscoveryWithMDSBackend: React.FC<{
loadStudiesFunction().then((rawStudies) => {
if (props.config.features.authorization.enabled) {
// mark studies as accessible or inaccessible to user
- const { authzField } = props.config.minimalFieldMapping;
+ const { authzField, dataAvailabilityField } = props.config.minimalFieldMapping;
// useArboristUI=true is required for userHasMethodForServiceOnResource
if (!useArboristUI) {
throw new Error('Arborist UI must be enabled for the Discovery page to work if authorization is enabled in the Discovery page. Set `useArboristUI: true` in the portal config.');
}
- const studiesWithAccessibleField = rawStudies.map((study) => ({
- ...study,
- __accessible: userHasMethodForServiceOnResource('read', '*', study[authzField], props.userAuthMapping),
- }));
+ const studiesWithAccessibleField = rawStudies.map((study) => {
+ let accessible: AccessLevel;
+ if (dataAvailabilityField && study[dataAvailabilityField] === 'pending') {
+ accessible = AccessLevel.PENDING;
+ } else if (study[authzField] === undefined || study[authzField] === '') {
+ accessible = AccessLevel.NOT_AVAILABLE;
+ } else {
+ accessible = userHasMethodForServiceOnResource('read', '*', study[authzField], props.userAuthMapping)
+ ? AccessLevel.ACCESSIBLE
+ : AccessLevel.UNACCESSIBLE;
+ }
+ return {
+ ...study,
+ __accessible: accessible,
+ };
+ });
setStudies(studiesWithAccessibleField);
} else {
setStudies(rawStudies);
diff --git a/src/Discovery/reduxer.js b/src/Discovery/reduxer.js
new file mode 100644
index 0000000000..9b40f83adb
--- /dev/null
+++ b/src/Discovery/reduxer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import DiscoveryActionBar from './DiscoveryActionBar';
+
+const ReduxDiscoveryActionBar = (() => {
+ const mapStateToProps = (state) => ({
+ user: state.user,
+ });
+
+ return connect(mapStateToProps)(DiscoveryActionBar);
+})();
+
+export default ReduxDiscoveryActionBar;
diff --git a/src/components/Popup.less b/src/components/Popup.less
index 05144db45b..ccd1395983 100644
--- a/src/components/Popup.less
+++ b/src/components/Popup.less
@@ -3,7 +3,7 @@
#popup {
width: 100%;
position: absolute;
- z-index:100;
+ z-index: 100;
}
.popup__mask {
diff --git a/src/healIndex.jsx b/src/healIndex.jsx
new file mode 100644
index 0000000000..560437954b
--- /dev/null
+++ b/src/healIndex.jsx
@@ -0,0 +1,505 @@
+import React from 'react';
+import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+import {
+ BrowserRouter, Route, Switch, Redirect,
+} from 'react-router-dom';
+import { ThemeProvider } from 'styled-components';
+import querystring from 'querystring';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
+import ReactGA from 'react-ga';
+import { Helmet } from 'react-helmet';
+import { datadogRum } from '@datadog/browser-rum';
+
+import 'antd/dist/antd.css';
+import '@gen3/ui-component/dist/css/base.less';
+import { fetchAndSetCsrfToken } from './configs';
+import {
+ fetchDictionary, fetchSchema, fetchVersionInfo, fetchUserAccess, fetchUserAuthMapping,
+} from './actions';
+import ReduxLogin, { fetchLogin } from './Login/ReduxLogin';
+import ProtectedContent from './Login/ProtectedContent';
+import HomePage from './Homepage/page';
+import DocumentPage from './Document/page';
+import { fetchCoreMetadata, ReduxCoreMetadataPage } from './CoreMetadata/reduxer';
+import Indexing from './Indexing/Indexing';
+// import IndexPage from './Index/page';
+import DataDictionary from './DataDictionary';
+import ReduxPrivacyPolicy from './PrivacyPolicy/ReduxPrivacyPolicy';
+import ProjectSubmission from './Submission/ReduxProjectSubmission';
+import ReduxMapFiles from './Submission/ReduxMapFiles';
+import ReduxMapDataModel from './Submission/ReduxMapDataModel';
+import UserProfile, { fetchAccess } from './UserProfile/ReduxUserProfile';
+import UserAgreementCert from './UserAgreement/ReduxCertPopup';
+import GraphQLQuery from './GraphQLEditor/ReduxGqlEditor';
+import theme from './theme';
+import getReduxStore from './reduxStore';
+import { ReduxNavBar, ReduxTopBar, ReduxFooter } from './Layout/reduxer';
+import ReduxQueryNode, { submitSearchForm } from './QueryNode/ReduxQueryNode';
+import {
+ basename, dev, gaDebug, workspaceUrl, workspaceErrorUrl,
+ explorerPublic, enableResourceBrowser, resourceBrowserPublic, enableDAPTracker,
+ discoveryConfig, ddApplicationId, ddClientToken, ddEnv, ddSampleRate,
+} from './localconf';
+import Analysis from './Analysis/Analysis';
+import ReduxAnalysisApp from './Analysis/ReduxAnalysisApp';
+import { gaTracking, components } from './params';
+import GA, { RouteTracker } from './components/GoogleAnalytics';
+import { DAPRouteTracker } from './components/DAPAnalytics';
+import GuppyDataExplorer from './GuppyDataExplorer';
+import isEnabled from './helpers/featureFlags';
+import sessionMonitor from './SessionMonitor';
+import Workspace from './Workspace';
+import ResourceBrowser from './ResourceBrowser';
+import Discovery from './Discovery';
+import ErrorWorkspacePlaceholder from './Workspace/ErrorWorkspacePlaceholder';
+import { ReduxStudyViewer, ReduxSingleStudyViewer } from './StudyViewer/reduxer';
+import NotFound from './components/NotFound';
+
+// monitor user's session
+sessionMonitor.start();
+
+// render the app after the store is configured
+async function init() {
+ const store = await getReduxStore();
+
+ // asyncSetInterval(() => store.dispatch(fetchUser), 60000);
+ ReactGA.initialize(gaTracking);
+ ReactGA.pageview(window.location.pathname + window.location.search);
+
+ // Datadog setup
+ if (ddApplicationId && !ddClientToken) {
+ console.warn('Datadog applicationId is set, but clientToken is missing');
+ } else if (!ddApplicationId && ddClientToken) {
+ console.warn('Datadog clientToken is set, but applicationId is missing');
+ } else if (ddApplicationId && ddClientToken) {
+ datadogRum.init({
+ applicationId: ddApplicationId,
+ clientToken: ddClientToken,
+ site: 'datadoghq.com',
+ service: 'portal',
+ env: ddEnv,
+ // Specify a version number to identify the deployed version of your application in Datadog
+ // version: '1.0.0',
+ sampleRate: ddSampleRate,
+ trackInteractions: true,
+ });
+ }
+
+ await Promise.all(
+ [
+ store.dispatch(fetchSchema),
+ store.dispatch(fetchDictionary),
+ store.dispatch(fetchVersionInfo),
+ // resources can be open to anonymous users, so fetch access:
+ store.dispatch(fetchUserAccess),
+ store.dispatch(fetchUserAuthMapping),
+ // eslint-disable-next-line no-console
+ fetchAndSetCsrfToken().catch((err) => { console.log('error on csrf load - should still be ok', err); }),
+ ],
+ );
+ // FontAwesome icons
+ library.add(faAngleUp, faAngleDown);
+
+ render(
+
+
+
+
+
+ {GA.init(gaTracking, dev, gaDebug) &&
}
+ {enableDAPTracker &&
}
+ {isEnabled('noIndex')
+ ? (
+
+
+
+ )
+ : null}
+
+
+
+
+ {/* process with trailing and duplicate slashes first */}
+ {/* see https://github.com/ReactTraining/react-router/issues/4841#issuecomment-523625186 */}
+ {/* Removes trailing slashes */}
+ (
+
+ )}
+ />
+ {/* Removes duplicate slashes in the middle of the URL */}
+ (
+
+ )}
+ />
+ (
+ store.dispatch(fetchLogin())}
+ component={ReduxLogin}
+ {...props}
+ />
+ )
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+ {
+ isEnabled('analysis')
+ ? (
+
+ }
+ />
+ )
+ : null
+ }
+ {
+ isEnabled('analysis')
+ ? (
+
+ }
+ />
+ )
+ : null
+ }
+ (
+ store.dispatch(fetchAccess())}
+ component={UserProfile}
+ {...props}
+ />
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+ (
+ store.dispatch(fetchCoreMetadata(props.match.params[0]))}
+ component={ReduxCoreMetadataPage}
+ {...props}
+ />
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+
+ }
+ />
+
+
+ {
+ const queryFilter = () => {
+ const { location } = props;
+ const queryParams = querystring.parse(location.search ? location.search.replace(/^\?+/, '') : '');
+ if (Object.keys(queryParams).length > 0) {
+ // Linking directly to a search result,
+ // so kick-off search here (rather than on button click)
+ return store.dispatch(
+ submitSearchForm({
+ project: props.match.params.project, ...queryParams,
+ }),
+ );
+ }
+ return Promise.resolve('ok');
+ };
+ return (
+
+ );
+ }
+ }
+ />
+ {isEnabled('explorer')
+ ? (
+ (
+
+ )
+ }
+ />
+ )
+ : null}
+ {components.privacyPolicy
+ && (!!components.privacyPolicy.file || !!components.privacyPolicy.routeHref)
+ ? (
+
+ )
+ : null}
+ {enableResourceBrowser
+ ? (
+ (
+
+ )
+ }
+ />
+ )
+ : null}
+ (
+
+ )
+ }
+ />
+ (
+
+ )
+ }
+ />
+ {isEnabled('discovery')
+ && (
+ (
+
+ )
+ }
+ />
+ )}
+ {isEnabled('discovery')
+ && (
+ (
+
+ )
+ }
+ />
+ )}
+
+ (
+
+ )
+ }
+ />
+
+
+
+
+
+
+
+
+
,
+ document.getElementById('root'),
+ );
+}
+
+init();
diff --git a/src/index.ejs b/src/index.ejs
index d756af3b6a..c5e4568a04 100644
--- a/src/index.ejs
+++ b/src/index.ejs
@@ -15,6 +15,7 @@
+
)}
- {isEnabled('discovery')
- && (
-
(
-
- )
- }
- />
- )}
+ {isEnabled('discovery') && (
+ (
+
+ )
+ }
+ />
+ )}