Skip to content

Commit

Permalink
[JN-487] admin left bar (#496)
Browse files Browse the repository at this point in the history
Co-authored-by: Brian Reilly <[email protected]>
  • Loading branch information
devonbush and breilly2 authored Aug 9, 2023
1 parent c69a7e3 commit bf214dd
Show file tree
Hide file tree
Showing 55 changed files with 1,010 additions and 693 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ public ResponseEntity<Resource> get(

private ResponseEntity<Resource> convertToResourceResponse(Optional<SiteImage> imageOpt) {
if (imageOpt.isPresent()) {
MediaType contentType;
SiteImage image = imageOpt.get();
MediaType contentType =
MediaType.parseMediaType(
URLConnection.guessContentTypeFromName(image.getCleanFileName()));
if (image.getCleanFileName().endsWith(".ico")) {
contentType = MediaType.valueOf("image/x-icon");
} else {
contentType =
MediaType.parseMediaType(
URLConnection.guessContentTypeFromName(image.getCleanFileName()));
}
return ResponseEntity.ok()
.contentType(contentType)
.body(new ByteArrayResource(image.getData()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ public Portal fullLoad(AdminUser user, String portalShortcode) {
return portalService.fullLoad(portal, "en");
}

/** gets all the portals the user has access to, and attaches the corresponding studies */
public List<Portal> getAll(AdminUser user) {
// no additional auth checks needed -- the underlying service filters out portals the user does
// not have access to
return portalService.findByAdminUser(user);
List<Portal> portals = portalService.findByAdminUser(user);
portalService.attachStudies(portals);
return portals;
}

public PortalEnvironmentConfig updateConfig(
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/bio/terra/pearl/core/dao/portal/PortalDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ public Portal fullLoad(Portal portal, String language) {
return portal;
}

public void attachStudies(List<Portal> portals) {
List<UUID> portalIds = portals.stream().map(portal -> portal.getId()).toList();
List<PortalStudy> portalStudies = portalStudyDao.findByPortalIds(portalIds);
portalStudyDao.attachStudies(portalStudies);
for(Portal portal : portals) {
List<PortalStudy> matches = portalStudies.stream().filter(portalStudy -> portalStudy.getPortalId().equals(portal.getId())).toList();
portal.getPortalStudies().addAll(matches);
}
}

public List<Portal> findByAdminUserId(UUID userId) {
List<PortalAdminUser> portalAdmins = portalAdminUserDao.findByUserId(userId);
return findAll(portalAdmins.stream().map(PortalAdminUser::getPortalId).toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import bio.terra.pearl.core.model.study.Study;
import org.jdbi.v3.core.Jdbi;
import org.springframework.stereotype.Component;

@Component
public class PortalStudyDao extends BaseJdbiDao<PortalStudy> {
public PortalStudyDao(Jdbi jdbi) {
private StudyDao studyDao;
public PortalStudyDao(Jdbi jdbi, StudyDao studyDao) {
super(jdbi);
this.studyDao = studyDao;
}
@Override
protected Class<PortalStudy> getClazz() {
Expand All @@ -25,6 +29,9 @@ public List<PortalStudy> findByStudyId(UUID studyId) {
public List<PortalStudy> findByPortalId(UUID portalId) {
return findAllByProperty("portal_id", portalId);
}
public List<PortalStudy> findByPortalIds(List<UUID> portalIds) {
return findAllByPropertyCollection("portal_id", portalIds);
}

/** gets a list of PortalStudies corresponding to an Enrollee. Enrollees are specific to a single Study,
* so this will only return multiple results if that Study is in multiple Portals
Expand Down Expand Up @@ -52,6 +59,15 @@ public Optional<PortalStudy> findStudyInPortal(String studyShortcode, UUID porta
.findOne());
}

public void attachStudies(List<PortalStudy> portalStudies) {
List<UUID> ids = portalStudies.stream().map(ps -> ps.getStudyId()).toList();
List<Study> studies = studyDao.findAll(ids);
for(PortalStudy portalStudy : portalStudies) {
portalStudy.setStudy(studies.stream().filter(study -> study.getId().equals(portalStudy.getStudyId()))
.findFirst().get());
}
}

public void deleteByPortalId(UUID portalId) {
deleteByProperty("portal_id", portalId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public List<Portal> findByAdminUser(AdminUser user) {
return dao.findByAdminUserId(user.getId());
}

public void attachStudies(List<Portal> portals) {
dao.attachStudies(portals);
}

public boolean checkAdminIsInPortal(AdminUser user, UUID portalId) {
return user.isSuperuser() || portalAdminUserDao.isUserInPortal(user.getId(), portalId);
}
Expand Down
1 change: 1 addition & 0 deletions populate/lombok.config
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# This file is generated by the 'io.freefair.lombok' Gradle plugin
config.stopBubbling = true
lombok.extern.findbugs.addSuppressFBWarnings = true
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@
}
],
"siteImageDtos": [{
"populateFileName": "images/main_banner.webp"
},
"populateFileName": "images/main_banner.webp"
},
{
"populateFileName": "images/participant_smile.webp"
},
{
"populateFileName": "images/HEX_Round.webp"
}]
},
{
"populateFileName": "images/favicon.ico"
}]
}
58 changes: 30 additions & 28 deletions ui-admin/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { lazy, Suspense, useContext } from 'react'
import React, { lazy, Suspense } from 'react'
import 'react-notifications-component/dist/theme.css'
import 'styles/notifications.css'
import 'survey-core/defaultV2.min.css'
Expand All @@ -9,9 +9,8 @@ import { ReactNotifications } from 'react-notifications-component'

import { RedirectFromOAuth } from 'login/RedirectFromOAuth'
import { ProtectedRoute } from 'login/ProtectedRoute'
import NavbarProvider, { NavbarContext } from 'navbar/NavbarProvider'
import AdminNavbar from 'navbar/AdminNavbar'
import PortalList from 'portal/PortalList'
import HomePage from 'HomePage'
import PortalProvider from 'portal/PortalProvider'
import UserProvider from 'user/UserProvider'
import ConfigProvider, { ConfigConsumer } from 'providers/ConfigProvider'
Expand All @@ -23,6 +22,8 @@ import InvestigatorTermsOfUsePage from './terms/InvestigatorTermsOfUsePage'
import PrivacyPolicyPage from 'terms/PrivacyPolicyPage'
import { IdleStatusMonitor } from 'login/IdleStatusMonitor'
import LoadingSpinner from './util/LoadingSpinner'
import AdminSidebar from './navbar/AdminSidebar'
import NavContextProvider from 'navbar/NavContextProvider'
const HelpRouter = lazy(() => import('./help/HelpRouter'))


Expand All @@ -37,27 +38,26 @@ function App() {
<div className="App d-flex flex-column min-vh-100">
<IdleStatusMonitor maxIdleSessionDuration={30 * 60 * 1000} idleWarningDuration={5 * 60 * 1000}/>
<ReactNotifications />
<NavbarProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<PageFrame/>}>
<Route path="help/*" element={<Suspense fallback={<LoadingSpinner/>}>
<HelpRouter />
</Suspense>} />
<Route element={<ProtectedRoute/>}>
<Route path="users" element={<UserList/>}/>
<Route path=":portalShortcode/*" element={<PortalProvider><PortalRouter/></PortalProvider>}/>
<Route index element={<PortalList/>}/>
</Route>

<Route path="privacy" element={<PrivacyPolicyPage />} />
<Route path="terms" element={<InvestigatorTermsOfUsePage />} />
<Route path="*" element={<div>Unknown page</div>}/>
<BrowserRouter>
<Routes>
<Route path="/">
<Route path="help/*" element={<Suspense fallback={<LoadingSpinner/>}>
<HelpRouter />
</Suspense>} />
<Route element={<ProtectedRoute>
<NavContextProvider><PageFrame/></NavContextProvider>
</ProtectedRoute>}>
<Route path="users" element={<UserList/>}/>
<Route path=":portalShortcode/*" element={<PortalProvider><PortalRouter/></PortalProvider>}/>
<Route index element={<HomePage/>}/>
</Route>
<Route path='redirect-from-oauth' element={<RedirectFromOAuth/>}/>
</Routes>
</BrowserRouter>
</NavbarProvider>
<Route path="privacy" element={<PrivacyPolicyPage />} />
<Route path="terms" element={<InvestigatorTermsOfUsePage />} />
<Route path="*" element={<div>Unknown page</div>}/>
</Route>
<Route path='redirect-from-oauth' element={<RedirectFromOAuth/>}/>
</Routes>
</BrowserRouter>
</div>
</UserProvider>
</AuthProvider>
Expand All @@ -69,12 +69,14 @@ function App() {

/** Renders the navbar and footer for the page */
function PageFrame() {
const navContext = useContext(NavbarContext)
return (
<>
<AdminNavbar {...navContext}/>
<Outlet/>
</>
<div className="d-flex">
<AdminSidebar/>
<div className="flex-grow-1 d-flex flex-column">
<AdminNavbar/>
<Outlet/>
</div>
</div>
)
}
export default App
35 changes: 35 additions & 0 deletions ui-admin/src/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { studyParticipantsPath } from './portal/PortalRouter'
import { useNavContext } from './navbar/NavContextProvider'
import { getImageUrl } from './api/api'

/** Shows a user the list of portals available to them */
function HomePage() {
const { portalList } = useNavContext()

return <div className="container">
<h1 className="h2">Juniper Home</h1>
<div className="ms-5 mt-4">
<h2 className="h4">My Studies</h2>
<ul className="list-group list-group-flush fs-5">
{ portalList.flatMap(portal =>
portal.portalStudies.map(portalStudy => {
const study = portalStudy.study
return <li key={`${portal.shortcode}-${study.shortcode}`}
className="list-group-item my-1 border border-secondary-subtle rounded ">
<Link to={studyParticipantsPath(portal.shortcode, study.shortcode, 'live')}>
<img
src={getImageUrl(portal.shortcode, 'favicon.ico', 1)}
className="me-3" style={{ maxHeight: '1.5em' }}/>
{study.name}
</Link>
</li>
})
)}
</ul>
</div>
</div>
}

export default HomePage
10 changes: 10 additions & 0 deletions ui-admin/src/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -802,11 +802,21 @@ export default {
}
}

/** gets an image url for a SiteImage suitable for including in an img tag */
export function getImageUrl(portalShortcode: string, cleanFileName: string, version: number) {
return `${basePublicPortalEnvUrl(portalShortcode, 'live')}/siteImages/${version}/${cleanFileName}`
}

/** base api path for study-scoped api requests */
function basePortalEnvUrl(portalShortcode: string, envName: string) {
return `${API_ROOT}/portals/v1/${portalShortcode}/env/${envName}`
}

/** base api path for study-scoped api requests */
function basePublicPortalEnvUrl(portalShortcode: string, envName: string) {
return `${API_ROOT}/public/portals/v1/${portalShortcode}/env/${envName}`
}

/** base api path for study-scoped api requests */
function baseStudyUrl(portalShortcode: string, studyShortcode: string) {
return `${API_ROOT}/portals/v1/${portalShortcode}/studies/${studyShortcode}`
Expand Down
8 changes: 8 additions & 0 deletions ui-admin/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ code {
opacity: $form-check-label-disabled-opacity;
}
}

[aria-expanded="false"] .hidden-when-collapsed {
display: none;
}

[aria-expanded="true"] .hidden-when-expanded {
display: none;
}
5 changes: 2 additions & 3 deletions ui-admin/src/navbar/AdminNavbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { mockAdminUser, MockUserProvider } from 'test-utils/user-mocking-utils'
import { render, screen } from '@testing-library/react'
import AdminNavbar from './AdminNavbar'
import userEvent from '@testing-library/user-event'
import { emptyNavbarContext } from './NavbarProvider'

test('renders the help menu', async () => {
const { RoutedComponent } = setupRouterTest(
<MockUserProvider user={mockAdminUser(false)}>
<AdminNavbar {...emptyNavbarContext}/>
<AdminNavbar/>
</MockUserProvider>)
render(RoutedComponent)
expect(screen.getByTitle('help menu')).toBeInTheDocument()
Expand All @@ -24,7 +23,7 @@ test('renders the user menu', async () => {
...mockAdminUser(false),
username: 'testuser123'
}}>
<AdminNavbar {...emptyNavbarContext}/>
<AdminNavbar/>
</MockUserProvider>)
render(RoutedComponent)
expect(screen.getByTitle('user menu')).toBeInTheDocument()
Expand Down
Loading

0 comments on commit bf214dd

Please sign in to comment.