Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ COPY craco.config.js .
COPY tsconfig.json .
COPY types ./types
COPY public ./public
COPY scripts ./scripts
COPY src ./src

RUN chmod +x scripts/*.sh


FROM lib AS app

Expand Down
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [0.45.1](https://github.com/ImagingDataCommons/slim/compare/v0.45.0...v0.45.1) (2025-12-17)


### Bug Fixes

* **about modal:** Address hash + disable select dialog for bulk ann + auto load ([#360](https://github.com/ImagingDataCommons/slim/issues/360)) ([2336601](https://github.com/ImagingDataCommons/slim/commit/2336601924fcc6c450b9478d16cec4d386419757))

# [0.44.0](https://github.com/ImagingDataCommons/slim/compare/v0.43.1...v0.44.0) (2025-10-27)


Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"name": "slim",
"version": "0.44.0",
"version": "0.45.1",
"private": true,
"author": "ImagingDataCommons",
"scripts": {
"start": "rm -rf ./node_modules/.cache/default-development && craco start",
"build": "craco build",
"build:firebase": "REACT_APP_CONFIG=gcp PUBLIC_URL=/ craco build",
"start": "rm -rf ./node_modules/.cache/default-development && ./scripts/set-git-env.sh craco start",
"dev": "npm run start",
"build": "./scripts/set-git-env.sh craco build",
"build:firebase": "REACT_APP_CONFIG=gcp PUBLIC_URL=/ ./scripts/set-git-env.sh craco build",
"lint": "ts-standard --env jest 'src/**/*.{tsx,ts}'",
"fmt": "ts-standard --env jest 'src/**/*.{tsx,ts}' --fix",
"test": "ts-standard --env jest 'src/**/*.{tsx,ts}' && craco test --watchAll=false",
"predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' craco build",
"predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' ./scripts/set-git-env.sh craco build",
"deploy": "gh-pages -d build",
"clean": "rm -rf ./build ./node_modules"
},
Expand Down Expand Up @@ -57,7 +58,7 @@
"craco-less": "^2.0.0",
"dcmjs": "^0.35.0",
"detect-browser": "^5.2.1",
"dicom-microscopy-viewer": "^0.48.15",
"dicom-microscopy-viewer": "^0.48.16",
"dicomweb-client": "^0.10.3",
"gh-pages": "^5.0.0",
"oidc-client": "^1.11.5",
Expand Down
20 changes: 20 additions & 0 deletions scripts/set-git-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
# Script to set git SHA environment variables for build

# Get current repository git SHA
if [ -d .git ] || git rev-parse --git-dir > /dev/null 2>&1; then
export REACT_APP_GIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo '')
else
export REACT_APP_GIT_SHA=''
fi

# Get dicom-microscopy-viewer git SHA
if [ -d ../dicom-microscopy-viewer/.git ] || (cd ../dicom-microscopy-viewer && git rev-parse --git-dir > /dev/null 2>&1); then
export REACT_APP_DMV_GIT_SHA=$(cd ../dicom-microscopy-viewer && git rev-parse HEAD 2>/dev/null || echo '')
else
export REACT_APP_DMV_GIT_SHA=''
fi

# Execute the command passed as arguments
exec "$@"

101 changes: 45 additions & 56 deletions src/components/CaseViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary
interface NaturalizedInstance {
SeriesInstanceUID: string
SOPInstanceUID: string
FrameOfReferenceUID?: string
ContainerIdentifier?: string
ReferencedSeriesSequence?: Array<{
SeriesInstanceUID: string
}>
Expand All @@ -40,11 +42,6 @@ interface NaturalizedInstance {
}>
}

interface ReferencedSlideResult {
slide: Slide | undefined
metadata: NaturalizedInstance
}

