Skip to content
Open
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: 5 additions & 0 deletions .github/workflows/deploy-to-firebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ jobs:
node-version: 24
cache: pnpm

- name: Disable strict dep builds check globally
run: |
pnpm config set strict-dep-builds false --global
echo "strict-dep-builds=false" >> ~/.npmrc

- name: Resolve DMV branch for preview
id: dmv
if: github.event_name == 'pull_request'
Expand Down
4 changes: 4 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ shamefully-hoist=true

# react-scripts 5 peer dependency ranges are outdated.
strict-peer-dependencies=false

# pnpm 11+ requires explicit approval for build scripts from dependencies.
# Disable strict dependency build checking to avoid CI failures.
strict-dep-builds=false
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ _Slim_ also supports interactive visualization of image annotations and analysis

**Raster graphics:**

- [DICOM Segmentation](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.51.html) instances that contain binary or fractional segmentation masks
- [DICOM Segmentation](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.51.html) instances that contain binary or fractional segmentation masks, including TILED_SPARSE segmentations at non-standard resolution levels (e.g., segmentations created from rescaled image patches that don't match any pyramid level)
- [DICOM Parametric Map](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_A.75.html) instances that contain saliency maps, attention maps, class activation maps, and similar derived images

Fractional segmentations and parametric maps show an in-viewport color legend when at least one overlay is visible. The legend is collapsible and its per-item visibility toggles stay in sync with the switches in the right-hand panel.

Clicking on a segment label in the right-hand panel zooms the viewport to that segment's bounding box, providing quick navigation to regions of interest.

| | DICOM IOD |
| :-: | :-------- |
| <img src="docs/screenshots/IDC_CPTAC_C3N-01016-22_segmentation.png" alt="IDC CPTAC Segmentation" width="350"> | Segmentation |
Expand Down Expand Up @@ -162,6 +164,27 @@ Custom selections are stored in `localStorage`, re-apply the current Bearer toke

See [docs/CONFIGURATION.md](docs/CONFIGURATION.md#runtime-server-selection-header-button) for details.

#### Runtime OIDC Configuration

When `enableServerSelection` is enabled, users can also configure OIDC authentication settings at runtime through the server selection modal. This allows connecting to servers that require different authentication providers without redeploying the application.

To use a custom OIDC provider, enter a JSON configuration in the OIDC config field:

```json
{
"authority": "https://accounts.google.com",
"clientId": "your-client-id.apps.googleusercontent.com",
"scope": "email profile openid https://www.googleapis.com/auth/cloud-healthcare",
"grantType": "implicit"
}
```

Required fields: `authority`, `clientId`, `scope`

Optional fields: `grantType`, `authorizationEndpoint`, `endSessionEndpoint`

The OIDC configuration is cached in localStorage. If not provided, the deployment's default OIDC settings are used.

### Handling mixed content and HTTPS

When deploying Slim with HTTPS, you may encounter mixed content scenarios where your PACS/VNA server returns HTTP URLs in its responses. This commonly occurs when:
Expand Down
1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ shamefullyHoist: true
strictPeerDependencies: false

allowBuilds:
'@parcel/watcher': true
core-js: true
core-js-pure: true
dicom-microscopy-viewer: true
Expand Down
95 changes: 90 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
} from 'react-router-dom'

import type AppConfig from './AppConfig'
import type { ErrorMessageSettings, ServerSettings } from './AppConfig'
import type {
ErrorMessageSettings,
OidcSettings,
ServerSettings,
} from './AppConfig'
import type { AuthManager, User } from './auth'
import OidcManager from './auth/OidcManager'
import AppLoading from './components/AppLoading'
Expand Down Expand Up @@ -200,7 +204,7 @@
}

class App extends React.Component<AppProps, AppState> {
private readonly auth?: AuthManager
private auth?: AuthManager
private reauthInProgress = false
private unsubscribeAuthorization?: () => void

Expand Down Expand Up @@ -537,9 +541,34 @@
}
}

