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

[JN-1388] Pin study environment [wip] #1157

Draft
wants to merge 2 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion ui-admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import LogEventViewer from './health/LogEventViewer'
import { initializeMixpanel } from '@juniper/ui-core'
import mixpanel from 'mixpanel-browser'
import { StatusPage } from './status/StatusPage'
import { PinnedEnvProvider } from './study/usePinnedEnv'

/** auto-scroll-to-top on any navigation */
const ScrollToTop = () => {
Expand Down Expand Up @@ -64,7 +65,9 @@ function App() {
<Route path="/">
<Route path="/system/status" element={<StatusPage/>}/>
<Route element={<ProtectedRoute>
<NavContextProvider><PageFrame config={config}/></NavContextProvider>
<PinnedEnvProvider>
<NavContextProvider><PageFrame config={config}/></NavContextProvider>
</PinnedEnvProvider>
</ProtectedRoute>}>
<Route path="populate/*" element={<PopulateRouteSelect/>}/>
<Route path="logEvents/*" element={<LogEventViewer/>}/>
Expand Down
4 changes: 3 additions & 1 deletion ui-admin/src/navbar/AdminNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ function AdminNavbar() {
* This component does not render anything directly, but is still structured as a component rather than a pure hook
* so that order rendering will be in-order rather than reversed. See https://github.com/facebook/react/issues/15281
* */
export function NavBreadcrumb({ value, children }: {value: string, children: React.ReactNode}) {
export function NavBreadcrumb({ value, children }: {
value: string, children: React.ReactNode
}) {
const { setBreadCrumbs } = useNavContext()
useEffect(() => {
/** use the setState arg that takes a function to avoid race conditions */
Expand Down
43 changes: 26 additions & 17 deletions ui-admin/src/navbar/StudySidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Portal, Study } from '@juniper/ui-core'
import { EnvironmentName, Portal, Study } from '@juniper/ui-core'
import { NavLink, useNavigate } from 'react-router-dom'
import { studyKitsPath, studyParticipantsPath } from 'portal/PortalRouter'
import StudySelector from './StudySelector'
Expand All @@ -14,11 +14,12 @@ import {
studyEnvNotificationsPath,
studyEnvSiteContentPath,
studyEnvSiteSettingsPath
} from '../study/StudyEnvironmentRouter'
} from 'study/StudyEnvironmentRouter'
import CollapsableMenu from './CollapsableMenu'
import { userHasPermission, useUser } from 'user/UserProvider'
import { studyPublishingPath, studyUsersPath } from '../study/StudyRouter'
import { studyPublishingPath, studyUsersPath } from 'study/StudyRouter'
import { sidebarNavLinkClasses } from './AdminSidebar'
import { usePinnedEnv } from 'study/usePinnedEnv'


/** shows menu options related to the current study */
Expand Down Expand Up @@ -46,57 +47,60 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
<div className="text-white">
<CollapsableMenu header={'Research Coordination'} content={<ul className="list-unstyled">
<li className="mb-2">
<NavLink to={studyParticipantsPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyParticipantsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Participant List</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyKitsPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyKitsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Kits</NavLink>
</li>
<li className="mb-2">
<NavLink to={adminTasksPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={adminTasksPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Tasks</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvMailingListPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyEnvMailingListPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Mailing List</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvImportPath(portalShortcode, study.shortcode, 'sandbox')}
<NavLink to={studyEnvImportPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('sandbox'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Import Participants</NavLink>
</li>
</ul>}/>
<CollapsableMenu header={'Analytics & Data'} content={<ul className="list-unstyled">
<li className="mb-2">
<NavLink to={studyEnvMetricsPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyEnvMetricsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Participant Analytics</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvDataBrowserPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyEnvDataBrowserPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Data Export</NavLink>
</li>
{ portalId && userHasPermission(user.user, portalId, 'export_integration') && <li className="mb-2">
<NavLink to={studyEnvExportIntegrationsPath({ ...studyParams, envName: 'live' })}
className={sidebarNavLinkClasses} style={navStyleFunc}>Export Integrations</NavLink>
<NavLink to={studyEnvExportIntegrationsPath({
...studyParams,
envName: getPinnedOrDefaultEnv('live') as EnvironmentName
})}
className={sidebarNavLinkClasses} style={navStyleFunc}>Export Integrations</NavLink>
</li>
}
{ portalId && userHasPermission(user.user, portalId, 'tdr_export') && <li>
<NavLink to={studyEnvDatasetListViewPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyEnvDatasetListViewPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Terra Data Repo</NavLink>
</li>
}
</ul>}/>
<CollapsableMenu header={'Design & Build'} content={<ul className="list-unstyled">
<li className="mb-2">
<NavLink to={studyEnvSiteContentPath(portalShortcode, study.shortcode, 'sandbox')}
<NavLink to={studyEnvSiteContentPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('sandbox'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Website</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvFormsPath(portalShortcode, study.shortcode, 'sandbox')}
<NavLink to={studyEnvFormsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('sandbox'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Forms &amp; Surveys</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvNotificationsPath(portalShortcode, study.shortcode, 'sandbox')}
<NavLink to={studyEnvNotificationsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('sandbox'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Emails &amp; Notifications</NavLink>
</li>
</ul>}/>
Expand All @@ -106,7 +110,7 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
className={sidebarNavLinkClasses} style={navStyleFunc}>Publish Content</NavLink>
</li>
<li>
<NavLink to={studyEnvSiteSettingsPath(portalShortcode, study.shortcode, 'live')}
<NavLink to={studyEnvSiteSettingsPath(portalShortcode, study.shortcode, getPinnedOrDefaultEnv('live'))}
className={sidebarNavLinkClasses} style={navStyleFunc}>Site Settings</NavLink>
</li>
</ul>}/>
Expand All @@ -120,3 +124,8 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
</div>
</div>
}

const getPinnedOrDefaultEnv = (defaultEnv: string) => {
const { pinnedEnv } = usePinnedEnv()
return pinnedEnv || defaultEnv
}
35 changes: 2 additions & 33 deletions ui-admin/src/study/StudyEnvironmentRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import { StudyParams } from 'study/StudyRouter'
import {
Route,
Routes,
useNavigate,
useParams
} from 'react-router-dom'
import { NavBreadcrumb } from '../navbar/AdminNavbar'
import {
LoadedPortalContextT,
PortalContext,
Expand All @@ -30,9 +28,7 @@ import ExportDataBrowser from './export/ExportDataBrowser'
import StudyEnvMetricsView from './metrics/StudyEnvMetricsView'
import DatasetDashboard from './export/datarepo/DatasetDashboard'
import DatasetList from './export/datarepo/DatasetList'
import Select from 'react-select'
import MailingListView from '../portal/MailingListView'
import { ENVIRONMENT_ICON_MAP } from './publishing/PortalPublishingView'
import TriggerList from './notifications/TriggerList'
import SiteContentLoader from '../portal/siteContent/SiteContentLoader'
import AdminTaskList from './adminTasks/AdminTaskList'
Expand All @@ -53,6 +49,7 @@ import ExportIntegrationList from './export/integrations/ExportIntegrationList'
import ExportIntegrationView from './export/integrations/ExportIntegrationView'
import ExportIntegrationJobList from './export/integrations/ExportIntegrationJobList'
import LoadedSettingsView from './settings/SettingsView'
import { StudyEnvironmentSwitcher } from './StudyEnvironmentSwitcher'

export type StudyEnvContextT = { study: Study, currentEnv: StudyEnvironment, currentEnvPath: string, portal: Portal }

Expand All @@ -62,17 +59,6 @@ function StudyEnvironmentRouter({ study }: { study: Study }) {
const envName: string | undefined = params.studyEnv
const portalContext = useContext(PortalContext) as LoadedPortalContextT
const portal = portalContext.portal
const navigate = useNavigate()

const changeEnv = (newEnv?: string) => {
if (!newEnv) {
return
}
const currentPath = window.location.pathname
const newPath = currentPath
.replace(`/env/${envName}`, `/env/${newEnv}`)
navigate(newPath)
}

if (!envName) {
return <span>no environment selected</span>
Expand All @@ -81,11 +67,6 @@ function StudyEnvironmentRouter({ study }: { study: Study }) {
if (!currentEnv) {
return <span>invalid environment {envName}</span>
}
const envOpts = ['live', 'irb', 'sandbox'].map(env => ({
label: <span>
{ENVIRONMENT_ICON_MAP[env]} &nbsp; {env}
</span>, value: env
}))
const currentEnvPath = studyEnvPath(portal.shortcode, study.shortcode, currentEnv.environmentName)
const portalEnv = portal.portalEnvironments
.find(env => env.environmentName === currentEnv.environmentName) as PortalEnvironment
Expand All @@ -95,19 +76,7 @@ function StudyEnvironmentRouter({ study }: { study: Study }) {
}

return <div className="StudyView d-flex flex-column flex-grow-1">
<NavBreadcrumb value={currentEnvPath}>
<Select options={envOpts}
value={envOpts.find(opt => opt.value === envName)}
className="me-2"
styles={{
control: baseStyles => ({
...baseStyles,
minWidth: '9em'
})
}}
onChange={opt => changeEnv(opt?.value)}
/>
</NavBreadcrumb>
<StudyEnvironmentSwitcher currentEnvPath={currentEnvPath} envName={envName}/>
<ApiProvider api={previewApi(portal.shortcode, currentEnv.environmentName)}>
<I18nProvider defaultLanguage={'en'} portalShortcode={portal.shortcode}>
<Routes>
Expand Down
73 changes: 73 additions & 0 deletions ui-admin/src/study/StudyEnvironmentSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NavBreadcrumb } from 'navbar/AdminNavbar'
import React from 'react'
import { IconButton } from 'components/forms/Button'
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
import Select from 'react-select'
import { ENVIRONMENT_ICON_MAP } from './publishing/PortalPublishingView'
import { usePinnedEnv } from './usePinnedEnv'
import { useNavigate } from 'react-router-dom'

const envOpts = ['live', 'irb', 'sandbox'].map(env => ({
label: <span>{ENVIRONMENT_ICON_MAP[env]} &nbsp; {env}</span>,
value: env
}))

interface StudyEnvironmentSwitcherProps {
currentEnvPath: string
envName: string
}

export const StudyEnvironmentSwitcher = ({
currentEnvPath,
envName
}: StudyEnvironmentSwitcherProps) => {
const navigate = useNavigate()
const { pinnedEnv, setPinnedEnv } = usePinnedEnv()

const changeEnv = (newEnv?: string) => {
if (!newEnv) {
return
}
const currentPath = window.location.pathname
const newPath = currentPath
.replace(`/env/${envName}`, `/env/${newEnv}`)

// reset the pinned env
setPinnedEnv(undefined)

navigate(newPath)
}

const handlePinClick = () => {
if (pinnedEnv) {
setPinnedEnv(undefined)
} else {
setPinnedEnv(envName)
}
changeEnv()
}

return (
<NavBreadcrumb value={currentEnvPath + pinnedEnv}>
<IconButton
icon={faThumbtack}
aria-label={
'Pin this study environment. Pinning an environment will keep it selected when navigating to other pages.'
}
tooltipPlacement={'bottom'} variant={pinnedEnv ? 'primary' : 'light'} className="me-2 border"
onClick={handlePinClick}
/>
<Select options={envOpts}
value={envOpts.find(opt => opt.value === envName)}
className="me-2"
styles={{
control: baseStyles => ({
...baseStyles,
minWidth: '9em'
})
}}
onChange={opt => changeEnv(opt?.value)}
/>
</NavBreadcrumb>
)
}
26 changes: 26 additions & 0 deletions ui-admin/src/study/usePinnedEnv.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'

interface PinnedEnvContextProps {
pinnedEnv?: string
setPinnedEnv: (env?: string) => void
}

const PinnedEnvContext = createContext<PinnedEnvContextProps | undefined>(undefined)

export const PinnedEnvProvider = ({ children }: { children: ReactNode }) => {
const [pinnedEnv, setPinnedEnv] = useState<string>()

return (
<PinnedEnvContext.Provider value={{ pinnedEnv, setPinnedEnv }}>
{children}
</PinnedEnvContext.Provider>
)
}

export const usePinnedEnv = (): PinnedEnvContextProps => {
const context = useContext(PinnedEnvContext)
if (!context) {
throw new Error('usePinnedEnv must be used within a PinnedEnvProvider')
}
return context
}