const findSeriesSlide = (slides: Slide[], seriesInstanceUID: string): Slide | undefined => {
return slides.find((slide: Slide) => {
return slide.seriesInstanceUIDs.find((uid: string) => {
Expand Down Expand Up @@ -82,65 +79,57 @@ function ParametrizedSlideViewer ({
const [derivedDataset, setDerivedDataset] = useState<NaturalizedInstance | null>(null)

useEffect(() => {
const seriesSlide = findSeriesSlide(slides, seriesInstanceUID)
if (seriesSlide !== null) {
setSelectedSlide(seriesSlide)
}
}, [seriesInstanceUID, slides])
const currentSlideMatchesSeries = selectedSlide?.seriesInstanceUIDs.some((uid: string) => uid === seriesInstanceUID) ?? false

useEffect(() => {
const findReferencedSlide = async ({ clients, studyInstanceUID, seriesInstanceUID }: {
clients: { [key: string]: DicomWebManager }
studyInstanceUID: string
seriesInstanceUID: string
}): Promise<ReferencedSlideResult | null> => await new Promise<ReferencedSlideResult | null>((resolve, reject) => {
try {
const allClients = Object.values(StorageClasses).map((storageClass) => clients[storageClass])
Promise.all(allClients.map(async (client) => {
const seriesMetadata = await client.retrieveSeriesMetadata({
studyInstanceUID: studyInstanceUID,
seriesInstanceUID: seriesInstanceUID
})
const [naturalizedSeriesMetadata] = seriesMetadata.map((metadata) => naturalizeDataset(metadata)) as NaturalizedInstance[]
if (selectedSlide === null || selectedSlide === undefined || !currentSlideMatchesSeries) {
const imageSlide = findSeriesSlide(slides, seriesInstanceUID)
if (imageSlide !== null && imageSlide !== undefined) {
setSelectedSlide(imageSlide)
setDerivedDataset(null)
return
}

if (naturalizedSeriesMetadata.ReferencedSeriesSequence != null) {
const referencedSeriesInstanceUID = naturalizedSeriesMetadata.ReferencedSeriesSequence[0].SeriesInstanceUID
const findReferencedSlide = async (): Promise<void> => {
const client = clients[StorageClasses.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE]
const derivedSeriesMetadata = await client.retrieveSeriesMetadata({
studyInstanceUID: studyInstanceUID,
seriesInstanceUID: seriesInstanceUID
})
const naturalizedDerivedMetadata = naturalizeDataset(derivedSeriesMetadata[0]) as NaturalizedInstance
if (
naturalizedDerivedMetadata.ReferencedSeriesSequence != null &&
naturalizedDerivedMetadata.ReferencedSeriesSequence.length > 0
) {
for (const referencedSeries of naturalizedDerivedMetadata.ReferencedSeriesSequence) {
const referencedImageSeriesUID = referencedSeries.SeriesInstanceUID
const referencedSlide = slides.find((slide: Slide) => {
return slide.seriesInstanceUIDs.find((uid: string) => {
return uid === referencedSeriesInstanceUID
})
return slide.seriesInstanceUIDs.some((uid: string) => uid === referencedImageSeriesUID)
})
resolve({ slide: referencedSlide, metadata: naturalizedSeriesMetadata })
if (referencedSlide !== null && referencedSlide !== undefined) {
setSelectedSlide(referencedSlide)
setDerivedDataset(naturalizedDerivedMetadata)
console.log('naturalizedDerivedMetadata', naturalizedDerivedMetadata)
return
}
}

const IMAGE_LIBRARY_CONCEPT_NAME_CODE = '111028'
const imageLibrary = naturalizedSeriesMetadata.ContentSequence?.find(
contentItem => contentItem.ConceptNameCodeSequence[0].CodeValue === IMAGE_LIBRARY_CONCEPT_NAME_CODE
)
if ((imageLibrary?.ContentSequence?.[0]?.ContentSequence?.[0]?.ReferencedSOPSequence?.[0]) != null) {
const referencedSOPInstanceUID = imageLibrary.ContentSequence[0].ContentSequence[0].ReferencedSOPSequence[0].ReferencedSOPInstanceUID
const referencedSlide = slides.find((slide: Slide) => {
return slide.volumeImages.find((image: { SOPInstanceUID: string }) => {
return image.SOPInstanceUID === referencedSOPInstanceUID
})
}
const IMAGE_LIBRARY_CONCEPT_NAME_CODE = '111028'
const imageLibrary = naturalizedDerivedMetadata.ContentSequence?.find(
contentItem => contentItem.ConceptNameCodeSequence[0].CodeValue === IMAGE_LIBRARY_CONCEPT_NAME_CODE
)
if ((imageLibrary?.ContentSequence?.[0]?.ContentSequence?.[0]?.ReferencedSOPSequence?.[0]) != null) {
const referencedSOPInstanceUID = imageLibrary.ContentSequence[0].ContentSequence[0].ReferencedSOPSequence[0].ReferencedSOPInstanceUID
const referencedSlide = slides.find((slide: Slide) => {
return slide.volumeImages.find((image: { SOPInstanceUID: string }) => {
return image.SOPInstanceUID === referencedSOPInstanceUID
})
resolve({ slide: referencedSlide, metadata: naturalizedSeriesMetadata })
}
})).catch(reject)
} catch (error) {
reject(error)
})
setSelectedSlide(referencedSlide)
setDerivedDataset(naturalizedDerivedMetadata)
}
}
})

if (selectedSlide === null || selectedSlide === undefined) {
void findReferencedSlide({ clients, studyInstanceUID, seriesInstanceUID }).then((result: ReferencedSlideResult | null) => {
if (result !== null && result !== undefined) {
setSelectedSlide(result.slide)
setDerivedDataset(result.metadata)
}
}).catch(error => {
console.error('Error finding referenced slide:', error)
})
void findReferencedSlide()
}
}, [slides, clients, studyInstanceUID, seriesInstanceUID, selectedSlide])

Expand Down
115 changes: 92 additions & 23 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
Badge,
Collapse,
Radio,
Tooltip
Tooltip,
Typography
} from 'antd'
import {
ApiOutlined,
Expand All @@ -37,6 +38,47 @@ import AppConfig from '../AppConfig'
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser'
import DicomWebManager from '../DicomWebManager'

const aboutModalCopyTooltips: [React.ReactNode, React.ReactNode] = ['Copy hash', 'Copied!']

const aboutModalStyles: Record<string, React.CSSProperties> = {
container: {
textAlign: 'center',
lineHeight: 1.6,
paddingRight: 25,
fontSize: '1rem'
},
title: {
fontSize: '1.4rem',
fontWeight: 700,
marginBottom: 4
},
subtitle: {
fontSize: '1.15rem',
fontWeight: 600,
marginBottom: 12
},
link: {
display: 'inline-block',
marginBottom: 16
},
section: {
marginBottom: 12
},
label: {
fontWeight: 600,
display: 'block',
marginBottom: 4
},
bodyText: {
marginBottom: 4
},
code: {
display: 'inline-block',
wordBreak: 'break-all',
fontSize: '0.85rem'
}
}

interface HeaderProps extends RouteComponentProps {
app: {
name: string
Expand Down Expand Up @@ -182,45 +224,72 @@ class Header extends React.Component<HeaderProps, HeaderState> {
'unknown'
const viewerVersion = viewerVersionRaw.replace(/^[^0-9]*/, '')

const renderHashText = (hash: string | null): JSX.Element => {
if (hash == null) {
return (
<Typography.Text code style={aboutModalStyles.code}>
unknown
</Typography.Text>
)
}
return (
<Typography.Text
code
style={aboutModalStyles.code}
copyable={{
text: hash,
tooltips: aboutModalCopyTooltips
}}
>
{hash}
</Typography.Text>
)
}

Modal.info({
width: 480,
title: null,
centered: true,
content: (
<div style={{ textAlign: 'center', lineHeight: 1.6, paddingRight: 25 }}>
<div style={{ fontSize: '2.2rem', fontWeight: 700, marginBottom: 4 }}>{this.props.app.name}</div>
<div style={{ fontSize: '2rem', fontWeight: 600 }}>{this.props.app.version}</div>
<div style={{ fontSize: '1rem', marginBottom: 16 }}>
<a href={this.props.app.homepage} target='_blank' rel='noreferrer'>
{this.props.app.homepage}
</a>
</div>

<div style={{ marginBottom: 12 }}>
<div style={{ fontWeight: 600 }}>Commit Hash</div>
<code>{slimCommit ?? 'unknown'}</code>
<div style={aboutModalStyles.container}>
<Typography.Title level={3} style={aboutModalStyles.title}>
<Typography.Link href={this.props.app.homepage} target='_blank' rel='noreferrer'>
{this.props.app.name}
</Typography.Link>
</Typography.Title>
<Typography.Text style={aboutModalStyles.subtitle}>
{this.props.app.version}
</Typography.Text>

<div style={aboutModalStyles.section}>
<Typography.Text style={aboutModalStyles.label}>Commit Hash</Typography.Text>
{renderHashText(slimCommit)}
</div>

<div style={{ marginBottom: 12 }}>
<div style={{ fontWeight: 600 }}>
<div style={aboutModalStyles.section}>
<Typography.Text style={aboutModalStyles.label}>
<a
href='https://github.com/MGHComputationalPathology/dicom-microscopy-viewer'
target='_blank'
rel='noreferrer'
>
DICOM Microscopy Viewer
</a>
</div>
<div>Version {viewerVersion}</div>
<code>{viewerCommit ?? 'unknown'}</code>
</Typography.Text>
<Typography.Text style={aboutModalStyles.bodyText}>
Version {viewerVersion}
</Typography.Text>
{renderHashText(viewerCommit)}
</div>

<div style={{ marginBottom: 12 }}>
<div style={{ fontWeight: 600 }}>Current Browser &amp; OS</div>
<div>
<div style={aboutModalStyles.section}>
<Typography.Text style={aboutModalStyles.label}>Current Browser &amp; OS</Typography.Text>
<Typography.Text style={aboutModalStyles.bodyText}>
{environment.browser.name ?? 'Unknown'} {environment.browser.version ?? ''}
</div>
<div>{environment.os.name ?? 'Unknown OS'}</div>
</Typography.Text>
<Typography.Text style={aboutModalStyles.bodyText}>
{environment.os.name ?? 'Unknown OS'}
</Typography.Text>
</div>
</div>
),
Expand Down
Loading