Skip to content

Commit

Permalink
Merge pull request #109 from ImagingDataCommons/feat-download-study-d…
Browse files Browse the repository at this point in the history
…ialog

feat: add download study button
  • Loading branch information
igoroctaviano authored Oct 22, 2024
2 parents 34e7a2f + 523a390 commit bd430c9
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ class App extends React.Component<AppProps, AppState> {
showWorklistButton={false}
onServerSelection={this.handleServerSelection}
showServerSelectionButton={false}
appConfig={this.props.config}
/>
<Layout.Content style={layoutContentStyle}>
<FaSpinner />
Expand Down Expand Up @@ -482,6 +483,7 @@ class App extends React.Component<AppProps, AppState> {
onServerSelection={this.handleServerSelection}
onUserLogout={isLogoutPossible ? onLogout : undefined}
showServerSelectionButton={enableServerSelection}
appConfig={this.props.config}
/>
<Layout.Content style={layoutContentStyle}>
{worklist}
Expand All @@ -500,6 +502,7 @@ class App extends React.Component<AppProps, AppState> {
onServerSelection={this.handleServerSelection}
onUserLogout={isLogoutPossible ? onLogout : undefined}
showServerSelectionButton={enableServerSelection}
appConfig={this.props.config}
/>
<Layout.Content style={layoutContentStyle}>
<ParametrizedCaseViewer
Expand All @@ -523,6 +526,7 @@ class App extends React.Component<AppProps, AppState> {
onServerSelection={this.handleServerSelection}
onUserLogout={isLogoutPossible ? onLogout : undefined}
showServerSelectionButton={enableServerSelection}
appConfig={this.props.config}
/>
<Layout.Content style={layoutContentStyle}>
<ParametrizedCaseViewer
Expand All @@ -546,6 +550,7 @@ class App extends React.Component<AppProps, AppState> {
onServerSelection={this.handleServerSelection}
onUserLogout={isLogoutPossible ? onLogout : undefined}
showServerSelectionButton={enableServerSelection}
appConfig={this.props.config}
/>
Logged out
</Layout>
Expand Down
9 changes: 9 additions & 0 deletions src/AppConfig.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ export interface OidcSettings {
endSessionEndpoint?: string
}

export interface DownloadStudyDialogSettings {
description: string
instructions: Array<{
command: string
label: string
}>
}

export default interface AppConfig {
/**
* Currently, only one server is supported. However, support for multiple
Expand All @@ -94,4 +102,5 @@ export default interface AppConfig {
enableServerSelection?: boolean
mode?: string
preload?: boolean
downloadStudyDialog?: DownloadStudyDialogSettings
}
161 changes: 161 additions & 0 deletions src/components/DownloadStudySeriesDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, { useCallback, useState } from 'react'
import { PaperClipOutlined } from '@ant-design/icons'
import AppConfig, { DownloadStudyDialogSettings } from '../AppConfig'

/**
* If there's a downloadStudyDialogSettings in the appConfig object then it will return it, otherwise it will return the default settings
* @param appConfig the object with all app config
* @returns the downloadStudyDialogSettings config object
*/
const getConfig = (
appConfig: AppConfig
): DownloadStudyDialogSettings => {
if (appConfig.downloadStudyDialog != null) {
return appConfig.downloadStudyDialog
}

const defaultConfig = {
description:
'Follow the instructions below to download the study or series:',
instructions: [
{
command: 'pip install idc-index --upgrade',
label: 'First, install the idc-index python package:'
},
{
command: 'idc download {{StudyInstanceUID}}',
label: 'Then, to download the whole study, run:'
},
{
command: 'idc download {{SeriesInstanceUID}}',
label: "Or, to download just the active viewport's series, run:"
}
]
}

return defaultConfig
}

const DialogInstruction = ({
instruction
}: {
instruction: { command: string, label: string }
}): JSX.Element => {
const [message, setMessage] = useState('')
const { command, label } = instruction

const copyToClipboard = useCallback(async () => {
try {
await navigator.clipboard.writeText(command)
setMessage('Copied')
} catch (err) {
console.error('Failed to copy: ', err)
setMessage('Failed')
} finally {
setTimeout((): void => {
resetState()
}, 500)
}
}, [command])

const resetState = (): void => {
setMessage('')
}

return (
<div>
{typeof label === 'string' ? <section>{label}</section> : <></>}
<section
style={{
margin: '1rem 0',
padding: '0.25rem 0.5rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#EFFBFE',
alignItems: 'center'
}}
>
{command}
<div
style={{
position: 'relative',
display: 'flex',
height: '2rem',
alignItems: 'center'
}}
>
{message !== ''
? (
message
)
: (
<>
<div style={{ cursor: 'pointer' }} onClick={copyToClipboard}>
<PaperClipOutlined />
</div>
</>
)}
</div>
</section>
</div>
)
}

/**
* Gets the studyInstanceUID and seriesInstanceUID params from the URL using the location object
* based on the position of the /series and /studies strings
* @returns an object with the studyInstanceUID and seriesInstanceUID from the URL
*/
const getStudyAndSeriesInfo = (): {
studyInstanceUID: string
seriesInstanceUID: string
} => {
const urlParams = window.location.pathname.split('/')
const studiesIndex = urlParams.findIndex((param) => param === 'studies')
const seriesIndex = urlParams.findIndex((param) => param === 'series')
const studyInstanceUID = urlParams[studiesIndex + 1]
const seriesInstanceUID = urlParams[seriesIndex + 1]

return { studyInstanceUID, seriesInstanceUID }
}

const DownloadStudySeriesDialog = ({ appConfig }: { appConfig: AppConfig }): JSX.Element => {
const { studyInstanceUID, seriesInstanceUID } = getStudyAndSeriesInfo()
const config = getConfig(appConfig)

const replaceVariables = useCallback(
(text: string): string =>
text
.replace(/\{\{StudyInstanceUID\}\}/g, studyInstanceUID)
.replace(/\{\{SeriesInstanceUID\}\}/g, seriesInstanceUID),
[studyInstanceUID, seriesInstanceUID]
)

const instructions = config.instructions.map((instruction) => {
const { command, label } = instruction
return {
command: replaceVariables(command),
label: replaceVariables(label)
}
})

return (
<div style={{ width: '850px' }}>
<h1>{config.description}</h1>
<div
style={{ marginTop: '0.5rem', padding: '0.5rem' }}
className='mt-2 p-2'
>
{instructions.map((instruction) => (
<DialogInstruction
instruction={instruction}
key={instruction.command}
/>
))}
</div>
</div>
)
}

export default DownloadStudySeriesDialog
22 changes: 21 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
StopOutlined,
UnorderedListOutlined,
UserOutlined,
SettingOutlined
SettingOutlined,
CloudDownloadOutlined
} from '@ant-design/icons'
import { detect } from 'detect-browser'

Expand All @@ -29,6 +30,8 @@ import { RouteComponentProps, withRouter } from '../utils/router'
import NotificationMiddleware, { NotificationMiddlewareEvents } from '../services/NotificationMiddleware'
import { CustomError } from '../utils/CustomError'
import { v4 as uuidv4 } from 'uuid'
import DownloadStudySeriesDialog from './DownloadStudySeriesDialog'
import AppConfig from '../AppConfig'

interface HeaderProps extends RouteComponentProps {
app: {
Expand All @@ -46,6 +49,7 @@ interface HeaderProps extends RouteComponentProps {
onServerSelection: ({ url }: { url: string }) => void
onUserLogout?: () => void
showServerSelectionButton: boolean
appConfig: AppConfig
}

interface ExtendedCustomError extends CustomError {
Expand Down Expand Up @@ -278,6 +282,17 @@ class Header extends React.Component<HeaderProps, HeaderState> {
this.setState({ isServerSelectionModalVisible: true })
}

handleDownloadButtonClick = (appConfig: AppConfig): void => {
Modal.info({
title: 'Download Study or Series',
width: 1000,
content: (
<DownloadStudySeriesDialog appConfig={appConfig} />
),
onOk (): void {}
})
}

render (): React.ReactNode {
let user = null
if (this.props.user !== undefined) {
Expand Down Expand Up @@ -409,6 +424,11 @@ class Header extends React.Component<HeaderProps, HeaderState> {
<Col>
<Space direction='horizontal'>
{worklistButton}
<Button
icon={CloudDownloadOutlined}
tooltip='Download Study/Series'
onClick={() => this.handleDownloadButtonClick(this.props.appConfig)}
/>
{infoButton}
{debugButton}
{serverSelectionButton}
Expand Down

0 comments on commit bd430c9

Please sign in to comment.