handleServerSelection = async ({ url }: { url: string }): Promise<void> => {
handleServerSelection = async ({
url,
oidc,
}: {
url: string
oidc?: OidcSettings
}): Promise<void> => {
const trimmedUrl = url.trim()
console.info('select DICOMweb server: ', trimmedUrl)

/** Handle OIDC configuration change */
if (oidc != null) {
console.info('applying custom OIDC configuration')
const { protocol, host } = window.location
const baseUri = `${protocol}//${host}`
const appUri = joinUrl(this.props.config.path, baseUri)
this.auth = new OidcManager(appUri, oidc)
/** Re-subscribe to authorization changes */
if (this.unsubscribeAuthorization != null) {
this.unsubscribeAuthorization()
}
this.unsubscribeAuthorization = this.auth.onAuthorizationChange(
(authorization) => {
this.applyAuthorization(authorization)
},
)
}

if (
trimmedUrl === '' ||
window.localStorage.getItem('slim_server_selection_mode') === 'default'
Expand Down Expand Up @@ -726,16 +755,72 @@
}
}

/**
* Parses cached OIDC config from localStorage.
* Handles both JSON and JavaScript object notation (unquoted keys).
*/
private static parseCachedOidcConfig(): OidcSettings | undefined {
const cachedOidcConfig = window.localStorage.getItem('slim_oidc_config')
if (cachedOidcConfig == null || cachedOidcConfig.trim() === '') {
return undefined
}
try {
/** Convert JS object notation to JSON (quote unquoted keys) */
const normalized = cachedOidcConfig
.trim()
.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3')

Check warning on line 771 in src/App.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\w' instead of '[a-zA-Z0-9_]'.

See more on https://sonarcloud.io/project/issues?id=ImagingDataCommons_slim&issues=AaCQyKcw2bF7KuJxOHLh&open=AaCQyKcw2bF7KuJxOHLh&pullRequest=438
const parsed = JSON.parse(normalized)
if (
typeof parsed === 'object' &&
parsed !== null &&
typeof parsed.authority === 'string' &&
typeof parsed.clientId === 'string' &&
typeof parsed.scope === 'string'
) {
return {
authority: parsed.authority,
clientId: parsed.clientId,
scope: parsed.scope,
grantType: parsed.grantType,
authorizationEndpoint: parsed.authorizationEndpoint,
endSessionEndpoint: parsed.endSessionEndpoint,
}
}
} catch {
/** Invalid format, ignore cached config */
}
return undefined
}

componentDidMount(): void {
// Restore cached server selection if it exists
/** Restore cached OIDC config and server selection if they exist */
const cachedOidcConfig = App.parseCachedOidcConfig()
const cachedServerUrl = window.localStorage.getItem('slim_selected_server')
const cachedMode = window.localStorage.getItem('slim_server_selection_mode')

/**
* Apply cached OIDC config if present (even for default server mode).
* This allows users to configure OIDC once and have it persist.
*/
if (cachedOidcConfig != null) {
console.info('restoring cached OIDC configuration')
const { protocol, host } = window.location
const baseUri = `${protocol}//${host}`
const appUri = joinUrl(this.props.config.path, baseUri)
this.auth = new OidcManager(appUri, cachedOidcConfig)
}

if (
cachedMode === 'custom' &&
cachedServerUrl !== null &&
cachedServerUrl !== undefined &&
cachedServerUrl !== ''
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.handleServerSelection({ url: cachedServerUrl })
this.handleServerSelection({
url: cachedServerUrl,
oidc: cachedOidcConfig,
})
}

if (this.auth != null) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface AttributeGroup {
}

interface DescriptionProps {
header?: string
header?: React.ReactNode
icon?: React.ComponentType<Record<string, never>>
attributes: Attribute[]
selectable?: boolean
Expand Down
Loading