Compare commits

..

No commits in common. "master" and "v0.1.14" have entirely different histories.

3959 changed files with 83403 additions and 517393 deletions

View File

@ -1,346 +0,0 @@
---
name: ohif-test-agent
description: Generate runnable Playwright E2E tests for the OHIF Viewer using its custom fixture system, page objects, and normalized WebGL viewport coordinates. Use this skill whenever the user asks to write, add, modify, or debug tests in an OHIF/Viewers context — including vague asks like "write a test for X" when working in the OHIF repo, tests touching platform/app/tests/, or anything involving Cornerstone viewports, DICOM studies, measurements, segmentations, or OHIF modes/extensions. Prefer this skill over generic test-writing even if the user doesn't say "Playwright" or "E2E" explicitly.
---
# OHIF Test Agent
This skill teaches you to generate correct, runnable Playwright end-to-end tests for the OHIF Viewer. Follow the workflow below.
## Environment model
This package follows the agentskills.io SKILL.md convention. `SKILL.md` is the entire behavior contract — there is no separate runtime entrypoint.
## Workflow: how to write a new OHIF test
1. **Classify the feature.** What area does the test belong to — a measurement tool, segmentation hydration, contour panel interaction, MPR layout, crosshairs, tag browser, etc.? The area determines the mode, the StudyInstanceUID, and the seed spec you'll read.
2. **Read the seed spec.** Consult [references/patterns-by-feature.md](references/patterns-by-feature.md) to find the canonical existing spec for that area. Read it end-to-end before writing. This is the single most important step — OHIF specs follow consistent idioms that are easier to mimic than to reconstruct from first principles. (This mirrors Playwright's own agent guidance: use seed tests as the example for generated tests.)
3. **Scaffold from the template.** Start from [assets/spec-template.ts](assets/spec-template.ts) — or copy the seed spec and adapt.
4. **Look up specifics in the source, not from memory.** The reference files [page-objects.md](references/page-objects.md) and [utilities.md](references/utilities.md) capture the **stable rules** — fixture keys, import conventions, access idioms, the reasons certain things trip people up. They deliberately do not enumerate methods. For the current method surface or a utility's exact signature, open the relevant file under `tests/pages/` or `tests/utils/` — the source evolves, and the source is always right. The seed spec you picked in step 2 is usually the fastest second source, because it co-evolves with the API.
5. **Run the test when execution is available.** `pnpm run test:e2e:ci` runs the whole suite, but for iteration use `TEST_ENV=true pnpm exec playwright test tests/YourNew.spec.ts` (or the Playwright VS Code extension). Invoke Playwright directly for targeted flags; `pnpm run test:e2e -- ...` inserts a `--` separator that can prevent Playwright from parsing options such as `--update-snapshots` and `--reporter`.
6. **If runtime execution is unavailable, do static validation.** Validate import source, fixture keys, normalized viewport usage, UID/mode pairing, and hydration/tracking prompt handling. Then report clearly that execution was not performed.
7. **If it fails, triage before debugging.** Use [references/failure-triage.md](references/failure-triage.md) — most OHIF test failures are timing / hydration, not real regressions.
## Architecture
OHIF uses Playwright with a custom fixture system. Tests are **not** vanilla Playwright — they import `test`, `expect`, and utilities from `./utils`, which re-exports an extended test runner that injects page objects.
```text
playwright.config.ts → Chromium-only, port 3335, data-cy as testId
└─ tests/utils/fixture.ts → Extends playwright-test-coverage, injects page objects
└─ tests/*.spec.ts → Each imports { test, expect, ... } from './utils'
├─ tests/pages/ → Page objects (ViewportPageObject, MainToolbarPageObject, …)
└─ tests/utils/ → Utilities (visitStudy, checkForScreenshot, screenShotPaths, …)
```
Why the custom fixture matters: the page objects are created for each test and bound to the right Playwright `page`. If you `new ViewportPageObject(page)` manually, you skip the fixture wiring and some sub-objects won't resolve correctly.
### Import rule
```ts
// Correct
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Wrong — will compile but fixtures won't be injected
import { test, expect } from '@playwright/test';
```
A few utilities (`press`, `downloadAsString`, the `assert*` helpers) are NOT re-exported from `./utils`. See [references/utilities.md](references/utilities.md) for the correct import path per utility.
## The viewport is WebGL
OHIF renders medical images onto a WebGL canvas. You cannot query *canvas* pixels by CSS selector. Use **normalized coordinates** (01 range, top-left is `{x:0, y:0}`) for clicks and drags, and **visual regression** (screenshot comparison) for canvas assertions. (Not everything in the viewport is canvas — some overlays render as SVG you *can* query via DOM, e.g. a vector overlay's color through `getSvgAttribute`. The canvas rule is about raster output painted onto the WebGL surface.)
```ts
const activeViewport = await viewportPageObject.active;
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }]); // click center
await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]); // draw two points
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }], 'right'); // right-click
await activeViewport.normalizedDragAt({
start: { x: 0.3, y: 0.3 },
end: { x: 0.7, y: 0.7 },
});
```
Pixel coordinates (`clickAt`, `doubleClickAt`) exist but prefer normalized for portability across viewport sizes.
For DOM-rendered state (panel counts, dialog text, overlay text values, button enabled states), assert directly:
```ts
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
expect(count).toBe(1);
```
## Study loading lifecycle
Every test follows this sequence. Skipping steps causes flakiness:
```ts
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000); // 2s delay is the community norm
});
```
`visitStudy` navigates to `/{mode}/ohif?StudyInstanceUIDs={uid}`, waits for `domcontentloaded`, then `networkidle`, then the explicit delay. Default delay is `0`, but most specs pass `2000` to let the first render settle.
Delay by scene type (observed across the current suite, not a rule to apply blindly):
| Scene | Delay |
|-------|-------|
| Default (viewer mode, 2D/MPR/3D layouts, crosshairs) | `2000` |
| `mode: 'tmtv'` | `10000` — PET fusion + SUV calculation takes noticeably longer |
Start at the convention for your scene; ramp only if the test flakes on initial render. 3D layouts in `viewer` mode already stay at `2000` — the stabilization problem there is solved with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not with a longer `visitStudy` delay.
If the study has DICOM SEG, RT, or SR data, OHIF asks whether to hydrate. Handle it:
```ts
await leftPanelPageObject.loadSeriesByModality('SEG'); // or 'RTSTRUCT', 'SR'
await page.waitForTimeout(3000); // allow the prompt to appear
await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible();
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
```
The first measurement you create triggers a "start tracking?" prompt:
```ts
await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
```
For 3D / MPR scenes, wrap stabilization in `attemptAction(() => reduce3DViewportSize(page), 10, 100)` or insert a `page.waitForTimeout(...)` after the layout change before asserting.
## Wait for renders, don't sleep
`page.waitForTimeout(...)` after an action that re-renders the viewport is a smell. The viewports tell us when they're done — use that signal. `tests/utils/waitForViewportsRendered.ts` exposes three helpers, all barrel-exported from `./utils`:
- `waitForViewportRenderCycle(page)` — wait for the next full cycle: a viewport enters `needsRender`, then **all** viewports report `rendered` (and volumes are loaded, by default).
- `waitForViewportsRendered(page)` — only the second half: wait until all viewports are `rendered`. Use this when the action has already requested a render before you started waiting (e.g. a layout change or `loadSeriesByDescription`).
- `waitForAnyViewportNeedsRender(page)` — only the first half. Rarely needed directly.
The canonical idiom — **start the watcher before the action, await it after**:
```ts
// start watching for the next render cycle
const viewportRenderCycle = waitForViewportRenderCycle(page);
await action(); // e.g. segmentationHydration.yes.click(), layoutSelection.MPR.click(), addSegmentation, etc.
// wait for the render to finish
await viewportRenderCycle;
await check(); // e.g. checkForScreenshot, count assertion, overlay text
```
Why "start before"? `waitForViewportRenderCycle` first waits for a viewport to enter `needsRender`. If you start it **after** the action, that transition may already be over and you'll hang until the timeout. Starting it first captures the cycle the action is about to trigger.
When to use which:
| Situation | Helper |
|-----------|--------|
| Click that triggers a re-render and you want to assert after | `waitForViewportRenderCycle(page)` started before the click |
| Layout switch / series load — render already in flight | `await waitForViewportsRendered(page)` after the call |
| Compose with another await (e.g. screenshot the same time as load) | Save the promise, `await` it later |
Replace patterns like this:
```ts
// ❌ Sleep-and-pray
await action();
await page.waitForTimeout(5000);
await checkForScreenshot(...);
// ✅ Wait on the actual signal
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await checkForScreenshot(...);
```
This shaves real wall-clock time off the suite and removes a class of flake (sleep too short → flake; sleep too long → slow). `tests/SEGHydrationFromMPR.spec.ts` is the canonical seed for this pattern.
Caveats:
- These helpers wait on Cornerstone viewport state. They won't help for purely DOM-side state (panel rows appearing, dialogs opening) — for those, prefer `expect(locator).toHaveCount(n)` / `toBeVisible()` which auto-retry, or `expect.toPass({ timeout })`.
- For some actions (hanging-protocol changes are the documented example) the viewport doesn't transition through `needsRender` synchronously — those still need a short `waitForTimeout`. The source comment in `waitForViewportsRendered.ts` calls this out.
## Fixture-injected page objects
Destructure these from the test function argument. **Never `new` them manually.**
```ts
test('my test', async ({
page,
viewportPageObject,
mainToolbarPageObject,
leftPanelPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // note the capital D — this matches the fixture key
notFoundStudyPageObject,
}) => { ... });
```
Two page objects are **not** fixture-injected:
- `DataOverlayPageObject` — reach via `viewportPageObject.getById(id).overlayMenu.dataOverlay`.
- `DicomTagBrowserPageObject` — reach via `DOMOverlayPageObject.dialog.dicomTagBrowser`.
See [references/page-objects.md](references/page-objects.md) for fixture rules and a map of which file covers which concern; read the `.ts` file under `tests/pages/` for the current method surface.
## When the control you need has no page object yet
A spec must not reach for `page.getByTestId(...)` / `getByRole(...)` directly for
application controls. If the button, menu, dialog, or field you need isn't already
exposed by a page object, **add it to one — or create a new page object — instead of
inlining a raw selector.** Raw selectors in a spec are the clearest sign a test was
written without reading the existing suite: they duplicate locators, bypass the
fixture wiring, and rot silently when the DOM changes.
Where new coverage goes:
| What you need | Where it belongs |
|---|---|
| A toolbar button or tool (Zoom, Pan) | a getter on `MainToolbarPageObject`, next to `crosshairs` / `measurementTools` |
| A menu, prompt, context menu, or small dialog | `DOMOverlayPageObject` |
| A substantial dialog with its own fields (User Preferences) | its **own** page object class, reached through `DOMOverlayPageObject` — follow `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Each field/row inside that dialog | a method or sub-object on the dialog's page object — not a raw selector in the spec |
| A side-panel control | `LeftPanelPageObject` / `RightPanelPageObject` |
If the control has no `data-cy`, **add `data-cy` to the source component** and target
it — don't fall back to `getByRole`/text selectors, which are brittle and
locale-sensitive (`testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`). Call out any `data-cy` you add so it ships in the same PR.
**Worked example — "set the Zoom hotkey in User Preferences":** the options menu, the
preferences dialog, each preference field, and the Zoom toolbar button should all be
page-object surface — e.g. `mainToolbarPageObject.zoom`,
`DOMOverlayPageObject.optionsMenu.settings.click()`,
`DOMOverlayPageObject.dialog.userPreferences.hotkey('Zoom').set('q')`. The spec then
reads as intent, not as a pile of `getByTestId` calls.
## Assert the effect, not just the attribute
Prefer asserting the actual rendered result over a proxy attribute. Activating Zoom and
checking `data-active="true"` confirms the *button* toggled — not that zoom works. Drag
on the viewport and assert the image actually zoomed (a viewport-scoped screenshot, or a
measurable state change). Attribute checks are fine as a secondary signal, not the whole
test.
## Visual regression
**Direction:** the suite is moving off *full-app* screenshots — not off screenshots
altogether. "Avoid screenshots" means: don't screenshot the whole page, and don't
screenshot something that has a faithful DOM/state signal. It does **not** mean avoid
screenshots for output that is genuinely canvas-only — for that output a screenshot is
the correct and required tool, and you should use it without apology. Older specs that
screenshot the whole page are the legacy pattern being phased out; viewport-scoped
screenshots are not.
### Screenshot vs. DOM assertion — how to choose
Reach for the cheapest *faithful* signal, in this order:
1. **A faithful DOM/SVG/state signal exists → assert on it.** Panel counts, dialog and
overlay text, enabled/disabled state, and any overlay that renders as SVG (a vector
overlay's color is readable via `getSvgAttribute`) all have a DOM representation — assert
on it directly, no screenshot.
2. **The thing under test is painted onto the WebGL canvas with no DOM representation → a
screenshot is correct and required.** Raster output on the canvas exposes no attribute to
read for a painted pixel. Scope a `checkForScreenshot` to the viewport (pane or grid) and
assert it — this is the right tool, not a last resort, whenever what you're verifying is
the rendered canvas itself.
3. **Never substitute a service/state read for a render assertion.** Reading a service's
state (any `window.services...`) asserts the *data model*, not the pixels the user sees —
it passes even when rendering is broken. `page.evaluate(() => window.services...)` is an
escape hatch for *setup*, not for *appearance* assertions.
For anything drawn onto the WebGL canvas with no DOM signal, compare a screenshot scoped to a specific viewport or the viewport grid:
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid, // scope to the viewport grid — not the whole page
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
`checkForScreenshot` retries up to 10 times at 500 ms intervals. Use `screenShotPaths.<category>.<name>` rather than a hand-typed string — the tree of valid keys lives in `tests/utils/screenShotPaths.ts`.
Rules (apply to all new screenshot assertions):
- **Use the object form.** The positional form is legacy; don't introduce it in new code, and don't treat existing positional-form usage as a pattern to copy.
- **Never screenshot the full app.** Full-page screenshots include panels, toolbars, and dialogs that drift independently of what's under test and make baselines fragile. Scope by passing a `locator``viewportPageObject.grid` for the grid, or a specific viewport pane. A bare `normalizedClip: { x: 0, y: 0, width: 1, height: 1 }` with no `locator` is **not** scoping — it clips to the full page. Use `normalizedClip` only to target a sub-region *of a locator* (e.g. a scrollbar strip). If you reach for `fullPage: true`, stop and pick a locator.
- **Do not tune `maxDiffPixelRatio` or `threshold`** to make a screenshot pass. If a baseline mismatches, regenerate it after a human review of the diff, or fix the underlying flake.
## Playwright config facts worth remembering
| Setting | Value | Why |
|---------|-------|-----|
| `baseURL` | `http://localhost:3335` | OHIF e2e uses 3335, not 3000 |
| `testIdAttribute` | `data-cy` | `getByTestId(...)` maps to `[data-cy="..."]` |
| `browser` | Chromium only | Firefox/WebKit disabled (SharedArrayBuffer + stability) |
| `retries` | 3 in CI, 0 locally | Flaky rendering needs CI retries |
| `workers` | 6 in CI, undefined locally | Parallel execution |
| `globalTimeout` / `timeout` | 800_000 ms | Medical image loads are slow |
| `actionTimeout` | 10_000 ms | Per-action cap |
| `webServer.command` | `cross-env APP_CONFIG=config/e2e.js COVERAGE=true OHIF_PORT=3335 nyc yarn start` | e2e config + coverage |
## Test data: which study for which test
| StudyInstanceUID | Mode | Used for |
|------------------|------|----------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | General measurements, annotations, context menu |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | 3D, MPR, crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | Labelmap SEG |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | RTSTRUCT/contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR hydration |
Full mapping in [references/patterns-by-feature.md](references/patterns-by-feature.md). **Do not invent UIDs** — they must exist on the e2e data server.
## Rules (short, so they're actually read)
1. Import `test`, `expect`, and utilities from `./utils`, not `@playwright/test`.
2. Destructure fixture-injected page objects; don't `new` them.
3. Use normalized coordinates (01) for viewport interactions.
4. Use `visitStudy` with a real UID, correct mode, and a non-zero delay (2000 is conventional).
5. Handle hydration and measurement-tracking prompts where applicable.
6. Choose the faithful signal: DOM/SVG assertions where the rendered result has one (panels, dialogs, overlay text, SVG/vector overlays), a viewport-scoped screenshot when what you're verifying is canvas-only raster output, and never a `window.services` state read standing in for a render check. Screenshots use the object form, scoped via a `locator` — never the full app.
7. Use `data-cy` selectors (already wired via `testIdAttribute`).
8. When an assertion needs retry tolerance, wrap it in `expect.toPass({ timeout })`.
9. Test in the correct mode — segmentation tools aren't available in `viewer` mode.
10. If a utility isn't exported from `./utils`, import from the deeper path (see [references/utilities.md](references/utilities.md)).
11. After an action that re-renders the viewport, prefer `waitForViewportRenderCycle(page)` (started before the action) over `page.waitForTimeout(...)`. See the "Wait for renders, don't sleep" section.
12. Don't inline raw `page.getByTestId(...)` / `getByRole(...)` for app controls. If a control has no page object, create or augment one (see "When the control you need has no page object yet"), adding a `data-cy` to the source if needed.
13. Assert the actual effect (e.g. the image zoomed), not just a proxy attribute like `data-active`.
## Pre-output self-check (mandatory)
Before returning a generated OHIF test, confirm all items:
1. Imports `test`/`expect` from `./utils` (not `@playwright/test`).
2. Uses fixture-injected keys and exact casing (especially `DOMOverlayPageObject`).
3. Uses normalized viewport interactions (`normalizedClickAt` / `normalizedDragAt`) unless there is a strong reason otherwise.
4. Uses a valid canonical StudyInstanceUID and compatible mode.
5. Handles hydration or measurement tracking prompts when the workflow requires them.
6. Uses the faithful signal for each assertion — DOM/SVG where the result has a DOM representation, a viewport-scoped screenshot when what's verified is canvas-only raster output, and never a `window.services` state read in place of a render check. Any `checkForScreenshot` call uses the object form, scoped via a `locator` (viewport pane or grid) — no full-app screenshots.
7. Replaces `page.waitForTimeout(...)` after viewport-rendering actions with `waitForViewportRenderCycle(page)` (started before the action) — keeps `waitForTimeout` only for non-render waits like the hydration prompt in `beforeEach`.
8. If execution was skipped, states that explicitly and provides concrete run commands.
9. Every application control is reached through a page object — no raw `getByTestId`/`getByRole` in the spec for buttons, menus, dialogs, or fields. Any control not already covered was added to the right page object (or a new one), with a source `data-cy` if it lacked one.
10. Assertions verify the real effect where feasible (e.g. the image visibly zoomed), not only an attribute toggle.
## Output contract (for non-executing agents)
When execution cannot be performed in the current environment, the response should include:
1. The test code.
2. Assumptions made (if any).
3. Static checks that were verified.
4. What still must be run locally and exact commands to run.
## When to consult each reference
- **Before writing** → [references/patterns-by-feature.md](references/patterns-by-feature.md). Pick the seed spec for the feature area and read it. The seed spec is the closest thing to a live API example because it co-evolves with the code.
- **For a stable rule or idiom** (fixture keys, import paths, panel-access order, capital-D quirk, object-param convention) → [references/page-objects.md](references/page-objects.md), [references/utilities.md](references/utilities.md).
- **For a method name, property, or signature** → read the source under `tests/pages/` or `tests/utils/`. Do not rely on a static table for these; they drift as the code is refactored.
- **When a test fails** → [references/failure-triage.md](references/failure-triage.md).

View File

@ -1,54 +0,0 @@
import {
checkForScreenshot,
expect,
screenShotPaths,
test,
visitStudy,
waitForViewportRenderCycle,
} from './utils';
test.beforeEach(async ({ page }) => {
// Pick the right UID + mode for your feature (see references/patterns-by-feature.md)
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test.describe('FEATURE NAME', () => {
test('describes the behaviour in one sentence', async ({
page,
viewportPageObject,
mainToolbarPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // capital D on purpose
}) => {
// 1. Arrange — select a tool, open a panel, etc.
// 2. Act — interact with the viewport.
// Prefer normalized (01) coordinates:
// const activeViewport = await viewportPageObject.active;
// await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]);
// For actions that re-render the viewport, gate on the render cycle
// instead of sleeping — start the watcher BEFORE the action:
// const cycle = waitForViewportRenderCycle(page);
// await action();
// await cycle;
// 3. Handle prompts (first measurement prompts for tracking; SEG/RT/SR prompts for hydration)
// await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
// 4. Assert canvas output via visual regression — object form, scoped to the
// viewport via a locator (not the full page). normalizedClip is only for
// clipping to a sub-region of that locator.
// await checkForScreenshot({
// page,
// locator: viewportPageObject.grid,
// screenshotPath: screenShotPaths.YOUR_CATEGORY.YOUR_KEY,
// });
// 5. Assert DOM state directly
// const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
// expect(count).toBe(1);
});
});

View File

@ -1,72 +0,0 @@
# Failure triage
Before debugging, classify. Most OHIF test failures are timing or hydration — not real regressions.
| Category | Symptom | Fix |
|----------|---------|-----|
| Timing | Element not visible, action timeout | Add / increase the `delay` param of `visitStudy`; for actions that re-render viewports, use `waitForViewportRenderCycle(page)` (started before the action) instead of `waitForTimeout`; wrap the assertion in `expect.toPass({ timeout })` |
| Selector | Element not found | Verify `data-cy` on the target; confirm the panel is open (`toggle()` / `select()` before interacting); check for capital `D` in `DOMOverlayPageObject` when destructuring |
| Hydration | Segmentation/RT/SR not interactive | Ensure the `segmentationHydration.yes.click()` fired; wait for an observable hydrated state such as measurement/segment rows or the target series overlay, then wait for the resulting viewport render |
| Data | Study not found, empty viewport | Confirm the UID is in the canonical list (see [patterns-by-feature.md](patterns-by-feature.md)); confirm the mode supports the feature (segmentation tools aren't in `viewer` mode) |
| Visual drift | Screenshot mismatch but feature works | Have a human review the diff, then regenerate the baseline with `TEST_ENV=true pnpm exec playwright test --update-snapshots`. Do not adjust `maxDiffPixelRatio` or `threshold` to make a failing screenshot pass. |
| Real regression | Feature is actually broken | Report as a bug — this is the test doing its job |
## Prefer render-cycle waits over sleeps
If you're tempted to add `await page.waitForTimeout(2000)` after an action, ask whether the action re-rendered the viewport. If it did, use:
```ts
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await check();
```
The watcher must be created **before** the action — it waits for `needsRender` first, and that transition is gone by the time the action returns. See the "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) and `tests/SEGHydrationFromMPR.spec.ts`.
### When the cycle helper times out at `waitForAnyViewportNeedsRender`
Symptom: the test fails inside `waitForAnyViewportNeedsRender` after 5s, with the action having actually completed in the UI. The action just doesn't transition the viewport through `needsRender` synchronously. Known cases:
- Hanging-protocol changes.
- **RTSTRUCT / contour segmentation hydration confirm.** SEG (labelmap) hydration does fire `needsRender`; contour does not. The fix is to gate on the actual end-state — for hydration in `beforeEach`, `await expect(page.getByTestId('data-row')).toHaveCount(N)` is the right wait.
Don't react by raising the cycle's timeout — the transition isn't coming. Replace the cycle wrapper with an auto-retrying DOM/SVG assertion, or `expect.toPass({ timeout })` around the assertion block.
An immediate `waitForViewportsRendered(page)` can also return too early when a click dispatches work through an asynchronous state machine: the old viewport is already `rendered` before the new series or annotations are applied. In that case, first wait for the target state (for example, hydrated measurement rows or the expected series overlay), then call `waitForViewportsRendered(page)` to settle that state's render.
## The `toPass` pattern
When an assertion needs to wait for async render / propagation:
```ts
await expect(async () => {
await rightPanelPageObject.labelMapSegmentationPanel.panel.segmentByText('Spleen').click();
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
}).toPass({ timeout: 10_000 });
```
`toPass` reruns the assertion block until it succeeds or the timeout expires — cleaner than a hand-rolled retry loop and surfaces the last failure reason if it times out.
## Common `DOMOverlayPageObject` mistake
The fixture key is capital-D `DOMOverlayPageObject`, not lowercase. If your destructure is silently `undefined`, check the casing.
## `press` import mistake
`press` is NOT re-exported from `./utils`. `import { press } from './utils'` resolves to `undefined` and fails at runtime. Use:
```ts
import { press } from './utils/keyboardUtils';
await press({ page, key: 'ArrowDown', nTimes: 50 }); // object param, not (page, key)
```
## Visual regression iteration
Screenshots live under `tests/screenshots/chromium/<testFilePath>/`. To accept new output as the baseline:
```sh
TEST_ENV=true pnpm exec playwright test tests/YourSpec.spec.ts --update-snapshots
```
Review the resulting PNGs carefully — an agent-accepted baseline that's subtly wrong is worse than a failing test.

View File

@ -1,120 +0,0 @@
# OHIF Page Object guide
> This file documents the **stable structural rules** of the page object system. For the current list of methods and properties on any class, **read the source under `tests/pages/`** — it is always authoritative, and it evolves as the product does. A static method table in a reference file goes stale the moment someone refactors; the source does not.
## How to discover the API of a page object
1. Find the relevant class in `tests/pages/`. File names match class names.
2. Read it end-to-end once — most are under a few hundred lines.
3. Some classes compose sub-objects (e.g. `RightPanelPageObject` holds a measurementsPanel, contourSegmentationPanel, labelMapSegmentationPanel, tmtvPanel, etc.). Those sub-objects usually live in the same file or a sibling under `tests/pages/`.
4. To see how a method is actually used, grep `tests/` or open the seed spec listed in [patterns-by-feature.md](patterns-by-feature.md). Real usage beats a synthesized signature every time.
Do not try to memorize a method surface from this file — it intentionally does not list one. It lists only the rules you cannot derive from the source by reading a single file.
---
## Stable rules
### Fixture keys (case-sensitive)
These are injected via `tests/utils/fixture.ts`. Destructure them from the test function's first argument — do not `new` them, because the fixture wires sub-objects to the correct `page` and hand-constructed instances skip that wiring.
- `viewportPageObject`
- `mainToolbarPageObject`
- `leftPanelPageObject`
- `rightPanelPageObject`
- `DOMOverlayPageObject`**capital D**. A silent `undefined` destructure is almost always a casing typo here.
- `notFoundStudyPageObject`
If the fixture file is updated and new keys are added, they will show up there first — check it if something feels missing.
### Non-fixture page objects
Some page object classes are not fixture-injected. They are reached through an injected fixture:
- `DicomTagBrowserPageObject` → via `DOMOverlayPageObject.dialog.dicomTagBrowser`
- `DataOverlayPageObject` → via `viewportPageObject.getById(viewportId).overlayMenu.dataOverlay`
Both can be constructed manually (`new DataOverlayPageObject(page)`) if a test really needs a fresh instance, but the accessor path is the idiomatic one.
### Viewport wrapper vs. viewport instance
`viewportPageObject` is a **wrapper**. You almost always want a specific viewport out of it first:
- `await viewportPageObject.active` — the currently focused viewport
- `viewportPageObject.getAll()` — every viewport in the grid
- `viewportPageObject.getNth(i)` — zero-indexed
- `viewportPageObject.getById(cornerstoneViewportId)` — e.g. `'default'`, `'ctAXIAL'`
The object these return is the one with `normalizedClickAt`, `normalizedDragAt`, `overlayText`, `nthAnnotation`, etc. Reach for the viewport instance first, then call methods on it.
### Panel access order
Every `rightPanelPageObject` sub-panel follows the same three-step idiom: **open the side panel, `.select()` the sub-panel tab, then interact with `.panel.*`**. Skipping either of the first two is the most common cause of "element not found".
```ts
await rightPanelPageObject.toggle();
await rightPanelPageObject.measurementsPanel.select();
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
```
The exact row/action methods vary by panel — check the source file for the one you need.
### Layout identifiers are camelCase JS properties
`mainToolbarPageObject.layoutSelection.<layout>.click()` — access layouts by camelCase property name (e.g. `threeDFourUp`, `axialPrimary`), not with bracket-escaped DICOM-ish strings like `['3DFourUp']`. This is a convention enforced by how the class exposes its tools.
### Sub-tools auto-open their dropdown
Tools nested inside a toolbar dropdown (measurement tools, more tools, layouts) each expose a `.click()` that opens the parent menu for you. You almost never need to open the menu first. `await mainToolbarPageObject.measurementTools.length.click()` does both the expand and the select.
### When the control you need isn't covered yet — create or augment
The page objects here cover what the suite currently exercises. When your test needs a
control that isn't exposed, **extend the page object system rather than dropping a raw
`page.getByTestId(...)` into the spec.** A raw selector in a spec is the clearest tell
that the author didn't read the existing tests — it duplicates a locator that should
live in one place and bypasses the fixture wiring.
| What you need | Where it goes | Precedent to copy |
|---|---|---|
| A toolbar button/tool (Zoom, Pan) | a getter on `MainToolbarPageObject` | `crosshairs`, `measurementTools` |
| A menu / prompt / context menu / small dialog | `DOMOverlayPageObject` | `viewport.measurementTracking`, `dialog.input` |
| A large dialog with its own fields (User Preferences) | a **new** page object class reached via `DOMOverlayPageObject` | `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Individual fields/rows in that dialog | methods/sub-objects on the dialog's page object | `DicomTagBrowserPageObject.seriesSelect` |
| Side-panel controls | `LeftPanelPageObject` / `RightPanelPageObject` | existing sub-panels |
Two more expectations:
- **Missing `data-cy`?** Add it to the source component and target it; don't fall back
to `getByRole`/text. `testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`. Mention any attribute you added so it ships in the same PR.
- **Mirror the existing shape.** A new getter should look like its neighbors (return a
locator, or a small object with `.click()` etc.) so the new surface is
indistinguishable from what was already there.
Example — a "set the Zoom hotkey" test should make `mainToolbarPageObject.zoom`, an
options-menu accessor on `DOMOverlayPageObject`, and a `userPreferences` dialog page
object (with a per-hotkey field accessor) exist — then the spec reads as steps, not
selectors.
---
## Page object map — what each class is *for*
This table exists to help you pick the right file to open, not to enumerate methods. Source of truth for any specific method remains the `.ts` file.
| Class | File | Covers |
|-------|------|--------|
| ViewportPageObject | `tests/pages/ViewportPageObject.ts` | Cornerstone viewports — clicks, drags, overlays, annotations, crosshairs |
| MainToolbarPageObject | `tests/pages/MainToolbarPageObject.ts` | Top toolbar — measurement tools, more tools, layouts, crosshairs, pan |
| LeftPanelPageObject | `tests/pages/LeftPanelPageObject.ts` | Study browser — thumbnails, load by modality or description |
| RightPanelPageObject | `tests/pages/RightPanelPageObject.ts` | Side panels — measurements, contour seg, labelmap seg, TMTV, microscopy |
| DOMOverlayPageObject | `tests/pages/DOMOverlayPageObject.ts` | DOM overlays — dialogs, hydration/tracking prompts, context menus, tag-browser accessor |
| NotFoundStudyPageObject | `tests/pages/NotFoundStudyPageObject.ts` | Study-not-found error page |
| DicomTagBrowserPageObject | `tests/pages/DicomTagBrowserPageObject.ts` | Tag-browser dialog (non-fixture; reach via `DOMOverlayPageObject.dialog`) |
| DataOverlayPageObject | `tests/pages/DataOverlayPageObject.ts` | Data-overlay menu (non-fixture; reach via `viewport.overlayMenu`) |
If the directory adds or renames a file, that diff is your first clue and this table is your second — trust the directory.
For live usage, the seed spec in [patterns-by-feature.md](patterns-by-feature.md) shows how a class is actually called; the source file tells you everything else that's on it.

View File

@ -1,148 +0,0 @@
# Seed specs by feature area
> When writing a new OHIF test, find the closest feature area below and read the listed spec in full before writing. Playwright's own guidance says the seed test "serves as an example of all the generated tests" — that applies here.
>
> **If a spec listed below has moved or been renamed**, grep `tests/` for a remaining example (e.g. `grep -rn "loadSeriesByModality('RTSTRUCT')" tests/`). The pattern matters more than the exact filename — specs get renamed, the feature area persists.
>
> **No close match below?** This list only covers areas with a seed worth copying; don't add a stub for every untested area. See [Feature area not listed above?](#feature-area-not-listed-above-no-existing-seed).
## Canonical study UIDs
| UID | Mode(s) | What it has |
|-----|---------|-------------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | CT — default for measurement/annotation tests |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | CT volume for 3D/MPR/crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | CT + SEG for labelmap tests |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | CT + RTSTRUCT + PET, used for contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR structured report |
Do not invent UIDs — they must exist on the e2e data server.
---
## 1. Simple measurement tools (length, angle, bidirectional, rectangle, ellipse, circle)
**Seed:** `tests/Length.spec.ts`, `tests/Angle.spec.ts`
Pattern: select tool via `mainToolbarPageObject.measurementTools.<tool>.click()`, place N points via `activeViewport.normalizedClickAt([...])`, confirm the tracking prompt, screenshot via `screenShotPaths.<name>.<name>DisplayedCorrectly`.
## 2. Freehand / spline / livewire ROIs
**Seed:** `tests/FreehandROI.spec.ts`, `tests/Livewire.spec.ts`, `tests/Spline.spec.ts`
Pattern: `normalizedDragAt({ start, end, config: { steps: 20, delay: 30 } })` for smooth strokes; `subscribeToMeasurementAdded` to assert the event fires; `activeViewport.nthAnnotation(0)` to reference what was drawn.
## 3. Annotations (arrow, probe)
**Seed:** `tests/ArrowAnnotate.spec.ts`, `tests/Probe.spec.ts`
Arrow annotate opens `DOMOverlayPageObject.dialog.input` for the label. Use `fillAndSave(label)`.
## 4. Measurement panel interactions
**Seed:** `tests/MeasurementPanel.spec.ts`
Panel access: `rightPanelPageObject.toggle()``.measurementsPanel.select()``.panel.nthMeasurement(i)``.actions.rename|delete|toggleLock|duplicate|...`. Also demonstrates `addLengthMeasurement(page)` and panel-row `click()` for jump-to.
## 5. Context menu (right-click on annotation)
**Seed:** `tests/ContextMenu.spec.ts`
Two ways to open: `activeViewport.normalizedClickAt([{...}], 'right')` on an empty area, or `activeViewport.nthAnnotation(0).contextMenu.open()` on a drawn annotation. Then `DOMOverlayPageObject.viewport.annotationContextMenu.addLabel|delete.click()`.
## 6. Labelmap segmentation (SEG) hydration
**Seed:** `tests/SEGHydration.spec.ts`, `tests/SEGHydrationThenMPR.spec.ts`
Flow: `leftPanelPageObject.loadSeriesByModality('SEG')``waitForTimeout(3000)``DOMOverlayPageObject.viewport.segmentationHydration.yes.click()`. Often pokes Cornerstone state directly via `page.evaluate(() => window.cornerstone...)` for zoom/render.
## 7. Contour segmentation (RTSTRUCT) + interactions
**Seeds:**
- Hydration: `tests/RTHydration.spec.ts`
- Rename: `tests/ContourSegmentRename.spec.ts`
- Navigation between segments: `tests/ContourSegNavigation.spec.ts`
- Duplicate: `tests/ContourSegmentDuplicate.spec.ts`
- Visibility: `tests/ContourSegmentToggleVisibility.spec.ts`
- Locking: `tests/ContourSegLocking.spec.ts`
Pattern: load RTSTRUCT series → hydrate → use `rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(i)` / `.segmentByText('Small Sphere')` and their `.actions.rename|delete`, or the segment's `.click()` to jump.
## 8. Labelmap segmentation editing (brush / eraser / threshold)
**Seed:** `tests/LabelMapSegLocking.spec.ts`, `tests/SEGDrawingToolsResizing.spec.ts`
Pattern: `rightPanelPageObject.labelMapSegmentationPanel.tools.brush.setRadius(n)` / `.click()`, then `activeViewport.normalizedDragAt(...)` to paint.
## 9. TMTV / PET
**Seeds:** `tests/TMTVCSVReport.spec.ts`, `tests/TMTVSUV.spec.ts`, `tests/TMTVAlignment.spec.ts`, `tests/TMTVRendering.spec.ts`
Visit `mode: 'tmtv'` with a longer delay (`10000`). `rightPanelPageObject.tmtvPanel` for the side panel. Exporting a report uses `page.waitForEvent('download')` + `downloadAsString(download)`.
## 10. MPR and 3D layouts
**Seeds:** `tests/MPR.spec.ts`, `tests/3DOnly.spec.ts`, `tests/3DFourUp.spec.ts`, `tests/3DMain.spec.ts`, `tests/AxialPrimary.spec.ts`
Pattern: `mainToolbarPageObject.layoutSelection.<layoutName>.click()`. For 3D, wrap stabilization with `attemptAction(() => reduce3DViewportSize(page), 10, 100)` to settle the render before asserting.
## 11. Crosshairs
**Seed:** `tests/Crosshairs.spec.ts`
Pattern: `initializeMousePositionTracker(page)` in `beforeEach`, `mainToolbarPageObject.crosshairs.click()`, then `viewportPageObject.crosshairs.axial.rotate()` / `.increase()`.
## 12. Overlays — data overlay menu, window/level, orientation
**Seeds:** `tests/DataOverlayMenu.spec.ts`, `tests/WindowLevelOverlayText.spec.ts`, `tests/MultipleSegmentationDataOverlays.spec.ts`
Pattern: `viewportPageObject.getById('default').overlayMenu.dataOverlay.toggle()`, then `.addSegmentation(name)` / `.changeSegmentation(from, to)` / `.remove(name)`. For keyboard navigation, remember `press({ page, key, nTimes })` imports from `./utils/keyboardUtils`.
## 13. DICOM Tag Browser
**Seed:** `tests/DicomTagBrowser.spec.ts`
Open with `mainToolbarPageObject.moreTools.tagBrowser.click()`, then interact via `DOMOverlayPageObject.dialog.dicomTagBrowser.waitVisible()` / `.seriesSelect.selectOption(i)` / `.seriesSelect.getOptionText(i)`.
## 14. Study validation / not-found / worklist
**Seeds:** `tests/StudyValidation.spec.ts`, `tests/Worklist.spec.ts`
These are the rare specs that use `page.goto(...)` directly instead of `visitStudy()` — because they test error states or non-study pages. `notFoundStudyPageObject` gives you `errorMessage`, `returnMessage`, `studyListLink`.
## 15. SR (Structured Report) hydration
**Seeds:** `tests/SRHydration.spec.ts`, `tests/SRHydrationThenReload.spec.ts`
Same shape as SEG hydration but load via `loadSeriesByModality('SR')`.
## Feature area not listed above? (no existing seed)
Don't add a stub section here for an untested area. When your target isn't listed:
1. **Look for an indirect seed.** Grep `tests/` for the controls or flow you need
(`grep -rn "options-menu" tests/`, a related panel/dialog). An adjacent area's seed
usually still shows how the harness is driven.
2. **If genuinely uncovered, you're *creating* page objects.** Read
[page-objects.md](page-objects.md) → "When the control you need isn't covered yet", then:
- One page object per new control. Dialogs follow `DicomTagBrowserPageObject` (reached
through `DOMOverlayPageObject`); toolbar buttons get a getter on `MainToolbarPageObject`.
- Add a `data-cy` to any control that lacks one.
- **Verify the real effect, not a proxy** — drag and screenshot that the image moved via
`locator: viewportPageObject.grid`, not just that a button gained `data-active`.
**Example — user preferences / hotkeys (no seed today):** open the options menu and User
Preferences dialog (options-menu accessor on `DOMOverlayPageObject` + a `userPreferences`
dialog page object reached through it), set the hotkey, save, then trigger it and confirm the
viewport effect (activate Zoom, drag, screenshot the zoom). Add the Zoom button as a getter on
`MainToolbarPageObject`.
---
## Advanced patterns worth knowing
- **`subscribeToMeasurementAdded`** (`tests/FreehandROI.spec.ts`) — for async "was a measurement actually added?" assertions. Always `try { ... } finally { await measurementAdded.unsubscribe() }`.
- **`attemptAction`** (`tests/3DOnly.spec.ts`) — retry flaky setup (3D render, heavy layout change) without silencing real failures.
- **`addOHIFConfiguration`** (`tests/RTHydrationDisableConfirmation.spec.ts`) — pre-load config overrides before `visitStudy`.
- **`page.evaluate(() => window.services...)`** — used in several SEG/SR specs to set customizations or poke viewport state. Treat as an escape hatch, not a default.
- **`expect.toPass({ timeout })`** — wrap flaky assertions (common for jump-to-measurement tests where rendering settles asynchronously).

View File

@ -1,100 +0,0 @@
# OHIF Test Utility guide
> This file documents the **stable import rules and conventions** around `tests/utils/`. For the current list of exported helpers and their exact signatures, **read `tests/utils/index.ts` and the files it re-exports** — the barrel is always current; a static table here is not. Utilities get added, renamed, and refactored; the rules below change much more slowly.
## How to discover what's available
1. Open `tests/utils/index.ts`. Every symbol exported from the barrel is importable as `import { foo } from './utils'`.
2. If what you need isn't in the barrel, look in the rest of `tests/utils/` — there are a handful of specialized files (`keyboardUtils.ts`, `assertions.ts`, `download.ts`, …). These need the **deeper import path**; see the rule below.
3. For the actual signature, read the utility's `.ts` file. It's one short function per file in most cases.
4. To see a utility in context, grep `tests/` (`grep -rn visitStudy tests/`) — or open the seed spec for the relevant feature area ([patterns-by-feature.md](patterns-by-feature.md)). Existing specs are the most reliable signature reference because they co-evolve with the API.
Do not guess parameter shapes from memory, and do not treat this file as an API catalog — it intentionally isn't one.
---
## Stable rules
### Barrel vs. deep imports
Two import styles exist. They are not interchangeable.
```ts
// Barrel — anything re-exported from tests/utils/index.ts
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Deep — for files NOT re-exported by the barrel
import { press } from './utils/keyboardUtils';
import { assertNumberOfModalityLoadBadges } from './utils/assertions';
import { downloadAsString } from './utils/download';
```
If a symbol isn't in the barrel, `import { x } from './utils'` **compiles**, resolves `x` to `undefined`, and blows up at the first call site. Confirm by opening `tests/utils/index.ts` for your working revision. At the time of this writing, `press`, `downloadAsString`, and the `assert*` helpers live outside the barrel — but maintainers can move things in or out, so treat `index.ts` as the ground truth rather than this note.
### Never import `test` / `expect` from `@playwright/test`
```ts
// ✅ Correct — gets the fixture-extended runner
import { test, expect } from './utils';
// ❌ Wrong — compiles, but every page-object fixture is silently undefined
import { test, expect } from '@playwright/test';
```
If your test function's destructured arguments (like `viewportPageObject`) are `undefined`, this import is almost always why.
### `visitStudy` — 2000 ms is a convention, not a default
```ts
await visitStudy(page, studyInstanceUID, mode, 2000);
```
The function's own default delay is `0`. Nearly every spec passes `2000` to let the first render settle. The one consistent exception is `mode: 'tmtv'`, where the suite uniformly uses `10000` because PET fusion and SUV calculation add real wall-clock cost before the UI is interactive.
Notably, 3D layouts in `viewer` mode (3DOnly, 3DFourUp, 3DMain, 3DPrimary) and MPR also use `2000` — they're not "heavy" in the `visitStudy` sense. Their stabilization problem is solved at the interaction layer with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not by a longer visit delay.
So: pick `10000` when the mode is `tmtv`, `2000` otherwise, and only ramp up if a specific test flakes on first-render assertions. The delay is a good first lever for "not visible" flakes, but it's not a universal upgrade.
### `checkForScreenshot` — use the object form, never screenshot the full app
**This is the direction going forward** The suite is being migrated off *full-app* screenshots — not off screenshots altogether. Screenshots are the correct and required tool whenever what you're verifying is canvas-only raster output with no DOM signal; don't avoid them there. Avoid them only where a faithful DOM/SVG signal exists (e.g. a vector overlay's color via `getSvgAttribute`) or where you'd be capturing the whole app. See SKILL.md → "Screenshot vs. DOM assertion — how to choose". Any new spec must follow the rules below, and any modification to an older spec should bring it in line when reasonable.
- **Object form** (required for all new specs): `checkForScreenshot({ page, screenshotPath, normalizedClip?, ... })`
- **Positional form**: legacy. It still appears in older specs because they haven't been migrated yet. **Do not treat existing positional-form usage as a pattern to copy** — those specs are the thing being moved away from. Do not introduce the positional form in new code.
**Hard rules for new screenshots:**
1. Use the object form.
2. Scope by passing a `locator``viewportPageObject.grid` for the whole grid, or a specific viewport pane locator. **Never screenshot the full app.** `normalizedClip` is computed *relative to the locator* (and defaults to the full page when no locator is given), so `{ x: 0, y: 0, width: 1, height: 1 }` alone does not scope anything — reserve `normalizedClip` for clipping to a sub-region of a locator. If you find yourself reaching for `fullPage: true`, stop and pass a locator instead.
Do not tune `maxDiffPixelRatio` or `threshold` to make a screenshot pass — those are intentionally rarely touched and not the right knob for flakes. If a baseline mismatches, regenerate it (`--update-snapshots`) after a human review of the diff, or fix the underlying instability. Check the current signature in `tests/utils/checkForScreenshot.ts` if something looks off.
### `screenShotPaths` — use keys, not raw strings
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid,
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
The full tree of categories lives in `tests/utils/screenShotPaths.ts`. When you need a new baseline, **add the key there and reference it by name** rather than typing a raw path. A typo in a key becomes a compile error instead of a silent mismatch, and other tests become discoverable through the object.
### Object-param convention
Many OHIF test utilities take a single **object argument** rather than positional arguments — notably `press({ page, key, nTimes? })`, the `simulate*` helpers, and the `assert*` helpers. If a call looks like it should work but throws "cannot read properties of undefined," check whether you're passing positional args to something that expects `{ page, ... }`.
Read the one-line signature at the top of the utility's source file before calling it — it's faster than guessing, and it's always right.
---
## Utility shapes worth flagging
Most utilities are obvious once you read the source; these earn a mention:
- **`waitForViewportRenderCycle(page, options?)`** — preferred replacement for `page.waitForTimeout(...)` after any viewport-mutating action (hydration confirm, layout change, segmentation add, series load, etc.). Start it **before** the action, await it after — it captures the `needsRender → rendered` transition the action triggers. Use `waitForViewportsRendered(page)` (the second-half-only variant) when the render is already in flight before you can attach a watcher. Source: `tests/utils/waitForViewportsRendered.ts`. Seed: `tests/SEGHydrationFromMPR.spec.ts`. The "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) covers the idiom in full.
- **`subscribeToMeasurementAdded(page)`** — returns `{ waitFired(timeout?), unsubscribe() }`. Use in freehand/livewire/spline specs to assert the event fired. Always wrap in `try { ... } finally { await sub.unsubscribe() }` so a failing assertion doesn't leak the listener across tests.
- **`attemptAction(action, attempts?, delay?)`** — retries a flaky async action without masking real failures. Mainly used to stabilize 3D scenes (`attemptAction(() => reduce3DViewportSize(page), 10, 100)`).
For everything else, the pattern is: find a spec that uses it (see [patterns-by-feature.md](patterns-by-feature.md)), copy the shape, adapt.

68
.all-contributorsrc Normal file
View File

@ -0,0 +1,68 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "swederik",
"name": "Erik Ziegler",
"avatar_url": "https://avatars3.githubusercontent.com/u/607793?v=4",
"profile": "https://github.com/swederik",
"contributions": [
"code"
]
},
{
"login": "evren217",
"name": "Evren Ozkan",
"avatar_url": "https://avatars1.githubusercontent.com/u/4920551?v=4",
"profile": "https://github.com/evren217",
"contributions": [
"code"
]
},
{
"login": "galelis",
"name": "Gustavo André Lelis",
"avatar_url": "https://avatars3.githubusercontent.com/u/2378326?v=4",
"profile": "https://github.com/galelis",
"contributions": [
"code"
]
},
{
"login": "dannyrb",
"name": "Danny Brown",
"avatar_url": "https://avatars1.githubusercontent.com/u/5797588?v=4",
"profile": "http://dannyrb.com/",
"contributions": [
"code"
]
},
{
"login": "allcontributors",
"name": "allcontributors[bot]",
"avatar_url": "https://avatars3.githubusercontent.com/u/46843839?v=4",
"profile": "https://github.com/all-contributors/all-contributors-bot",
"contributions": [
"doc"
]
},
{
"login": "EsrefDurna",
"name": "Esref Durna",
"avatar_url": "https://avatars0.githubusercontent.com/u/1230575?v=4",
"profile": "https://www.linkedin.com/in/siliconvalleynextgeneration/",
"contributions": [
"question"
]
}
],
"contributorsPerLine": 7,
"projectName": "Viewers",
"projectOwner": "OHIF",
"repoType": "github",
"repoHost": "https://github.com"
}

19
.babelrc Normal file
View File

@ -0,0 +1,19 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "11"
}
}
],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-regenerator",
"@babel/plugin-transform-runtime"
]
}

View File

@ -1,6 +0,0 @@
# Browsers that we support
> 1%
IE 11
not dead
not op_mini all

View File

@ -1,580 +1,155 @@
version: 2.1 version: 2
# ci: re-trigger pipeline ### ABOUT
#
orbs: # This configuration powers our Circleci.io integration
codecov: codecov/codecov@1.0.5 #
cypress: cypress-io/cypress@3.4.2 # Note:
# Netlify works independently from this configuration to
# create pull request previews and to update `https://docs.ohif.org`
###
defaults: &defaults defaults: &defaults
docker:
- image: cimg/node:24.15.0
environment:
TERM: xterm
QUICK_BUILD: true
working_directory: ~/repo working_directory: ~/repo
docker:
commands: - image: circleci/node:10.15.1
install_pnpm:
steps:
- run:
name: Install pnpm
command: |
# The cimg/node global modules dir (/usr/local/lib/node_modules) is
# root-owned, so a plain `npm install -g` fails with EACCES. Install
# with sudo so the global pnpm binary lands in the shared prefix.
sudo npm install -g pnpm@11.5.2
echo 'export PATH="$(pnpm store path)/../.bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
jobs: jobs:
UNIT_TESTS: build_and_test:
<<: *defaults <<: *defaults
resource_class: large
steps: steps:
- install_pnpm # Download and cache dependencies
- run: node --version
- checkout - checkout
- restore_cache:
name: Restore Yarn Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- run: - run:
name: Install Dependencies name: Install Dependencies
command: pnpm install --frozen-lockfile command: yarn install --frozen-lockfile
# RUN TESTS - save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
# Build & Test
- run: yarn build:package:ci
# https://www.viget.com/articles/using-junit-on-circleci-2-0-with-jest-and-eslint/
- run: - run:
name: 'JavaScript Test Suite' name: 'JavaScript Test Suite'
command: pnpm run test:unit:ci command: yarn test:ci
# platform/app environment:
- run: JEST_JUNIT_OUTPUT: 'reports/junit/js-test-results.xml'
name: 'VIEWER: Combine report output' # Store result
command: | - store_test_results:
viewerCov="/home/circleci/repo/platform/app/coverage" path: reports/junit
touch "${viewerCov}/reports" - store_artifacts:
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports" path: reports/junit
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/app/coverage/reports'
flags: 'viewer'
# PLATFORM/CORE
- run:
name: 'CORE: Combine report output'
command: |
coreCov="/home/circleci/repo/platform/core/coverage"
touch "${coreCov}/reports"
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
- codecov/upload:
file: '/home/circleci/repo/platform/core/coverage/reports'
flags: 'core'
BUILD:
<<: *defaults
resource_class: large
steps:
# Checkout code and ALL Git Tags
- checkout
- install_pnpm
- run:
name: Install Dependencies
command: pnpm install --frozen-lockfile
# Build & Test
- run:
name: 'Perform the versioning before build'
command: node ./version.mjs
- run:
name: 'Build the OHIF Viewer'
command: pnpm run build
no_output_timeout: 45m
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
command: |
# export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
# export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
# export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
# Persist :+1: # Persist :+1:
- persist_to_workspace: - persist_to_workspace:
root: ~/repo root: ~/repo
paths: paths: .
- platform/app/dist
- Dockerfile
- version.txt
- commit.txt
- version.json
BUILD_PACKAGES_QUICK: npm_publish:
<<: *defaults <<: *defaults
resource_class: large
steps: steps:
- install_pnpm
# Checkout code and ALL Git Tags
- checkout
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
# SECURITY AUDIT - only when pnpm-lock.yaml has changed
- run:
name: 'Security Audit - High Risk Vulnerabilities'
command: |
git fetch origin master 2>/dev/null || true
BASE_REF=$(git merge-base HEAD origin/master 2>/dev/null)
if [[ -z "$BASE_REF" ]]; then
echo "Could not determine base ref (e.g. shallow clone or no origin/master), skipping security audit."
exit 0
fi
CHANGED_FILES=$(git diff --name-only origin/master...HEAD 2>/dev/null || echo "")
if ! echo "$CHANGED_FILES" | grep -qx 'pnpm-lock.yaml'; then
echo "pnpm-lock.yaml unchanged - skipping security audit."
exit 0
fi
echo "pnpm-lock.yaml changed - running pnpm audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..."
if pnpm audit --audit-level high; then
echo "No high-risk vulnerabilities found"
echo "Security audit passed!"
else
echo ""
echo "HIGH-RISK VULNERABILITIES DETECTED!"
echo "======================================"
echo ""
echo "To fix these issues:"
echo " 1. Run: pnpm audit"
echo " 2. Review the vulnerability details"
echo " 3. Update affected packages to secure versions"
echo " 4. Test your changes"
echo " 5. Re-run: pnpm audit --audit-level high"
echo ""
echo "Full audit report:"
pnpm audit || true
echo ""
echo "This build cannot proceed until high-risk vulnerabilities are resolved."
exit 1
fi
- run:
name: Install Dependencies
command: pnpm install
- run: - run:
name: Avoid hosts unknown for github name: Avoid hosts unknown for github
command: | command:
rm -rf ~/.ssh mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
mkdir ~/.ssh/ no\n" > ~/.ssh/config
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config # --no-ci argument is not ideal; however, semantic-rlease thinks we're
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com" # attempting to run it from a `pr`, which is not the case
git config --global user.name "ohif-bot"
- run: - run:
name: Authenticate with NPM registry name: Publish using Semantic Release
# Write the auth token to npm's per-user config (~/.npmrc), not the command: npx semantic-release --debug
# repo root. publish-package.mjs chdir's into each package dir to run # Persist :+1:
# `npm publish`, and npm only reads the project .npmrc from that dir - persist_to_workspace:
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a root: ~/repo
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids paths: .
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
pnpm run build:package-all
- run:
name: build the other half of the packages
command: |
pnpm run build:package-all-1
NPM_PUBLISH: docs_publish:
<<: *defaults <<: *defaults
resource_class: large
steps: steps:
- install_pnpm
# Checkout code and ALL Git Tags
- checkout
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
- run:
name: Install Dependencies
command: pnpm install --frozen-lockfile
- run: - run:
name: Avoid hosts unknown for github name: Avoid hosts unknown for github
command: | command:
rm -rf ~/.ssh mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
mkdir ~/.ssh/ no\n" > ~/.ssh/config
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - run: git config --global user.email "gh-pages@localhost"
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com" - run: git config --global user.name "npm gh-pages"
git config --global user.name "ohif-bot" - run: yarn global add gh-pages
- run: - run:
name: Authenticate with NPM registry name: Generate Docs
# Write the auth token to npm's per-user config (~/.npmrc), not the command: yarn run staticDeploy
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run: - run:
name: build half of the packages (to avoid out of memory in circleci) name: Publish Docs
command: | command: yarn run docs:publish
pnpm run build:package-all
- run:
name: build the other half of the packages
command: |
pnpm run build:package-all-1
- run:
name: increase min time out
command: |
npm config set fetch-retry-mintimeout 20000
- run:
name: increase max time out
command: |
npm config set fetch-retry-maxtimeout 120000
- run:
name: publish package versions
command: |
node ./publish-version.mjs
- run:
name: Re-assert the NPM auth token before publishing
# Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run:
name: publish package dist
command: |
node ./publish-package.mjs
- persist_to_workspace:
root: ~/repo
paths:
- .
DOCKER_RELEASE_PUBLISH: docker_publish:
<<: *defaults <<: *defaults
resource_class: large
steps: steps:
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
- setup_remote_docker: - setup_remote_docker:
docker_layer_caching: false docker_layer_caching: true
- run: - run:
name: Build Docker image for amd64 name: Build and push Docker image
command: | command: |
# This file will exist if a new version was published by # This file will exist if a new version was published by
# our command in the previous job. # our `semantic-release` command in the previous job
if [[ ! -e version.txt ]]; then if [[ ! -e tmp/updated-version.txt ]]; then
exit 0 exit 0
else else
# Restore the committed .npmrc (pnpm workspace config such as # Remove npm config
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our amd64 image, auth, and push
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-amd64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
docker push ohif/app:latest-amd64
fi
- persist_to_workspace:
root: ~/repo
paths:
- .
DOCKER_RELEASE_PUBLISH_ARM:
<<: *defaults
resource_class: arm.large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for arm64 (Release)
command: |
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our arm64 image, auth, and push
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-arm64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
docker push ohif/app:latest-arm64
fi
- persist_to_workspace:
root: ~/repo
paths:
- .
DOCKER_BETA_PUBLISH:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for amd64 (Beta)
command: |
echo $(ls -l)
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
echo "don't have version txt"
exit 0
else
echo "Building and pushing Docker image from the master branch (beta releases)"
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our amd64 image, auth, and push
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-beta-amd64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
docker push ohif/app:latest-beta-amd64
fi
DOCKER_BETA_PUBLISH_ARM:
<<: *defaults
resource_class: arm.large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for arm64 (Beta)
command: |
echo $(ls -l)
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
echo "don't have version txt"
exit 0
else
echo "Building and pushing ARM64 Docker image from the master branch (beta releases)"
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our arm64 image, auth, and push
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-beta-arm64 .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
docker push ohif/app:latest-beta-arm64
fi
CYPRESS:
<<: *defaults
resource_class: large
parallelism: 8
steps:
- install_pnpm
- run:
name: Install System Dependencies
command: |
# CircleCI's base image registers third-party apt sources (git-lfs via
# packagecloud, git-core PPA via launchpad) that periodically hang
# `apt-get update`. The Cypress libs below all come from the Ubuntu
# archive, so drop those sources and bound the update with timeouts +
# retries so a slow mirror can't stall the job indefinitely.
sudo rm -f /etc/apt/sources.list.d/*git* || true
sudo apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=20 \
-o Acquire::https::Timeout=20
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6
- run:
name: Start Xvfb
command: Xvfb :99 -screen 0 1920x1080x24 &
background: true
- run:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
install-command: pnpm install
- cypress/run-tests:
# CI runs headless under Xvfb with no GPU, so Electron uses software
# WebGL. Newer Chromium deprecated that implicit fallback (canvas
# rendering degrades and races with element clicks). ELECTRON_EXTRA_LAUNCH_ARGS
# is the reliable way to pass the opt-in flag to Cypress's Electron browser.
cypress-command: |
npx wait-on@latest http://localhost:3000 && cd platform/app && ELECTRON_EXTRA_LAUNCH_ARGS="--enable-unsafe-swiftshader" npx cypress run --record --parallel
start-command: pnpm run test:data && pnpm run test:e2e:serve
DOCKER_MULTIARCH_MANIFEST:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Create and push multi-architecture manifest (Release)
command: |
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
echo "Building and pushing multi-architecture manifest from the master branch (release releases)"
rm -f ./.npmrc rm -f ./.npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat version.txt) export IMAGE_VERSION=$(cat tmp/updated-version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
echo $IMAGE_VERSION echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
# Create and push manifest for specific version docker push ohif/$IMAGE_NAME:latest
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
docker manifest push ohif/app:$IMAGE_VERSION_FULL
# Create and push manifest for "latest" tag
docker manifest create ohif/app:latest \
--amend ohif/app:latest-amd64 \
--amend ohif/app:latest-arm64
docker manifest push ohif/app:latest
fi
DOCKER_BETA_MULTIARCH_MANIFEST:
<<: *defaults
resource_class: large
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Create and push multi-architecture manifest (Beta)
command: |
echo $(ls -l)
# This file will exist if a new version was published by
# our command in the previous job.
if [[ ! -e version.txt ]]; then
exit 0
else
echo "Building and pushing multi-architecture manifest from the master branch (beta releases)"
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
# Create and push manifest for specific beta version
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
docker manifest push ohif/app:$IMAGE_VERSION_FULL
# Create and push manifest for "latest-beta" tag
docker manifest create ohif/app:latest-beta \
--amend ohif/app:latest-beta-amd64 \
--amend ohif/app:latest-beta-arm64
docker manifest push ohif/app:latest-beta
fi fi
workflows: workflows:
PR_CHECKS: version: 2
# PULL REQUESTS
pull_requests:
jobs: jobs:
- BUILD_PACKAGES_QUICK: - build_and_test:
filters: filters:
branches: branches:
ignore: master ignore:
- UNIT_TESTS - master
- CYPRESS: - feature/*
name: 'Cypress Tests' - hotfix/*
context: cypress
# viewer-dev.ohif.org # MERGE TO MASTER
DEPLOY_MASTER: cut_release:
jobs: jobs:
- BUILD: - build_and_test:
filters: filters:
branches: branches:
only: master only: master
# - HOLD_FOR_APPROVAL: - npm_publish:
# type: approval
# requires:
# - BUILD
- NPM_PUBLISH:
requires: requires:
# - HOLD_FOR_APPROVAL - build_and_test
- BUILD - docs_publish:
- DOCKER_BETA_PUBLISH:
requires: requires:
- NPM_PUBLISH - build_and_test
- DOCKER_BETA_PUBLISH_ARM: - docker_publish:
requires: requires:
- DOCKER_BETA_PUBLISH - build_and_test
- DOCKER_BETA_MULTIARCH_MANIFEST: - npm_publish
requires:
- DOCKER_BETA_PUBLISH_ARM
# viewer.ohif.org
DEPLOY_RELEASE:
jobs:
- BUILD:
filters:
branches:
only: /^release\/.*/
- HOLD_FOR_APPROVAL:
type: approval
requires:
- BUILD
- NPM_PUBLISH:
requires:
- HOLD_FOR_APPROVAL
- DOCKER_RELEASE_PUBLISH:
requires:
- NPM_PUBLISH
- DOCKER_RELEASE_PUBLISH_ARM:
requires:
- DOCKER_RELEASE_PUBLISH
- DOCKER_MULTIARCH_MANIFEST:
requires:
- DOCKER_RELEASE_PUBLISH_ARM

View File

@ -1,6 +0,0 @@
[codespell]
skip = .git,*.pdf,*.svg,yarn.lock,*.min.js,locales
# ignore words ending with … and some camelcased variables and names
ignore-regex = \b\S+…\S*|\b(doubleClick|afterAll|PostgresSQL)\b|\bWee, L\.|.*te.*Telugu.*
# some odd variables
ignore-words-list = datea,ser,childrens

View File

@ -1,43 +0,0 @@
# Docker compose files
This folder contains docker-compose files used to spin up OHIF-Viewer with
different options such as locally or with any PAS you desire to
## Public Server
## Local Orthanc
### Build
`$ docker-compose -f docker-compose-orthanc.yml build`
### Run
Starts containers and leaves them running in the background.
`$ docker-compose -f docker-compose-orthanc.yml up -d`
then, access the application at [http://localhost](http://localhost)
**remember that you have to access orthanc application and include your studies
there**
## Local Dcm4chee
#### build
`$ docker-compose -f docker-compose-dcm4chee.yml build`
#### run
`$ docker-compose -f docker-compose-dcm4chee.yml up -d`
then, access the application at [http://localhost](http://localhost)
**remember that you have to access dcm4chee application and include your studies
there** You can use the following command to import your studies into dcm4che
`$ docker run -v {YOUR_STUDY_FOLDER}:/tmp --rm --network=docker_dcm4che_default dcm4che/dcm4che-tools:5.14.0 storescu -cDCM4CHEE@arc:11112 /tmp`
**make sure that your Docker network name is docker_dcm4chee_default or change
it to the right one**

View File

@ -1,21 +0,0 @@
server {
gzip_static always;
gzip_proxied expired no-cache no-store private auth;
gunzip on;
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ ${PUBLIC_URL}index.html;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -1,20 +0,0 @@
server {
listen ${SSL_PORT} ssl http2 default_server;
listen [::]:${SSL_PORT} ssl http2 default_server;
ssl_certificate /etc/ssl/certs/ssl-certificate.crt;
ssl_certificate_key /etc/ssl/private/ssl-private-key.key;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -1,63 +0,0 @@
#!/bin/sh
if [ -n "$SSL_PORT" ]
then
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template | envsubst '${PUBLIC_URL}' > /etc/nginx/conf.d/default.conf
else
envsubst '${PORT}:${PUBLIC_URL}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]; then
echo "$APP_CONFIG" > /usr/share/nginx/html${PUBLIC_URL}app-config.js
echo "Using custom APP_CONFIG environment variable"
else
echo "Not using custom APP_CONFIG"
fi
if [ -f /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
if [ -s /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
echo "Detected non-empty app-config.js. Ensuring .gz file is updated..."
rm -f /usr/share/nginx/html${PUBLIC_URL}app-config.js.gz
gzip /usr/share/nginx/html${PUBLIC_URL}app-config.js
touch /usr/share/nginx/html${PUBLIC_URL}app-config.js
echo "Compressed app-config.js to app-config.js.gz"
else
echo "app-config.js is empty. Skipping compression."
fi
else
echo "No app-config.js file found. Skipping compression."
fi
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
if [ -n "$CLIENT_ID" ]
then
echo "Google Cloud Healthcare \$CLIENT_ID has been provided: "
echo "$CLIENT_ID"
echo "Updating config..."
# - Use SED to replace the CLIENT_ID that is currently in google.js
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
fi
# If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint
if [ -n "$HEALTHCARE_API_ENDPOINT" ]
then
echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: "
echo "$HEALTHCARE_API_ENDPOINT"
echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi
# - Copy google.js to overwrite app-config.js
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
fi
echo "Starting Nginx to serve the OHIF Viewer on ${PUBLIC_URL}"
exec "$@"

View File

@ -1,4 +0,0 @@
find platform/app/dist -name "*.js" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.map" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.css" -exec gzip -9 "{}" \; -exec touch "{}" \;
find platform/app/dist -name "*.svg" -exec gzip -9 "{}" \; -exec touch "{}" \;

View File

@ -1,23 +1,12 @@
# Reduces size of context and hides
# files from Docker (can't COPY or ADD these)
# Note that typically the Docker context for various OHIF containers is the
# directory of this file (i.e. the root of the source). As such, this is
# the .dockerignore file for ALL Docker containers that are built. For example,
# the Docker containers built from the recipes in ./platform/app/.recipes will
# have this file as their .dockerignore.
# Output # Output
**/dist/ dist/
**/build/
# Dependencies # Dependencies
**/node_modules/ node_modules/
# Root # Root
README.md README.md
Dockerfile Dockerfile
dockerfile
# Misc. Config # Misc. Config
.git .git
@ -25,13 +14,3 @@ dockerfile
.gitignore .gitignore
.vscode .vscode
.circleci .circleci
# Unnecessary things to pull into container
.circleci/
.github/
.netlify/
.scripts/
.vscode/
coverage/
platform/docs/
testdata/

14
.env Normal file
View File

@ -0,0 +1,14 @@
##
# Environment: Default
#
# We're using this to set variables for development.
# Please feel free to delete or modify this file for your own setup.
# Be careful not to commit anything sensitive to source control.
#
PUBLIC_URL=/
#
# Most vars require REACT_APP_* naming
#
REACT_APP_CONFIG=config/default.js

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
##
# EXAMPLE
#
# Read more about .env files when using create-react-app here:
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env
#
PUBLIC_URL=/demo
#
# Most vars require REACT_APP_* naming
#
REACT_APP_CONFIG=config/netlify.js

View File

@ -1,4 +1,3 @@
config/** config/**
docs/** docs/**
img/** img/**
node_modules

16
.eslintrc Normal file
View File

@ -0,0 +1,16 @@
{
"extends": [
"react-app",
"eslint:recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"env": {
"jest": true
},
"settings": {
"react": {
"version": "detect",
},
},
}

View File

@ -1,31 +0,0 @@
{
"plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc", "prettier"],
"extends": [
"react-app",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"env": {
"jest": true
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
// Enforce consistent brace style for all control statements for readability
"curly": "error",
"import/no-anonymous-default-export": "off"
},
"globals": {
"cy": true,
"before": true,
"context": true,
"Cypress": true,
"assert": true
}
}

5
.gitattributes vendored
View File

@ -1,5 +0,0 @@
# Set the default behavior,
# in case people don't have core.autocrlf set.
* text=auto
# Declares that files will always have CRLF line ends
*.sh text eol=lf

View File

@ -1,13 +0,0 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: 'npm'
# Disable all pull requests for npm/pnpm version updates.
open-pull-requests-limit: 0
directory: '/'
schedule:
interval: 'daily'
labels: ['dependencies']
commit-message:
prefix: 'chore'
include: 'scope'

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
custom: https://giving.massgeneral.org/ohif

View File

@ -1,85 +0,0 @@
name: 'Bug report'
description: Create a report to help us improve
title: '[Bug] '
labels: ['Community: Report :bug:', 'Awaiting Reproduction']
body:
- type: markdown
attributes:
value: |
👋 Hello, and thank you for contributing to our project! Your support is greatly appreciated.
🔍 Before proceeding, please make sure to read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md) and familiarize yourself with our [development process](https:/docs.ohif.org/development/our-process).
❓ If you're here to seek general support or ask a question, we encourage you to visit our [community discussion board](https://community.ohif.org/)
🐞 For bug reports, please complete the following template in as much detail as possible. This will help us reproduce and address the issue efficiently.
🧪 Finally, ensure that you're using the latest version of the software and check if your issue has already been reported to avoid duplicates.
- type: textarea
id: bug_description
attributes:
label: Describe the Bug
description: 'A clear and concise description of what the bug is.'
validations:
required: true
- type: textarea
id: reproduction_steps
attributes:
label: Steps to Reproduce
description: 'Please describe the steps to reproduce the issue.'
placeholder: "1. First step\n2. Second step\n3. ..."
validations:
required: true
- type: textarea
id: current_behavior
attributes:
label: The current behavior
description:
'A clear and concise description of what happens instead of the expected behavior.'
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: The expected behavior
description: 'A clear and concise description of what you expected to happen.'
validations:
required: true
- type: textarea
id: system_info
attributes:
label: 'System Information'
description: 'Please run the following command in your terminal and paste the output:'
placeholder: |
Run: npx envinfo --system --binaries --browsers
Then paste the output here. It should look something like:
System:
OS: Windows 10 10.0.19042
CPU: (8) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Memory: 15.89 GB / 31.74 GB
Shell: 1.0.0 - C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe
Binaries:
Node: 20.18.1 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.22 - C:\Users\user\AppData\Roaming\npm\yarn.CMD
npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
Browsers:
Chrome: 83.0.4103.116
Edge: Spartan (44.19041.1266.0), Chromium (83.0.478.58)
Firefox: 77.0.1
validations:
required: true
- type: markdown
attributes:
value: >
> :warning: Reports we cannot reproduce are at risk of being marked stale and > closed. The
more information you can provide, the more likely we are to look > into and address your
issue.

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 🤗 Support Question
url: https://community.ohif.org/
about: Please use our forum if you have questions or need help.

View File

@ -1,34 +0,0 @@
name: Feature request
description: Create a feature request
labels: ['Community: Request :hand:']
title: '[Feature Request] '
body:
- type: markdown
attributes:
value: |
👋 Hello and thank you for your interest in our project!
🔍 Before you proceed, please read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md).
🚀 If your request is specific to your needs, consider contributing it yourself! Read our [contributing guides](https://docs.ohif.org/development/contributing) to get started.
🖊️ Please provide as much detail as possible for your feature request. Mock-up screenshots, workflow or logic flow diagrams are very helpful. Discuss how your requested feature would interact with existing features.
⏱️ Lastly, tell us why we should prioritize your feature. What impact would it have?
- type: textarea
attributes:
label: 'What feature or change would you like to see made?'
description:
'Please include as much detail as possible including possibly mock up screen shots, workflow
or logic flow diagrams etc.'
placeholder: '...'
validations:
required: true
- type: textarea
attributes:
label: 'Why should we prioritize this feature?'
description: 'Discuss if and how the requested feature interacts with existing features.'
placeholder: '...'
validations:
required: true

View File

@ -1,93 +0,0 @@
<!-- Do Not Delete This! pr_template -->
<!-- Please read our Rules of Conduct: https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md -->
<!-- 🕮 Read our guide about our Contributing Guide here https://docs.ohif.org/development/contributing -->
<!-- :hand: Thank you for starting this amazing contribution! -->
<!--
⚠️⚠️ Please make sure the checklist section below is complete before submitting your PR.
To complete the checklist, add an 'x' to each item: [] -> [x]
(PRs that do not have all the checkboxes marked will not be approved)
-->
### Context
<!--
Provide a clear explanation of the reasoning behind this change, such as:
- A link to the issue being addressed, using the format "Fixes #ISSUE_NUMBER"
- An image showing the issue or problem being addressed (if not already in the issue)
- Error logs or callStacks to help with the understanding of the problem (if not already in the issue)
-->
### Changes & Results
<!--
List all the changes that have been done, such as:
- Add new components
- Remove old components
- Update dependencies
What are the effects of this change?
- Before vs After
- Screenshots / GIFs / Videos
-->
### Testing
<!--
Describe how we can test your changes.
- open a URL
- visit a page
- click on a button
- etc.
-->
### Checklist
#### PR
<!--
https://semantic-release.gitbook.io/semantic-release/#how-does-it-work
Examples:
Please note the letter casing in the provided examples (upper or lower).
- feat(MeasurementService): add ...
- fix(Toolbar): fix ...
- docs(Readme): update ...
- style(Whitespace): fix ...
- refactor(ExtensionManager): ...
- test(HangingProtocol): Add test ...
- chore(git): update ...
- perf(VolumeLoader): ...
You don't need to have each commit within the Pull Request follow the rule,
but the PR title must comply with it, as it will be used as the commit message
after the commits are squashed.
-->
- [] My Pull Request title is descriptive, accurate and follows the
semantic-release format and guidelines.
#### Code
- [] My code has been well-documented (function documentation, inline comments,
etc.)
#### Public Documentation Updates
<!-- https://docs.ohif.org/ -->
- [] The documentation page has been updated as necessary for any public API
additions or removals.
#### Tested Environment
- [] OS: <!--[e.g. Windows 10, macOS 10.15.4]-->
- [] Node version: <!--[e.g. 18.16.1]-->
- [] Browser:
<!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]-->
<!-- prettier-ignore-start -->
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
<!-- prettier-ignore-end -->

25
.github/stale.yml vendored
View File

@ -1,25 +0,0 @@
# GitHub App: Stale
# https://github.com/apps/stale
#
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 180
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 60
# Issues with these labels will never be considered stale
exemptLabels:
- 'Bug: Verified :bug:'
- 'PR: Awaiting Review 👀'
- 'Announcement 🎉'
- 'IDC:priority'
- 'IDC:candidate'
- 'IDC:collaboration'
- 'Community: Request :hand:'
- 'Community: Report :bug:'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had recent activity. It will
be closed if no further activity occurs. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -1,125 +0,0 @@
name: Build and Deploy Docs
on:
push:
branches: [master]
env:
ACTIONS_STEP_DEBUG: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-deploy-docs:
timeout-minutes: 60
runs-on: ubuntu-latest
# Need permissions to read actions and pull requests
permissions:
actions: read
contents: read
pull-requests: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.15.0
cache: pnpm
- name: Configure git for private repos
run: git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf ssh://git@github.com/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
# Internal @ohif/* deps are workspace:* in the committed manifests and the
# lockfile records them as links, so pnpm-lock.yaml stays in sync across
# version bumps and a frozen install passes. Frozen is the right choice
# for a deploy job: it fails fast if a PR ever changed a real dependency
# without committing the lockfile update, instead of silently reconciling.
# (Exact versions appear only in published tarballs, where pnpm publish
# rewrites workspace:* at pack time -- not publish-version.mjs.)
run: pnpm install --frozen-lockfile
# Removed Playwright tests and coverage generation steps
- name: Find PR and associated workflow run
id: find_pr_run
env:
GH_TOKEN: ${{ github.token }}
MERGE_COMMIT_SHA: ${{ github.sha }}
run: |
# Find the PR associated with the merge commit SHA
# Note: This relies on the merge commit being directly pushed to main
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
if [ -z "$PR_DATA" ]; then
echo "::warning::Could not find merged PR for commit $MERGE_COMMIT_SHA. Skipping coverage embedding."
echo "coverage_found=false" >> $GITHUB_OUTPUT
exit 0
fi
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
echo "Found PR Number: $PR_NUMBER"
echo "Found PR Head SHA: $PR_HEAD_SHA"
# Find the latest *successful* playwright.yml run for the PR head commit so we
# never embed coverage from a failed/incomplete run.
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --status success --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then
echo "::warning::Could not find a successful 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA). Skipping coverage embedding."
echo "coverage_found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Found Run ID: $RUN_ID"
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "coverage_found=true" >> $GITHUB_OUTPUT
- name: Download coverage artifact from PR run
if: steps.find_pr_run.outputs.coverage_found == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p ./coverage-artifact
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
# Verify the artifact contains an HTML coverage report rather than checking a single asset
if [ -z "$(ls -A ./coverage-artifact)" ] || ! ls ./coverage-artifact/*.html >/dev/null 2>&1; then
echo "Failed to download or find an HTML coverage report in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
exit 1
fi
echo "Artifact downloaded successfully."
- name: Copy coverage to docs static directory
if: steps.find_pr_run.outputs.coverage_found == 'true'
run: |
# Copy files from the downloaded artifact directory
mkdir -p platform/docs/static/coverage
cp -r ./coverage-artifact/* platform/docs/static/coverage/
# Copy specific asset files from the downloaded artifact root to static root
cp ./coverage-artifact/base.css platform/docs/static/
cp ./coverage-artifact/block-navigation.js platform/docs/static/
cp ./coverage-artifact/prettify.css platform/docs/static/
cp ./coverage-artifact/prettify.js platform/docs/static/
cp ./coverage-artifact/favicon.png platform/docs/static/
cp ./coverage-artifact/sort-arrow-sprite.png platform/docs/static/
cp ./coverage-artifact/sorter.js platform/docs/static/
- name: Build docs
run: pnpm --filter ohif-docs run build
- name: Deploy to Netlify
# The docs are already built by the "Build docs" step above, so pass
# --no-build: `netlify deploy` runs the build command by default, which
# would otherwise pick up the repo-root netlify.toml (configured for the
# main viewer app) and build the wrong project. --dir is resolved
# relative to the repo root (the netlify.toml base), not the cwd, so it
# must be the full path to the prebuilt docs output. --filter selects
# the ohif-docs package so Netlify CLI does not abort on monorepo detection.
run: npx netlify-cli deploy --filter ohif-docs --dir=platform/docs/build --prod --no-build
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

View File

@ -1,100 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
pull_request:
branches: [ "main" ]
schedule:
- cron: '15 1 * * 5'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with:
category: "/language:${{matrix.language}}"

View File

@ -1,232 +0,0 @@
name: Playwright Tests
on:
pull_request:
branches: [master, release/*]
workflow_dispatch:
inputs:
cs3d_ref:
description: >-
CS3D branch (e.g. main, origin:feat/foo) or version (e.g. 4.18.2, 4.19+, 4.x). Only used
when ohif-integration label is present or via workflow_dispatch.
required: false
default: '4.19+'
permissions:
contents: read
pull-requests: read
issues: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright-tests:
timeout-minutes: 120
environment: fork-pr-approval
runs-on: [self-hosted, nashua]
strategy:
fail-fast: false
matrix:
node-version: [24.15.0]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node-version }}
# No `cache: pnpm`: this is a self-hosted runner with a persistent
# pnpm store on local disk. The Actions cache doesn't preserve the
# pnpm self-install's links faithfully, dropping pnpm/dist/worker.js
# and breaking `pnpm install` with MODULE_NOT_FOUND.
# Install pnpm via Corepack instead of pnpm/action-setup. The action's
# self-installer is a JS action run under the runner's bundled
# externals/node{version} and derives the `npm` path from that runtime — which
# on this self-hosted runner is corrupted (Cannot find module
# '../lib/cli.js'), so it dies regardless of `standalone:true`. Corepack
# ships inside the Node that setup-node just installed, reads the pinned
# `packageManager` (pnpm@11.5.2) from package.json, and fetches pnpm via
# Node's own https — it never invokes the npm CLI.
- name: Enable Corepack (pnpm)
shell: bash
run: |
corepack enable
corepack prepare --activate
# ── CS3D integration: detect label and ref type ──────────────────────
- name: Check for CS3D integration label
id: cs3d-check
run: bash .scripts/ci/cs3d-check-integration.sh
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
CS3D_REF_INPUT: ${{ github.event.inputs.cs3d_ref || '4.19+' }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Detect CS3D ref type
id: cs3d-ref
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
REF="${CS3D_REF}"
if [[ "$REF" =~ ^[0-9]+\.[0-9x]+\+?(\.[0-9x]+)?(-[a-zA-Z0-9._]+)?$ ]]; then
echo "type=version" >> "$GITHUB_OUTPUT"
RESOLVED=$(node .scripts/cs3d-resolve-version.mjs "$REF")
echo "version=$RESOLVED" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D version: $REF -> $RESOLVED"
else
echo "type=branch" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D branch: $REF"
fi
env:
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
# ── CS3D branch path: clone and build before OHIF install ───────────
- name: Clone CS3D
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
run: |
REF="${CS3D_REF}"
if [[ "$REF" == *:* ]]; then
REPO="https://github.com/${REF%%:*}/cornerstone3D.git"
BRANCH="${REF#*:}"
else
REPO="https://github.com/cornerstonejs/cornerstone3D.git"
BRANCH="$REF"
fi
echo "::notice::Cloning CS3D from $REPO branch $BRANCH"
git clone --depth 1 --branch "$BRANCH" "$REPO" libs/@cornerstonejs
env:
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
- name: Install & Build CS3D
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
working-directory: libs/@cornerstonejs
run: pnpm install --frozen-lockfile && pnpm run build:esm
# ── Common: install OHIF dependencies ───────────────────────────────
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ── CS3D branch path: link packages after OHIF install ──────────────
- name: Link CS3D packages
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
working-directory: libs/@cornerstonejs
run: node scripts/link-ohif-cornerstone-node-modules.mjs "$GITHUB_WORKSPACE"
# ── CS3D version path: update versions after OHIF install ───────────
- name: Set CS3D version
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'version'
run: |
node .scripts/cs3d-set-version.mjs "${CS3D_VERSION}"
pnpm install --no-frozen-lockfile
env:
CS3D_VERSION: ${{ steps.cs3d-ref.outputs.version }}
# ── Common: run tests ───────────────────────────────────────────────
- name: Install Playwright browsers
# Only chromium is used (see playwright.config.ts and tests/globalSetup.ts);
# firefox/webkit projects are commented out. Scoping the install to chromium
# avoids downloading + dep-validating browsers we never launch (the WebKit
# validation is what surfaces the "missing libwoff1/libflite1/..." error on
# hosts without those libs).
run: npx playwright install chromium
- name: Run Playwright tests
run: |
export NODE_OPTIONS="--max_old_space_size=10192"
bash .scripts/ci/with-nashua-lock.sh pnpm run test:e2e:coverage
# ── Common: collect test results and coverage ───────────────────────
- name: Create directory of test results
if: ${{ !cancelled() }}
run: |
mkdir -p packaged-test-results
cp -r ./tests/test-results packaged-test-results/ || true
cp -r ./tests/playwright-report packaged-test-results/ || true
- name: Upload directory of test results artifact
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-results
path: packaged-test-results/
retention-days: 5
- name: create the coverage report
run: |
pnpm exec nyc report --reporter=lcov --reporter=text
- name: Upload the coverage report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report-pr
path: coverage
retention-days: 3
# ── CS3D: build and deploy preview to Netlify ───────────────────────
- name: Log build context (OHIF/CS3D branch and version for build diagnosis)
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
if [[ "$CS3D_REF_TYPE" == "branch" ]]; then
echo "::notice::Build type: ohif-downstream | OHIF: ${{ github.repository }}@${{ github.ref }} (${{ github.sha }}) | CS3D: branch ${{ steps.cs3d-check.outputs.cs3d_ref }}"
else
echo "::notice::Build type: ohif-upstream | OHIF: ${{ github.repository }}@${{ github.ref }} (${{ github.sha }}) | CS3D: version ${{ steps.cs3d-ref.outputs.version }}"
fi
node .scripts/log-build-context.mjs
env:
BUILD_TYPE:
${{ steps.cs3d-ref.outputs.type == 'branch' && 'ohif-downstream' || 'ohif-upstream' }}
CS3D_REF_TYPE: ${{ steps.cs3d-ref.outputs.type }}
- name: Build OHIF viewer (CS3D preview)
if: steps.cs3d-check.outputs.enabled == 'true'
run: pnpm run build:ci
- name: Deploy CS3D preview to Netlify
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
RESULT=$(npx netlify-cli deploy --dir=platform/app/dist --alias="cs3d-pr-${PR_NUM}" --json --filter=@ohif/app) || {
echo "::error::Netlify deploy command failed"
exit 1
}
URL=$(echo "$RESULT" | jq -r '.deploy_url')
if [[ -z "$URL" || "$URL" == "null" ]]; then
echo "::error::Netlify deploy did not return a valid URL"
echo "$RESULT"
exit 1
fi
echo "::notice::CS3D preview deployed: $URL"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
PR_NUM: ${{ github.event.pull_request.number || 'manual' }}
# ── CS3D: log results ───────────────────────────────────────────────
- name: Log CS3D build used
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
if [[ "$CS3D_REF_TYPE" == "branch" ]]; then
echo "::notice::CS3D integration PASSED with branch ${CS3D_REF} (linked from libs/@cornerstonejs)"
else
echo "::notice::CS3D integration PASSED with @cornerstonejs/*@${CS3D_VERSION}"
fi
env:
CS3D_REF_TYPE: ${{ steps.cs3d-ref.outputs.type }}
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
CS3D_VERSION: ${{ steps.cs3d-ref.outputs.version }}
# ── Separate job: block merge when using a CS3D branch ─────────────
cs3d-branch-merge-guard:
name: 'CS3D Branch Merge Guard'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for CS3D branch usage
run: bash .scripts/ci/cs3d-branch-merge-guard.sh
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
CS3D_REF_INPUT: ${{ github.event.inputs.cs3d_ref || '4.19+' }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}

51
.gitignore vendored
View File

@ -1,8 +1,6 @@
# Packages # Packages
node_modules node_modules
.cursor/
.claude/
.nyc_output/
# Output # Output
build build
dist dist
@ -10,16 +8,11 @@ docs/_book
src/version.js src/version.js
junit.xml junit.xml
coverage/ coverage/
.docz/
.yarn/
.nx/
addOns/yarn.lock
playwright-report/
# YALC (for Erik) # YALC (for Erik)
.yalc .yalc
yalc.lock yalc.lock
*.dcm
# Logging, System files, misc. # Logging, System files, misc.
.idea/ .idea/
.npm .npm
@ -27,48 +20,8 @@ npm-debug.log
package-lock.json package-lock.json
yarn-error.log yarn-error.log
.DS_Store .DS_Store
.env
*.code-workspace
.directory
# Common Example Data Directories # Common Example Data Directories
sampledata/ sampledata/
example/deps/ example/deps/
docker/dcm4che/dcm4che-arc docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
# Locize settings
.locize
# autogenerated files
platform/app/src/pluginImports.js
CLAUDE.md
/Viewers.iml
platform/app/.recipes/Nginx-Dcm4Chee/logs/*
platform/app/.recipes/OpenResty-Orthanc/logs/*
.vercel
.vs
# PlayWright
node_modules/
tests/test-results/
tests/playwright-report/
/blob-report/
/playwright/.cache/
# cornerstone3D local linking
libs/
# Backup files
*~
# cornerstone3D local linking
libs/
link-cs3d.js
unlink-cs3d.js
auth.json

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[submodule "testdata"]
path = testdata
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
branch = main

45
.jscsrc Normal file
View File

@ -0,0 +1,45 @@
{
"requireCurlyBraces": {
"keywords": [ "if", "else", "for", "while", "do" ],
"allExcept" : ["return" , "continue", "break"]
},
"requireSpaceAfterKeywords": [ "if", "else", "for", "while", "do", "switch", "return" ],
"requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideParentheses": true,
"disallowKeywordsOnNewLine": ["else"],
"disallowNewlineBeforeBlockStatements": true,
"requirePaddingNewLinesAfterUseStrict": true,
"requirePaddingNewLinesAfterBlocks": {
"allExcept": ["inCallExpressions", "inArrayExpressions", "inProperties"]
},
"requireObjectKeysOnNewLine": true,
"requireSemicolons": true,
"requireSpaceAfterBinaryOperators": true,
"requireSpaceAfterComma": true,
"requireSpaceBeforeObjectValues": true,
"requireSpacesInsideObjectBrackets": "all",
"requireLineBreakAfterVariableAssignment": true,
"requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
"disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"],
"disallowSpaceBeforePostfixUnaryOperators": ["++", "--"],
"disallowKeywords": [ "with" ],
"disallowMultipleLineBreaks": true,
"requireLineFeedAtFileEnd": true,
"disallowSpaceAfterObjectKeys": true,
"disallowQuotedKeysInObjects": true,
"disallowMultipleSpaces": true,
"disallowVar": true,
"validateLineBreaks": "LF",
"validateIndentation": 4,
"validateQuoteMarks": {
"mark": "'",
"escape": true
},
"validateParameterSeparator": ", ",
"requireDollarBeforejQueryAssignment": true
}

93
.jshintrc Normal file
View File

@ -0,0 +1,93 @@
{
// JSHint Default Configuration File (as on JSHint website)
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : false, // true: Identifiers must be in camelCase
"curly" : false, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
"immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"latedef" : false, // true: Require variables/functions to be defined before being used
"newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` and `--`
"quotmark" : false, // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : "vars", // Unused variables:
// true : all variables, last function parameter
// "vars" : all variables only
// "strict" : all variables, all function parameters
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : false, // {int} Max number of characters per line
"varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed.
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : true, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"noyield" : false, // true: Tolerate generator functions with no yield statement in them.
"notypeof" : false, // true: Tolerate invalid typeof operator values
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"browser" : true, // Web Browser (window, document, etc)
"browserify" : false, // Browserify (node.js code in the browser)
"couch" : false, // CouchDB
"devel" : false, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jasmine" : false, // Jasmine
"jquery" : true, // jQuery
"mocha" : false, // Mocha
"mootools" : false, // MooTools
"node" : false, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"phantom" : false, // PhantomJS
"prototypejs" : false, // Prototype and Scriptaculous
"qunit" : false, // QUnit
"rhino" : false, // Rhino
"shelljs" : false, // ShellJS
"typed" : false, // Globals for typed array constructions
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
"globals" : {
"require": true,
"Package": true // Meteor Package definition
}
}

View File

@ -1,26 +0,0 @@
#!/bin/bash
# Set directory to location of this script
# https://stackoverflow.com/a/3355423/1867984
cd "$(dirname "$0")"
cd .. # Up to project root
# Helpful to verify which versions we're using
echo 'My pnpm version is... '
pnpm -v
node -v
# Build && Move PWA Output
pnpm run build:ci
mkdir -p ./.netlify/www/pwa
mv platform/app/dist/* .netlify/www/pwa -v
echo 'Web application built and copied'
# Build && Move Docusaurus Output (for the docs themselves)
pnpm --filter ohif-docs run build
mkdir -p ./.netlify/www/docs
mv platform/docs/build/* .netlify/www/docs -v
echo 'Docs built (docusaurus) and copied'
echo 'Nothing left to see here. Go home, folks.'

View File

@ -1,5 +0,0 @@
# Specific to our non-deploy-preview deploys
# Confgure redirects using netlify.toml
# PWA Redirect
/* /index.html 200

View File

@ -1,14 +0,0 @@
{
"name": "root",
"private": true,
"engines": {
"node": ">=24",
"pnpm": ">=11"
},
"scripts": {
"deploy": "netlify deploy --prod --dir ./../platform/app/dist"
},
"devDependencies": {
"netlify-cli": "2.21.0"
}
}

View File

@ -1,10 +0,0 @@
# Specific to our deploy-preview
# Our docs are published using CircleCI + GitBook
# Confgure redirects using netlify.toml
# PWA Demo
/pwa/* /pwa/index.html 200
# UI Demo
/ui/* /ui/index.html 200
# UI Demo
/docs/* /docs/index.html 200

View File

@ -1,21 +0,0 @@
<html>
<head>
<title>OHIF Viewer: Deploy Preview</title>
</head>
<body>
<h1>Index of Previews</h1>
<ul>
<li>
<a href="/pwa">OHIF Viewer</a>
</li>
<li>
<a href="/docs">Documentation</a>
</li>
<li>
<a href="/ui">UI: Component Library</a>
</li>
</ul>
</body>
</html>

View File

@ -1 +0,0 @@
24.15.0

5
.npmrc
View File

@ -1,5 +0,0 @@
node-linker=hoisted
strict-peer-dependencies=false
link-workspace-packages=true
prefer-workspace-packages=true
manage-package-manager-versions=false

View File

@ -1,24 +0,0 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"instrument": true,
"sourceMap": true,
"cache": false,
"all": true,
"include": [
"platform/*/src/**/*.ts",
"platform/*/src/**/*.js",
"extensions/*/src/**/*.ts",
"extensions/*/src/**/*.js",
"modes/*/src/**/*.ts",
"modes/*/src/**/*.js"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/test/**",
"**/tests/**",
"**/examples/**",
"**/stories/**",
"platform/docs/**"
]
}

View File

@ -1 +0,0 @@
*.md

View File

@ -1,12 +1,9 @@
{ {
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5", "trailingComma": "es5",
"printWidth": 100, "printWidth": 80,
"proseWrap": "always", "proseWrap": "always",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"arrowParens": "avoid", "endOfLine": "lf"
"singleAttributePerLine": true,
"endOfLine": "auto"
} }

14
.releaserc Normal file
View File

@ -0,0 +1,14 @@
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github",
[
"@semantic-release/exec",
{
"publishCmd": "mkdir tmp && echo ${nextRelease.version} > tmp/updated-version.txt"
}
]
]
}

View File

@ -1,42 +0,0 @@
#!/usr/bin/env bash
# CS3D branch merge guard: blocks merge when tests ran against a CS3D branch (not a version).
# Exits 0 when merge is allowed or guard is skipped; exits 1 when merge must be blocked.
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)
set -e
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "::notice::workflow_dispatch — no merge to block, skipping guard."
exit 0
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
ENABLED=true
CS3D_REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$CS3D_REF" ]]; then
CS3D_REF="4.19+"
fi
else
ENABLED=false
fi
else
ENABLED=false
fi
if [[ "$ENABLED" != "true" ]]; then
echo "::notice::No ohif-integration label — skipping merge guard."
exit 0
fi
# Check if the ref is a branch (not a version)
if [[ "$CS3D_REF" =~ ^[0-9]+\.[0-9x]+\+?(\.[0-9x]+)?(-[a-zA-Z0-9._]+)?$ ]]; then
echo "::notice::CS3D ref '$CS3D_REF' is a version — merge allowed."
exit 0
fi
echo "::error::Tests ran against CS3D branch '${CS3D_REF}' — this build cannot be merged."
echo "::error::Re-run with a published CS3D version (e.g. 4.19+) before merging."
exit 1

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# CS3D integration check: detects ohif-integration label and parses CS3D_REF.
# Writes to GITHUB_OUTPUT: enabled (true|false), cs3d_ref (when enabled).
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER, GITHUB_OUTPUT
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)
set -e
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "cs3d_ref=${CS3D_REF_INPUT:-4.19+}" >> "$GITHUB_OUTPUT"
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$REF" ]]; then
REF="4.19+"
fi
echo "cs3d_ref=${REF}" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D ref from PR body: ${REF}"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi

View File

@ -1,54 +0,0 @@
#!/usr/bin/env bash
#
# Run a command while holding the nashua Playwright mutex.
#
# Only one Playwright run may execute at a time on the shared nashua self-hosted
# runner — across THIS repo (OHIF) AND the cornerstone3D repo. GitHub's
# `concurrency:` is scoped per-repository and cannot coordinate across the two
# orgs, so the mutex lives on the box's filesystem.
#
# This is OHIF's own copy (the script can't be shared across separate repos). It
# MUST use the SAME lock path as cornerstone3D's copy (NASHUA_LOCK_FILE default
# below) — if the paths differ, the mutex silently does nothing.
#
# flock holds the lock via fd 9 and it releases automatically when the wrapped
# command exits — including on job cancel / timeout / SIGKILL — so there are no
# stale locks to clean up.
#
# Usage: .scripts/ci/with-nashua-lock.sh <command> [args...]
# Strict mode: -e abort on any unhandled command failure, -u treat use of an
# unset variable as an error, -o pipefail make a pipeline fail if ANY stage
# fails (not just the last). Catches mistakes early instead of pressing on.
set -euo pipefail
LOCK="${NASHUA_LOCK_FILE:-/var/tmp/nashua-playwright.lock}" # MUST match cornerstone3D's copy
LOCK_WAIT="${NASHUA_LOCK_WAIT:-5400}" # max seconds to wait
# Guard: a command to wrap is required. With no arguments there is nothing to
# run, so print usage and exit instead of silently grabbing and releasing the
# lock for no reason (which would mask a mis-wired workflow step).
if [ "$#" -eq 0 ]; then
echo "usage: $0 <command> [args...]" >&2
exit 2
fi
# Open the lock file on fd 9 (read-write, create if missing, never truncate).
exec 9<>"$LOCK" || { echo "::error::cannot open lock file $LOCK"; exit 1; }
# Try instantly; if busy, report who holds it, then block up to LOCK_WAIT.
if ! flock -n 9; then
echo "nashua Playwright runner busy — held by: $(cat "${LOCK}.info" 2>/dev/null || echo unknown). Waiting up to ${LOCK_WAIT}s…"
if ! flock -w "$LOCK_WAIT" 9; then
echo "::error::Timed out after ${LOCK_WAIT}s waiting for the nashua Playwright lock"
exit 1
fi
fi
# Record the current holder for other jobs' "held by" message (best-effort).
echo "${GITHUB_REPOSITORY:-local}#${GITHUB_RUN_ID:-0} @ $(date -u +%FT%TZ 2>/dev/null || true)" > "${LOCK}.info" 2>/dev/null || true
echo "✅ Acquired nashua Playwright lock ($LOCK); running: $*"
# Replace the shell with the command. fd 9 is inherited (not close-on-exec), so
# the lock is held for the whole command and released when it exits.
exec "$@"

View File

@ -1,55 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
const workflowPath = path.resolve(
process.cwd(),
'libs',
'@cornerstonejs',
'.github',
'workflows',
'ohif-downstream.yml'
);
if (!fs.existsSync(workflowPath)) {
console.error(`[cs3d:check] Workflow file not found: ${workflowPath}`);
process.exit(1);
}
const workflowText = fs.readFileSync(workflowPath, 'utf8');
const ohifRefMatch = workflowText.match(/^\s*OHIF_REF:\s*["']?([^"'\r\n]+)["']?\s*$/m);
if (!ohifRefMatch) {
console.error('[cs3d:check] Could not find OHIF_REF in ohif-downstream workflow.');
process.exit(1);
}
const expectedBranch = ohifRefMatch[1].trim();
let currentBranch = '';
try {
currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: process.cwd(),
encoding: 'utf8',
}).trim();
} catch (error) {
console.error('[cs3d:check] Failed to determine current git branch.');
process.exit(1);
}
if (currentBranch !== expectedBranch) {
console.error(
`[cs3d:check] Branch mismatch: current='${currentBranch}', expected='${expectedBranch}' from ${path.relative(
process.cwd(),
workflowPath
)}`
);
process.exit(1);
}
console.log(
`[cs3d:check] OK: current branch '${currentBranch}' matches OHIF_REF in ${path.relative(
process.cwd(),
workflowPath
)}`
);

View File

@ -1,63 +0,0 @@
#!/usr/bin/env node
/**
* Resolves a version pattern to a concrete npm version of @cornerstonejs/core.
*
* Patterns:
* 4.18.2 -> 4.18.2 (exact, returned as-is)
* 4.18.2-beta.3 -> 4.18.2-beta.3 (exact prerelease)
* 4.x -> latest 4.* from npm
* 4.17.x -> latest 4.17.* from npm
* 4.19+ -> latest >=4.19.0 <5.0.0-0 from npm (4.19 and later, same major)
*
* Prints the resolved version to stdout.
*/
import { execSync } from 'child_process';
const pattern = process.argv[2];
if (!pattern) {
console.error('Usage: cs3d-resolve-version.mjs <pattern>');
console.error(' e.g. 4.x, 4.17.x, 4.19+, 4.18.2, 4.18.2-beta.3');
process.exit(1);
}
// Exact version (no wildcard or range) — pass through unchanged
if (!pattern.includes('x') && !pattern.endsWith('+')) {
console.log(pattern);
process.exit(0);
}
// Convert "M.m+" to npm semver range ">=M.m.0 <(M+1).0.0-0"
let npmRange = pattern;
const plusMatch = pattern.match(/^(\d+)\.(\d+)\+$/);
if (plusMatch) {
const major = Number(plusMatch[1]);
const minor = Number(plusMatch[2]);
npmRange = `>=${major}.${minor}.0 <${major + 1}.0.0-0`;
}
try {
// Let npm resolve the range: @package@<range> → concrete version
const raw = execSync(`npm view @cornerstonejs/core@"${npmRange}" version --json`, {
encoding: 'utf8',
timeout: 30_000,
});
const resolved = JSON.parse(raw);
if (!resolved) {
console.error(`npm returned no version when resolving @cornerstonejs/core@${npmRange}`);
process.exit(1);
}
// npm may return an array of versions for ranges; pick the latest (last) one
const version = Array.isArray(resolved) ? resolved[resolved.length - 1] : resolved;
console.log(version);
} catch (err) {
const message =
(err && (err.stderr?.toString() || err.message || String(err))) ||
`Unknown error resolving @cornerstonejs/core@${pattern}`;
console.error(`Failed to resolve @cornerstonejs/core version for pattern "${pattern}":`);
console.error(message);
process.exit(1);
}

View File

@ -1,131 +0,0 @@
#!/usr/bin/env node
/**
* Updates all @cornerstonejs/* package versions across the OHIF workspace.
*
* Usage: node .scripts/cs3d-set-version.mjs <version>
*
* Only updates the 8 main CS3D packages (not codec packages):
* adapters, ai, core, dicom-image-loader, labelmap-interpolation,
* nifti-volume-loader, polymorphic-segmentation, tools
*/
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
import { resolve, dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = resolve(__dirname, '..');
const version = process.argv[2];
if (!version) {
console.error('Usage: cs3d-set-version.mjs <version>');
console.error(' e.g. 4.18.2, 4.19.0-beta.1');
process.exit(1);
}
// The 8 CS3D packages that are built from source (not codecs)
const CS3D_PACKAGES = [
'@cornerstonejs/adapters',
'@cornerstonejs/ai',
'@cornerstonejs/core',
'@cornerstonejs/dicom-image-loader',
'@cornerstonejs/labelmap-interpolation',
'@cornerstonejs/nifti-volume-loader',
'@cornerstonejs/polymorphic-segmentation',
'@cornerstonejs/tools',
];
// Read root package.json to get workspace globs
const rootPkgPath = resolve(rootDir, 'package.json');
const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf8'));
const workspaceGlobs = rootPkg.workspaces?.packages || rootPkg.workspaces || [];
// Collect all package.json paths from workspace globs
function findWorkspacePackageJsons() {
const paths = [rootPkgPath]; // include root
for (const pattern of workspaceGlobs) {
const parts = pattern.split('/');
let searchDir = rootDir;
let hasWildcard = false;
for (const part of parts) {
if (part === '*') {
hasWildcard = true;
break;
}
searchDir = join(searchDir, part);
}
if (hasWildcard) {
try {
const entries = readdirSync(searchDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const pkgJson = join(searchDir, entry.name, 'package.json');
if (existsSync(pkgJson)) {
paths.push(pkgJson);
}
}
}
} catch {
// directory doesn't exist, skip
}
} else {
const pkgJson = join(rootDir, pattern, 'package.json');
if (existsSync(pkgJson)) {
paths.push(pkgJson);
}
}
}
return paths;
}
// Update a dependencies object, returning count of changes
function updateDeps(deps, targetVersion) {
let count = 0;
if (!deps) return count;
for (const pkg of CS3D_PACKAGES) {
if (pkg in deps && deps[pkg] !== targetVersion) {
deps[pkg] = targetVersion;
count++;
}
}
return count;
}
const pkgPaths = findWorkspacePackageJsons();
let totalChanges = 0;
for (const pkgPath of pkgPaths) {
const content = readFileSync(pkgPath, 'utf8');
const pkg = JSON.parse(content);
let changes = 0;
changes += updateDeps(pkg.dependencies, version);
changes += updateDeps(pkg.devDependencies, version);
changes += updateDeps(pkg.peerDependencies, version);
changes += updateDeps(pkg.resolutions, version);
if (changes > 0) {
// Preserve original formatting (detect indent — restrict to spaces/tabs so
// we don't accidentally capture a CRLF newline as part of the indent string)
const indent = content.match(/^([ \t]+)/m)?.[1] || ' ';
writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n');
const rel = pkgPath.replace(rootDir + '/', '').replace(rootDir + '\\', '');
console.log(` Updated ${rel} (${changes} packages)`);
totalChanges += changes;
}
}
console.log(
`\nDone: ${totalChanges} version(s) updated to ${version} across ${pkgPaths.length} package files.`
);
console.log(
'This step changes package.json; the following install must not use a frozen Bun lockfile ' +
'(OHIF+CS3D combined “version” CI does: `bun install --config=./bunfig.update-lockfile.toml`). ' +
'Other installs stay frozen. Locally after this script, use that bun command and/or ' +
'`bun run install:update-lockfile` when you intend to commit lockfile updates.\n'
);

View File

@ -1,14 +0,0 @@
#!/bin/bash
# https://github.com/shelljs/shelljs
# https://github.com/shelljs/shelljs#exclude-options
PROJECT=$1
if [ -z "$PROJECT" ]
then
# Default
pnpm --filter @ohif/app run dev:viewer
else
pnpm --filter @ohif/app run "dev:$PROJECT"
fi
read -p 'Press [Enter] key to continue...'

View File

@ -1,273 +0,0 @@
/*
* This script uses nodejs to generate a JSON file from a DICOM study folder.
* You need to have dcmjs installed in your project.
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
*
* params:
* - studyFolder: path to the study folder which contains the DICOM files
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
* json file for the first instance of the first series of the first study will be
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
*
* as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when
* the .dcm file is hosted statically and can be accessed via a URL (like our example above)
* However, you can specify a new scheme bellow.
*
* - outputJSONPath: path to the output JSON file
* - scheme: default dicomweb if not provided
*/
const dcmjs = require('dcmjs');
const path = require('path');
const fs = require('fs').promises;
const args = process.argv.slice(2);
const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args;
if (args.length < 3 || args.length > 4) {
console.error(
'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
);
process.exit(1);
}
const model = {
studies: [],
};
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) {
try {
const files = await recursiveReadDir(studyDirectory);
console.debug('Processing...');
for (const file of files) {
if (!file.includes('.DS_Store') && !file.includes('.xml')) {
const arrayBuffer = await fs.readFile(file);
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme);
processInstance(instance);
}
}
console.log('Successfully loaded data');
model.studies.forEach(study => {
study.NumInstances = findInstancesNumber(study);
study.Modalities = findModalities(study).join('/');
});
await fs.writeFile(outputPath, JSON.stringify(model, null, 2));
console.log('JSON saved');
} catch (error) {
console.error(error);
}
}
async function recursiveReadDir(dir) {
let results = [];
const list = await fs.readdir(dir);
for (const file of list) {
const filePath = path.resolve(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
const res = await recursiveReadDir(filePath);
results = results.concat(res);
} else {
results.push(filePath);
}
}
return results;
}
function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) {
const relativePath = path.relative(studyDirectory, fileLocation);
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
return `${scheme}:${urlPrefix}${normalizedPath}`;
}
function processInstance(instance) {
const { StudyInstanceUID, SeriesInstanceUID } = instance;
let study = getStudy(StudyInstanceUID);
if (!study) {
study = createStudyMetadata(StudyInstanceUID, instance);
model.studies.push(study);
}
let series = getSeries(StudyInstanceUID, SeriesInstanceUID);
if (!series) {
series = createSeriesMetadata(instance);
study.series.push(series);
}
const instanceMetaData =
instance.NumberOfFrames > 1
? createInstanceMetaDataMultiFrame(instance)
: createInstanceMetaData(instance);
series.instances.push(...[].concat(instanceMetaData));
}
function getStudy(StudyInstanceUID) {
return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID);
}
function getSeries(StudyInstanceUID, SeriesInstanceUID) {
const study = getStudy(StudyInstanceUID);
return study
? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID)
: undefined;
}
const findInstancesNumber = study => {
let numInstances = 0;
study.series.forEach(aSeries => {
numInstances = numInstances + aSeries.instances.length;
});
return numInstances;
};
const findModalities = study => {
let modalities = new Set();
study.series.forEach(aSeries => {
modalities.add(aSeries.Modality);
});
return Array.from(modalities);
};
function createStudyMetadata(StudyInstanceUID, instance) {
return {
StudyInstanceUID,
StudyDescription: instance.StudyDescription,
StudyDate: instance.StudyDate,
StudyTime: instance.StudyTime,
PatientName: instance.PatientName,
PatientID: instance.PatientID || '1234', // this is critical to have
AccessionNumber: instance.AccessionNumber,
PatientAge: instance.PatientAge,
PatientSex: instance.PatientSex,
PatientWeight: instance.PatientWeight,
series: [],
};
}
function createSeriesMetadata(instance) {
return {
SeriesInstanceUID: instance.SeriesInstanceUID,
SeriesDescription: instance.SeriesDescription,
SeriesNumber: instance.SeriesNumber,
SeriesTime: instance.SeriesTime,
Modality: instance.Modality,
SliceThickness: instance.SliceThickness,
instances: [],
};
}
function commonMetaData(instance) {
return {
Columns: instance.Columns,
Rows: instance.Rows,
InstanceNumber: instance.InstanceNumber,
SOPClassUID: instance.SOPClassUID,
AcquisitionNumber: instance.AcquisitionNumber,
PhotometricInterpretation: instance.PhotometricInterpretation,
BitsAllocated: instance.BitsAllocated,
BitsStored: instance.BitsStored,
PixelRepresentation: instance.PixelRepresentation,
SamplesPerPixel: instance.SamplesPerPixel,
PixelSpacing: instance.PixelSpacing,
HighBit: instance.HighBit,
ImageOrientationPatient: instance.ImageOrientationPatient,
ImagePositionPatient: instance.ImagePositionPatient,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
ImageType: instance.ImageType,
Modality: instance.Modality,
SOPInstanceUID: instance.SOPInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
StudyInstanceUID: instance.StudyInstanceUID,
WindowCenter: instance.WindowCenter,
WindowWidth: instance.WindowWidth,
RescaleIntercept: instance.RescaleIntercept,
RescaleSlope: instance.RescaleSlope,
};
}
function conditionalMetaData(instance) {
return {
...(instance.ConceptNameCodeSequence && {
ConceptNameCodeSequence: instance.ConceptNameCodeSequence,
}),
...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }),
...(instance.ReferencedSeriesSequence && {
ReferencedSeriesSequence: instance.ReferencedSeriesSequence,
}),
...(instance.SharedFunctionalGroupsSequence && {
SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence,
}),
...(instance.PerFrameFunctionalGroupsSequence && {
PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence,
}),
...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }),
...(instance.ContentTemplateSequence && {
ContentTemplateSequence: instance.ContentTemplateSequence,
}),
...(instance.CurrentRequestedProcedureEvidenceSequence && {
CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence,
}),
...(instance.CodingSchemeIdentificationSequence && {
CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence,
}),
...(instance.RadiopharmaceuticalInformationSequence && {
RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence,
}),
...(instance.ROIContourSequence && {
ROIContourSequence: instance.ROIContourSequence,
}),
...(instance.StructureSetROISequence && {
StructureSetROISequence: instance.StructureSetROISequence,
}),
...(instance.ReferencedFrameOfReferenceSequence && {
ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence,
}),
...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }),
...(instance.Units && { Units: instance.Units }),
...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }),
...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }),
...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }),
...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }),
...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }),
...(instance.FrameTime && { FrameTime: instance.FrameTime }),
...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }),
...(instance.SequenceOfUltrasoundRegions && {
SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions,
}),
};
}
function createInstanceMetaData(instance) {
const metadata = {
...commonMetaData(instance),
...conditionalMetaData(instance),
};
return { metadata, url: instance.fileLocation };
}
function createInstanceMetaDataMultiFrame(instance) {
const instances = [];
const commonData = commonMetaData(instance);
const conditionalData = conditionalMetaData(instance);
for (let i = 1; i <= instance.NumberOfFrames; i++) {
const metadata = { ...commonData, ...conditionalData };
const result = { metadata, url: instance.fileLocation + `?frame=${i}` };
instances.push(result);
}
return instances;
}
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme);

View File

@ -1,108 +0,0 @@
#!/usr/bin/env node
/**
* Logs build context (OHIF branch/version, CS3D source) for diagnosing build issues on GitHub.
* Run from repo root. Used by version.mjs and can be invoked from CI workflows.
*/
import { execa } from 'execa';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
function log(msg) {
console.log(`[build-context] ${msg}`);
}
async function detectCs3dSource() {
const libsCs3d = path.join(REPO_ROOT, 'libs/@cornerstonejs');
const coreInNodeModules = path.join(REPO_ROOT, 'node_modules/@cornerstonejs/core');
try {
const libsExists = await fs.access(libsCs3d).then(() => true).catch(() => false);
if (!libsExists) {
return { source: 'npm', detail: 'published @cornerstonejs packages (no libs/@cornerstonejs)' };
}
const coreStat = await fs.lstat(coreInNodeModules).catch(() => null);
const isSymlink = coreStat?.isSymbolicLink();
if (isSymlink) {
const target = await fs.readlink(coreInNodeModules);
return { source: 'integrated', detail: `linked from libs/@cornerstonejs (→ ${target})` };
}
return { source: 'npm', detail: 'published @cornerstonejs packages (libs exists but not linked)' };
} catch {
return { source: 'unknown', detail: 'could not detect' };
}
}
async function getCs3dVersion() {
try {
const corePkg = path.join(REPO_ROOT, 'node_modules/@cornerstonejs/core/package.json');
const pkg = JSON.parse(await fs.readFile(corePkg, 'utf-8'));
return pkg.version || 'unknown';
} catch {
return 'unknown';
}
}
async function getCs3dBranchFromLibs() {
try {
const libsCs3d = path.join(REPO_ROOT, 'libs/@cornerstonejs');
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: libsCs3d,
});
return stdout;
} catch {
return null;
}
}
async function run() {
const inCI = process.env.GITHUB_ACTIONS === 'true';
const buildType = inCI ? (process.env.BUILD_TYPE || 'ohif-upstream') : 'local';
log('═══════════════════════════════════════════════════════════════');
log('Build context (for diagnosing GitHub build issues)');
log('═══════════════════════════════════════════════════════════════');
if (inCI) {
log(`Build type: ${process.env.BUILD_TYPE || 'ohif-upstream'}`);
log(`GitHub repo: ${process.env.GITHUB_REPOSITORY || 'unknown'}`);
log(`GitHub ref: ${process.env.GITHUB_REF || 'unknown'}`);
log(`GitHub SHA: ${process.env.GITHUB_SHA || 'unknown'}`);
log(`Workflow: ${process.env.GITHUB_WORKFLOW || 'unknown'}`);
}
try {
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: REPO_ROOT,
});
const { stdout: sha } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
cwd: REPO_ROOT,
});
log(`OHIF branch: ${branch} (${sha})`);
} catch {
log('OHIF branch: (not a git repo or error)');
}
const cs3d = await detectCs3dSource();
log(`CS3D source: ${cs3d.source}${cs3d.detail}`);
if (cs3d.source === 'integrated') {
const branch = await getCs3dBranchFromLibs();
if (branch) log(`CS3D branch (libs/@cornerstonejs): ${branch}`);
} else {
const ver = await getCs3dVersion();
log(`CS3D version (@cornerstonejs/core): ${ver}`);
}
log('═══════════════════════════════════════════════════════════════');
}
run().catch((err) => {
console.error('[build-context] Error:', err.message);
process.exitCode = 1;
});

View File

@ -1,13 +1,8 @@
{ {
"recommendations": [ "recommendations": [
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker", "sysoev.language-stylus",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"mikestead.dotenv", "mikestead.dotenv"
"bungcip.better-toml",
"silvenon.mdx",
"gruntfuggly.todo-tree",
"wayou.vscode-todo-highlight",
"bradlc.vscode-tailwindcss"
] ]
} }

28
.vscode/launch.json vendored
View File

@ -1,28 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
// {
// "name": "Debug Jest Tests",
// "type": "node",
// "request": "launch",
// "runtimeArgs": [
// "--inspect-brk",
// "${workspaceRoot}/node_modules/.bin/jest",
// "--runInBand"
// ],
// "console": "integratedTerminal",
// "internalConsoleOptions": "neverOpen",
// "port": 9229
// }
]
}

111
.vscode/settings.json vendored
View File

@ -1,9 +1,10 @@
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.rulers": [80, 120], "editor.rulers": [80, 120],
// === // ===
// Spacing // Spacing
// === // ===
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.trimAutoWhitespace": true, "editor.trimAutoWhitespace": true,
@ -11,107 +12,19 @@
"files.eol": "\n", "files.eol": "\n",
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimFinalNewlines": true, "files.trimFinalNewlines": true,
// === // ===
// Event Triggers // Event Triggers
// === // ===
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"eslint.run": "onSave", "eslint.run": "onSave",
"jest.autoRun": "off", "eslint.validate": [
"prettier.disableLanguages": ["html"], { "language": "javascript", "autoFix": true },
"prettier.endOfLine": "lf", { "language": "javascriptreact", "autoFix": true }
"workbench.colorCustomizations": {}, ],
"editor.codeActionsOnSave": { "prettier.disableLanguages": [],
"source.fixAll.eslint": "explicit" "prettier.endOfLine": "lf"
},
"cSpell.userWords": [
"aabb",
"architectured",
"attrname",
"Barksy",
"browserslist",
"bulkdata",
"Cacheable",
"cfun",
"clonedeep",
"Colormap",
"Colormaps",
"Comlink",
"cornerstonejs",
"Crosshairs",
"datasource",
"dcmjs",
"decache",
"decached",
"decaching",
"deepmerge",
"Dicom",
"dicomweb",
"DISPLAYSETS",
"glwindow",
"grababble",
"grabbable",
"Hounsfield",
"Interactable",
"Interactor",
"istyle",
"kitware",
"labelmap",
"labelmaps",
"livewire",
"Mergeable",
"multiframe",
"nifti",
"ofun",
"OHIF",
"polylines",
"POLYSEG",
"prapogation",
"precisionmetrics",
"prefetch",
"Prescaled",
"pydicom",
"Radiopharmaceutical",
"rasterizing",
"reconstructable",
"Rehydratable",
"renderable",
"resampler",
"resemblejs",
"reslice",
"resliced",
"Reslices",
"roadmap",
"ROADMAPS",
"rtstruct",
"Segmentations",
"semibold",
"sitk",
"SUBRESOLUTION",
"suvbsa",
"suvbw",
"suvlbm",
"textbox",
"thresholded",
"thresholding",
"timepoint",
"timepoints",
"TMTV",
"TOOLGROUP",
"tqdm",
"transferables",
"typedoc",
"unsubscriptions",
"uuidv",
"viewplane",
"viewports",
"Voxel",
"Voxels",
"Vtkjs",
"wado",
"wadors",
"wadouri",
"workerpool",
"Colorbar",
"Colorbars"
]
} }

View File

@ -1,22 +0,0 @@
const path = require('path');
function excludeNodeModulesExcept(modules) {
var pathSep = path.sep;
if (pathSep == '\\')
// must be quoted for use in a regexp:
pathSep = '\\\\';
var moduleRegExps = modules.map(function (modName) {
return new RegExp('node_modules' + pathSep + modName);
});
return function (modulePath) {
if (/node_modules/.test(modulePath)) {
for (var i = 0; i < moduleRegExps.length; i++)
if (moduleRegExps[i].test(modulePath)) return false;
return true;
}
return false;
};
}
module.exports = excludeNodeModulesExcept;

View File

@ -1,51 +0,0 @@
const path = require('path');
// Shared module-resolution rules (alias + module search paths) used by BOTH
// build pipelines so they cannot drift apart:
// - the webpack/rspack build: webpack.base.js -> webpack.pwa.js, plus every
// per-package webpack.prod.js / webpack.dev.js that merges webpack.base.js
// - the rsbuild build: rsbuild.config.ts (dev:fast)
//
// Plugin aliases (writePluginImportsFile.getPluginResolveAliases) are NOT
// included here: they depend on pluginConfig.json and are merged in separately
// by each config.
//
// All paths are anchored to this file's location (the repo-root `.webpack/`
// directory), so the values are identical regardless of which package's build
// is doing the resolving.
const alias = {
// Some extensions import app-level utilities (e.g. history,
// preserveQueryParameters) from '@ohif/app'. pnpm's isolated layout does not
// expose the top-level app package to those extensions, and adding it as a
// workspace dependency would create an app<->default cycle, so we resolve the
// bare specifier to the app source here ($ = exact match).
'@ohif/app$': path.resolve(__dirname, '../platform/app/src/index.js'),
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
};
// Directories to search when resolving modules. The leading bare 'node_modules'
// preserves the default importer-relative walk-up, which pnpm's isolated layout
// requires so that transitive deps (e.g. react-remove-scroll -> tslib 2.x)
// resolve to the sibling copy inside .pnpm/<pkg>/node_modules rather than a
// hoisted older version.
const moduleSearchPaths = [
'node_modules',
path.resolve(__dirname, '../node_modules'),
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
];
// Build the resolve.modules array for a build. `srcDir` is the building
// package's source root; it is appended last (matching the previous inline
// behavior in webpack.base.js). Pass nothing to get just the shared paths.
function getModules(srcDir) {
return srcDir ? [...moduleSearchPaths, srcDir] : [...moduleSearchPaths];
}
module.exports = { alias, moduleSearchPaths, getModules };

View File

@ -1,29 +0,0 @@
const autoprefixer = require('autoprefixer');
const path = require('path');
const tailwindcss = require('tailwindcss');
const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
const rspack = require('@rspack/core');
const devMode = process.env.NODE_ENV !== 'production';
const cssToJavaScript = {
test: /\.css$/,
use: [
//'style-loader',
devMode ? 'style-loader' : rspack.CssExtractRspackPlugin.loader,
{ loader: 'css-loader', options: { importLoaders: 1 } },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
verbose: true,
plugins: [
[tailwindcss(tailwindConfigPath)],
[autoprefixer('last 2 version', 'ie >= 11')],
],
},
},
},
],
};
module.exports = cssToJavaScript;

View File

@ -1,10 +0,0 @@
/**
* This is exclusively used by `vtk.js` to bundle glsl files.
*/
const loadShaders = {
test: /\.glsl$/i,
include: /vtk\.js[\/\\]Sources/,
loader: 'shader-loader',
};
module.exports = loadShaders;

View File

@ -1,17 +0,0 @@
/**
* This allows us to include web workers in our bundle, and VTK.js
* web workers in our bundle. While this increases bundle size, it
* cuts down on the number of includes we need for `script tag` usage.
*/
const loadWebWorkers = {
test: /\.worker\.js$/,
include: /vtk\.js[\/\\]Sources/,
use: [
{
loader: 'worker-loader',
options: { inline: true, fallback: false },
},
],
};
module.exports = loadWebWorkers;

View File

@ -1,10 +0,0 @@
const stylusToJavaScript = {
test: /\.styl$/,
use: [
{ loader: 'style-loader' }, // 3. Style nodes from JS Strings
{ loader: 'css-loader' }, // 2. CSS to CommonJS
{ loader: 'stylus-loader' }, // 1. Stylus to CSS
],
};
module.exports = stylusToJavaScript;

View File

@ -1,47 +0,0 @@
const excludeNodeModulesExcept = require('./../helpers/excludeNodeModulesExcept.js');
function transpileJavaScript(mode) {
const exclude =
mode === 'production'
? excludeNodeModulesExcept([
// Workspace packages (needed for pnpm shamefully-hoist where they resolve through node_modules)
'@ohif',
// 'dicomweb-client',
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
'react-dnd',
// https://github.com/dcmjs-org/dcmjs/blob/master/.babelrc
// https://github.com/react-dnd/react-dnd/issues/1342
// 'dcmjs', // contains: loglevelnext
// https://github.com/shellscape/loglevelnext#browser-support
// 'loglevelnext',
// https://github.com/dcmjs-org/dicom-microscopy-viewer/issues/35
// 'dicom-microscopy-viewer',
// https://github.com/openlayers/openlayers#supported-browsers
// 'ol', --> Should be fine
])
: excludeNodeModulesExcept([]);
return {
// Include mjs, ts, tsx, js, and jsx files.
test: /\.(mjs|ts|js)x?$/,
// These are packages that are not transpiled to our lowest supported
// JS version (currently ES5). Most of these leverage ES6+ features,
// that we need to transpile to a different syntax.
exclude: [/(codecs)/, /(dicomicc)/, exclude],
loader: 'babel-loader',
options: {
// Find babel.config.js in monorepo root
// https://babeljs.io/docs/en/options#rootmode
rootMode: 'upward',
envName: mode,
cacheCompression: false,
// Note: This was causing a lot of issues with yarn link of the cornerstone
// only set this to true if you don't have a yarn link to external libs
// otherwise expect the lib changes not to be reflected in the dev server
// as it will be cached
cacheDirectory: false,
},
};
}
module.exports = transpileJavaScript;

View File

@ -1,257 +0,0 @@
// ~~ ENV
const dotenv = require('dotenv');
//
const path = require('path');
const fs = require('fs');
const webpack = require('@rspack/core');
// ~~ PLUGINS
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
// .rules;
// ~~ RULES
// const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
// Module-resolution rules shared with the rsbuild build (see rsbuild.config.ts).
const resolveConfig = require('./resolveConfig');
// Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
let ReactRefreshWebpackPlugin;
try {
const mod = require('@rspack/plugin-react-refresh');
ReactRefreshWebpackPlugin = mod.ReactRefreshRspackPlugin || mod.default || mod;
} catch { ReactRefreshWebpackPlugin = null; }
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
const IS_COVERAGE = process.env.COVERAGE === 'true';
// read from ../version.txt
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
//
dotenv.config();
const defineValues = {
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
'process.env.TEST_ENV': JSON.stringify(process.env.TEST_ENV || ''),
};
// Only redefine updated values. This avoids warning messages in the logs
if (!process.env.APP_CONFIG) {
defineValues['process.env.APP_CONFIG'] = '';
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
const mode = NODE_ENV === 'production' ? 'production' : 'development';
const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
// `rspack serve` (@rspack/cli) auto-enables lazyCompilation for web-only
// apps unless the config defines it explicitly. The on-demand proxy chunks
// it produces fail to load in the headless cypress/electron e2e run
// (ChunkLoadError on cornerstone vendor chunks), so disable it here to match
// the rsbuild build (see rsbuild.config.ts `dev.lazyCompilation: false`).
lazyCompilation: false,
entry: ENTRY,
optimization: {
// splitChunks: {
// // include all types of chunks
// chunks: 'all',
// },
//runtimeChunk: 'single',
minimize: isProdBuild,
sideEffects: false,
},
output: {
// clean: true,
publicPath: '/',
},
context: SRC_DIR,
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
cache: isProdBuild ? false : { type: 'filesystem' },
module: {
noParse: [/(dicomicc)/],
rules: [
...(isProdBuild
? []
: [
...(IS_COVERAGE
? [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
plugins: ['istanbul'],
},
},
},
]
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
]),
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
// loadShadersRule,
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
cssToJavaScript,
{
test: /\.wasm/,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/images/[name].[ext]',
},
},
],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
], //.concat(vtkRules),
},
resolve: {
mainFields: ['module', 'browser', 'main'],
// alias and modules are shared with the rsbuild build via ./resolveConfig
// so the two pipelines resolve identically.
alias: {
...resolveConfig.alias,
},
modules: resolveConfig.getModules(SRC_DIR),
// Attempt to resolve these extensions in order.
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// Workspace packages use relative imports between sibling packages.
// Resolve symlinks to keep those imports anchored at the real package paths.
symlinks: true,
fallback: {
fs: false,
path: false,
zlib: false,
buffer: require.resolve('buffer'),
},
},
node: {
// Leave __filename / __dirname references alone. The previous 'mock'
// value triggers an rspack warning whenever bundled deps reference
// __dirname (e.g. Emscripten-compiled cornerstone codecs). Those refs
// sit inside `if (ENVIRONMENT_IS_NODE)` branches that never execute in
// the browser, so leaving them un-substituted is harmless at runtime.
__filename: false,
__dirname: false,
},
plugins: [
new webpack.DefinePlugin(defineValues),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new webpack.IgnorePlugin({
resourceRegExp: /^(fs|path)$/,
contextRegExp: /@cornerstonejs[\\/]codec-/,
}),
...(isProdBuild || IS_COVERAGE || !ReactRefreshWebpackPlugin
? []
: [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
};
if (isProdBuild) {
config.optimization.minimizer = [new webpack.SwcJsMinimizerRspackPlugin()];
}
if (isQuickBuild) {
config.optimization.minimize = false;
config.devtool = false;
}
return config;
};

205
AGENTS.md
View File

@ -1,205 +0,0 @@
# AGENTS.md
This file provides guidance to AI coding agents (Claude, Codex, and other LLM tools) when working with code in this repository.
## Project Overview
This is **OHIF** v3 (Open Health Imaging Foundation) - a medical imaging viewer. It's an extensible web imaging platform.
## Development Commands
### Main Development
```bash
# Start development server for all packages
yarn dev
```
### Building
```bash
# Build all packages for production
yarn build
# Build specific packages
cd platform/app && yarn build # Main viewer app
```
## Architecture Overview
### Monorepo Structure
- **`platform/`** - Core OHIF infrastructure
- `app/` - Main viewer application (`@ohif/viewer`)
- `core/` - Core services and utilities
- `ui-next/` - Modern UI component library
- **`extensions/`** - Modular functionality plugins
- **`modes/`** - Application workflow configurations
### Key Extension Architecture
**Extension System**: Each extension exports modules (viewports, tools, panels, commands) that the app dynamically loads. Extensions are self-contained with their own webpack builds.
**Core Extensions:**
- `cornerstone/` - Medical image rendering engine
- `cornerstone-dicom-pmp/` - DICOM PMP support
- `cornerstone-dicom-seg/` - DICOM Segmentation support
- `cornerstone-dicom-sr/` - DICOM SR support
- `dicom-pdf/` - DICOM PDF support
- `dicom-video/` - DICOM Video support
- `measurement-tracking/` - Measurement tracking support
- `default/` - Standard OHIF functionality
### Service-Oriented Design (PUB-SUB)
The app uses a Services Manager pattern with these core services:
- **Display Set Service**: Manages image series organization
- **Measurement Service**: Handles annotations and measurements
- **Hanging Protocol Service**: Controls image layout and display rules
- **UI Service**: Manages panels, modals, and notifications
- **Segmentation Service**: AI/ML powered image segmentation, loading segmentations, etc.
- **Viewport Grid Service**: Manages viewport layout and display rules
- **Viewport Display Set History Service**: Manages viewport display set history
- **Viewport Dialog Service**: Manages viewport dialogs
- **Notification Service**: Manages notifications
- **Modal Service**: Manages modals
- **Dialog Service**: Manages dialogs, more general not just viewport dialogs
- **Customization Service**: Manages customization of the app
- **Toolbar Service**: Manages the toolbar, viewport action corners, tool states
- **User Authentication Service**: Manages user authentication, but used only for injecting tokens in dicomweb requests in our context
- **Panel Service**: Manages side panels
- **Cornerstone Viewport Service**: Manages the cornerstone viewport, rendering engines, presentation states, more tightly coupled to cornerstone than the other services
- **Tool Group Service**: Manages tool groups, creating and managing tool groups, etc.
- **Sync Group Service**: Manages sync groups, syncing zooming, panning, scrolling, etc.
- **Cornerstone Cache Service**: Manages the cornerstone cache, caching images, etc.
Most of the services utilize a pub sub architecture and extend the pub sub service interace at `pubSubServiceInterface.ts`
### Commands Manager
The Commands Manager tracks named commands (or functions) that are scoped to
a context. When we attempt to run a command with a given name, we look for it
in our active contexts, in the order specified.
If found, we run the command, passing in any application
or call specific data specified in the command's definition.
You can call `commandsManager.runCommand` to run a command.
### Extension Manager
Aggregates and exposes extension modules throughout the OHIF application, manages data sources, and provides a centralized registry for accessing extension functionality.
### Build System
**Yarn Workspaces**: Optimized monorepo builds with dependency caching
**Webpack 5**: Module federation for dynamic extension loading
**Plugin Import System**: Extensions auto-register via `writePluginImportsFile.js`
### Key Technologies
- **React 18 + TypeScript**: UI framework
- **Cornerstone.js**: Medical image rendering
- **DICOM**: Medical imaging standard support
- **ONNX Runtime**: AI model inference (SAM segmentation models)
- **Zustand**: State management
- **TailwindCSS**: Styling system
## Development Patterns
### Adding New Tools
1. Create tool class in `extensions/cornerstone/src/tools/`
2. Register in tool module's `toolNames.ts`
3. Add to toolbar via `getToolbarModule.tsx`
4. Add measurement mapping if needed in `measurementServiceMappings/`
### Creating Extensions
Extensions must export:
- `id.js` - Unique extension identifier
- `index.tsx` - Extension registration
- Module functions (`getToolbarModule`, `getViewportModule`, etc.)
### Viewport Customization
Custom viewports extend base Cornerstone viewport:
- Override render methods for custom overlays
- Implement measurement tracking
- Add viewport-specific tools and interactions
### Service Integration
Register services in extension's `servicesManager.registerService` and access via:
```javascript
const { MeasurementService } = servicesManager.services;
```
### Creating stores
To create a store, you can make one in your extension's `stores/` directory, and you can follow the example of an existing store such as `useLutPresentationStore.ts` or `useSynchronizersStore.ts`.
### Creating hooks
To create a hook, you can make one in your extension's `hooks/` directory, and you can follow the example of an existing hook such as `usePatientInfo.tsx`.
### Creating providers
To create a provider, you can make one in your extension's `providers/` or `contexts/` directory, and you can follow the example of an existing provider such as `ViewportGridProvider.tsx`.
### Adding new icons
To add a new icon, you can add it to the `icons/` directory, then register the icon using `import { addIcon } from '@ohif/extension-default/src/utils'`
### Creating synchronizers
You can create custom synchronizers and place them in the `synchronizers/` directory, you can follow the example of `frameViewSynchronizer.ts`
### Utilites
Any new utilites should be placed in the `utils/` directory, and you can follow the example of `formatPN.ts`
### Commands
Commands are created in the commandsModule of the extension, for example the cornerstone extension has `commandsModule.tsx`, sometimes its also named `getCommandsModule.tsx.`
### Overriding OHIF Components
To override an OHIF component, you can create a new component in your extension's `components/` directory, then import it instead of the original ui-next component.
### Mode layout
The layoutTemplate is a function that returns a layout object, you can follow the example of `longitudinal/src/index.ts`. This would be helpful when you need to override a component as you can know where to look for the original component.
### Pub Sub
Always prioritrize pub sub, by calling a services subscribe over useEffects as it's more reliable, for example
```ts
useEffect(() => {
const subscriptions = [
cornerstoneViewportService.subscribe(EVENTS.VIEWPORT_DATA_CHANGED, handleViewportDataChanged),
syncGroupService.subscribe(EVENTS.VIEWPORT_REMOVED, onHotKeyRemoval),
syncGroupService.subscribe(EVENTS.VIEWPORT_ADDED, onHotKeyAddition),
];
return () => {
subscriptions.forEach(({ unsubscribe }) => unsubscribe());
};
}, []);
```
### Never modify core architecture
Do not modify the core and always find a way to implement the solution via the extensions and modes, only modify core as a last resort if all other fail or there's an architectural constraint.
## Skills
The `ohif-test-agent` skill (Playwright E2E test guidance) lives at `.agents/skills/ohif-test-agent/`.
## Configuration
### Plugin Configuration
Extensions are auto-discovered via `pluginConfig.json` and dynamically imported during build.
## Medical Imaging Specifics
### DICOM Support
- Multi-format: CT, MRI, X-Ray, Mammography, Ultrasound
- SOP Class handlers for specialized DICOM types (RT, SEG, SR)
- DICOMweb protocol for web-based image retrieval
### Hanging Protocols
Define how images are arranged and displayed:
- Located in `hps/` directories
- JSON configuration with viewport rules
- Support for priors comparison and multi-monitor layouts
### Measurement Tools
- Cornerstone Tools integration for annotations
- Bidirectional measurements, polylines, annotations
- Export capabilities (DICOM SR, CSV reports)
- AI-assisted measurements via ONNX models

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1 +0,0 @@
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)

View File

@ -1,297 +0,0 @@
# OHIF public demo data sets
The OHIF Viewer's public demo page, available at https://viewer.ohif.org/, uses publicly anonymized demo datasets.
These datasets were mostly obtained from the [NIH NCI Imaging Data Commons](https://datacommons.cancer.gov/repository/imaging-data-commons)
and [NIH NCI TCIA](https://www.cancerimagingarchive.net/). Before listing the datasets,
we would like to extend a special thank you to all groups who have made their datasets publicly available.
Without them, we would not have been able to create this demo page.
Please find below the list of datasets used on the demo page, along with their respective citations.
## Platforms
### NIH NCI IDC
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
### NIH NCI TCIA
- Clark, K., Vendt, B., Smith, K., Freymann, J., Kirby, J., Koppel, P., Moore, S., Phillips, S., Maffitt, D., Pringle, M., Tarbox, L., & Prior, F. (2013). The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository. Journal of Digital Imaging, 26(6), 10451057. https://doi.org/10.1007/s10278-013-9622-7
## Datasets
Below you can find the StudyInstanceUID of the studies that are used in the demo page along with their citations.
### 1.3.6.1.4.1.14519.5.2.1.267424821384663813780850856506829388886
Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm (Vestibular-Schwannoma-SEG)
- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Bisdas, S., Dimitriadis, A., Grishchuck, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.9YTJ-5Q73
- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Dimitriadis, A., Grishchuk, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S. R., Bisdas, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of vestibular schwannoma from MRI, an open annotated dataset and baseline algorithm. In Scientific Data (Vol. 8, Issue 1). Springer Science and Business Media LLC. https://doi.org/10.1038/s41597-021-01064-w
### 1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463
### 1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339
ACRIN-NSCLC-FDG-PET (ACRIN 6668)
- Kinahan, P., Muzi, M., Bialecki, B., Herman, B., & Coombs, L. (2019). Data from the ACRIN 6668 Trial NSCLC-FDG-PET (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/tcia.2019.30ilqfcl
- Machtay, M., Duan, F., Siegel, B. A., Snyder, B. S., Gorelick, J. J., Reddin, J. S., Munden, R., Johnson, D. W., Wilf, L. H., DeNittis, A., Sherwin, N., Cho, K. H., Kim, S., Videtic, G., Neumann, D. R., Komaki, R., Macapinlac, H., Bradley, J. D., & Alavi, A. (2013). Prediction of Survival by [18F]Fluorodeoxyglucose Positron Emission Tomography in Patients With Locally Advanced NonSmall-Cell Lung Cancer Undergoing Definitive Chemoradiation Therapy: Results of the ACRIN 6668/RTOG 0235 Trial. In Journal of Clinical Oncology (Vol. 31, Issue 30, pp. 38233830). American Society of Clinical Oncology (ASCO). https://doi.org/10.1200/jco.2012.47.5947
### 2.25.103659964951665749659160840573802789777
The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM)
- Scarpace, L., Mikkelsen, T., Cha, S., Rao, S., Tekchandani, S., Gutman, D., Saltz, J. H., Erickson, B. J., Pedano, N., Flanders, A. E., Barnholtz-Sloan, J., Ostrom, Q., Barboriak, D., & Pierce, L. J. (2016). The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM) (Version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.RNYFUYE9
### 1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458
Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT)
- Tong T., Li M. (2022) Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT) [Dataset]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/p5k5-tg43
- Li, M., Gong, J., Bao, Y., Huang, D., Peng, J., & Tong, T. (2022). Special issue “The advance of solid tumor research in China”: Prognosis prediction for stage II colorectal cancer by fusing computed tomography radiomics and deeplearning features of primary lesions and peripheral lymph nodes. In International Journal of Cancer. Wiley. https://doi.org/10.1002/ijc.34053
### 1.3.6.1.4.1.14519.5.2.1.3023.4024.215308722288168917637555384485
The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC)
- Roche, C., Bonaccio, E., & Filippini, J. (2016). The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC) (Version 3) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.CX6YLSUX
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
BREAST-DIAGNOSIS
- Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
### 1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785
Multimodality annotated HCC cases with and without advanced imaging segmentation (HCC-TACE-Seg)
- Moawad, A. W., Fuentes, D., Morshid, A., Khalaf, A. M., Elmohr, M. M., Abusaif, A., Hazle, J. D., Kaseb, A. O., Hassan, M., Mahvash, A., Szklaruk, J., Qayyom, A., & Elsayes, K. (2021). Multimodality annotated HCC cases with and without advanced imaging segmentation [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.5FNA-0924
- Morshid, A., Elsayes, K. M., Khalaf, A. M., Elmohr, M. M., Yu, J., Kaseb, A. O., Hassan, M., Mahvash, A., Wang, Z., Hazle, J. D., & Fuentes, D. (2019). A Machine Learning Model to Predict Hepatocellular Carcinoma Response to Transcatheter Arterial Chemoembolization. Radiology: Artificial Intelligence, 1(5), e180021. https://doi.org/10.1148/ryai.2019180021
### 1.3.6.1.4.1.14519.5.2.1.1188.2803.137585363493444318569098508293
Ultrasound data of a variety of liver masses (B-mode-and-CEUS-Liver)
- Eisenbrey, J., Lyshchik, A., & Wessner, C. (2021). Ultrasound data of a variety of liver masses [Data set]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/TCIA.2021.v4z7-tc39
### 1.3.6.1.4.1.32722.99.99.62087908186665265759322018723889952421
NSCLC-Radiomics
- Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2019). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
- Aerts, H. J. W. L., Velazquez, E. R., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014, June 3). Decoding tumour phenotype by noninvasive imaging using a quantitative radiomics approach. Nature Communications. Nature Publishing Group. https://doi.org/10.1038/ncomms5006 (link)
### 1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520
QIN-PROSTATE-Repeatability
- Fedorov, A; Schwier, M; Clunie, D; Herz, C; Pieper, S; Kikinis, R; Tempany, C; Fennessy, F. (2018). Data From QIN-PROSTATE-Repeatability. The Cancer Imaging Archive. DOI: 10.7937/K9/TCIA.2018.MR1CKGND
- Fedorov A, Vangel MG, Tempany CM, Fennessy FM. Multiparametric Magnetic Resonance Imaging of the Prostate: Repeatability of Volume and Apparent Diffusion Coefficient Quantification. Investigative Radiology. 52, 538546 (2017). DOI: 10.1097/RLI.0000000000000382
- Fedorov, A., Schwier, M., Clunie, D., Herz, C., Pieper, S., Kikinis,R., Tempany, C. & Fennessy, F. An annotated test-retest collection of prostate multiparametric MRI. Scientific Data 5, 180281 (2018). DOI:
### 2.25.141277760791347900862109212450152067508
The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC)
- National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2018). The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC) (Version 10) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.OBLAMN27
- The CPTAC program requests that publications using data from this program include the following statement: “Data used in this publication were generated by the National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC).”
### 2.25.275741864483510678566144889372061815320
National Lung Screening Trial
- National Lung Screening Trial Research Team. (2013). Data from the National Lung Screening Trial (NLST) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.HMQ8-J677
- National Lung Screening Trial Research Team*; Aberle DR, Adams AM, Berg CD, Black WC, Clapp JD, Fagerstrom RM, Gareen IF, Gatsonis C, Marcus PM, Sicks JD (2011). Reduced Lung-Cancer Mortality with Low-Dose Computed Tomographic Screening. New England Journal of Medicine, 365(5), 395409. https://doi.org/10.1056/nejmoa1102873
### 1.3.6.1.4.1.14519.5.2.1.99.1071.26968527900428638961173806140069
Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
- Saltz, J., Saltz, M., Prasanna, P., Moffitt, R., Hajagos, J., Bremer, E., Balsamo, J., & Kurc, T. (2021). Stony Brook University COVID-19 Positive Cases [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.BBAG-2923
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
https://data.kitware.com/
### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772
### 2.25.269859997690759739055099378767846712697
### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1
### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1
### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1
https://github.com/ImagingInformatics/hackathon-images
### 2.16.124.113543.6004.101.103.20021117.162333.1
### 2.16.124.113543.6004.101.103.20021117.190619.1
### 2.16.124.113543.6004.101.103.20021117.123455.1
### 2.16.124.113543.6004.101.103.20021117.061159.1
https://www.aapm.org/
### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886
### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612
Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor.
### 1.3.6.1.4.1.9328.50.1.54652
https://www.cancerimagingarchive.net/collection/rider-pilot/
Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83
### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986
Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59
https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/
### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472
Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22
https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681
Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox
https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/
### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556
Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66
https://www.cancerimagingarchive.net/collection/cmb-aml/
### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577
https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/
Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip
### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384
https://www.cancerimagingarchive.net/collection/cmb-pca/
Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12
### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331
Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772
https://www.cancerimagingarchive.net/collection/cptac-ucec/
National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW
### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478
https://www.cancerimagingarchive.net/collection/remind/
Juvekar, P., Dorent, R., Kögl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070
### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010
Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux
https://www.cancerimagingarchive.net/collection/phantom-fda/
### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310
https://www.cancerimagingarchive.net/collection/4d-lung/
Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE
### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036
https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/
Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm
### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392
https://www.cancerimagingarchive.net/collection/prostate-mri/
Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv
### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844
https://www.cancerimagingarchive.net/collection/upenn-gbm/
Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., … Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481
https://www.cancerimagingarchive.net/collection/breast-diagnosis/
Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
https://www.cancerimagingarchive.net/collection/ea1141/
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
From OFFIS DICOM-Team
https://www.offis.de/
OFFIS DICOM-Team

View File

@ -1,89 +0,0 @@
# syntax=docker/dockerfile:1.7-labs
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
#
# It's a good example of how to build our static application and package it
# with a web server capable of hosting it as static content.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile has two stages:
#
# 1. Building the React application for production
# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output
#
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
# Copy Files
FROM node:24.15.0-slim as builder
RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm@11
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
# Copy package manifests for install caching. preinstall.js is included because
# the root package.json's "preinstall" lifecycle script (node preinstall.js)
# runs during `pnpm install` below -- before the full source is copied -- so the
# script file must already be present or install fails with MODULE_NOT_FOUND.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc preinstall.js ./
COPY --parents ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
# Run the install before copying the rest of the files.
# Keep --no-frozen-lockfile here (unlike CI): .dockerignore excludes
# platform/docs, so the lockfile's docs importer has no manifest in the build
# context and a frozen install would fail. pnpm reconciles (drops docs) instead.
RUN pnpm install --no-frozen-lockfile
# Copy the local directory
COPY --link --exclude=pnpm-lock.yaml --exclude=package.json --exclude=Dockerfile . .
# Build here
# After install it should hopefully be stable until the local directory changes
ENV QUICK_BUILD true
# ENV GENERATE_SOURCEMAP=false
ARG APP_CONFIG=config/default.js
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
RUN pnpm run show:config
RUN pnpm run build
# Precompress files
RUN chmod u+x .docker/compressDist.sh
RUN ./.docker/compressDist.sh
# Stage 2: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.27-alpine as final
#RUN apk add --no-cache bash
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
ARG PORT=80
ENV PORT=${PORT}
RUN rm /etc/nginx/conf.d/default.conf
USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html${PUBLIC_URL}
# Copy paths that are renamed/redirected generally
# Microscopy libraries depend on root level include, so must be copied
COPY --from=builder /usr/src/app/platform/app/dist/dicom-microscopy-viewer /usr/share/nginx/html/dicom-microscopy-viewer
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
RUN chown -R nginx:nginx /usr/share/nginx/html && chmod -R 777 /usr/share/nginx/html
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,6 +1,6 @@
MIT License The MIT License (MIT)
Copyright (c) 2018 Open Health Imaging Foundation Copyright (c) 2015 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

488
README.md
View File

@ -1,338 +1,211 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<div align="center"> <div align="center">
<h1>OHIF Medical Imaging Viewer</h1> <h1>ohif-viewer</h1>
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer <p><strong>ohif-viewer</strong> is a zero-footprint medical image viewer provided by the <a href="http://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/using/dicomweb/">DICOMweb</a>.</p>
</div> </div>
<div align="center"> <div align="center">
<a href="https://docs.ohif.org/"><strong>Read The Docs</strong></a> <a href="https://docs.ohif.org/"><strong>Read The Docs</strong></a> |
<a href="https://github.com/OHIF/Viewers/tree/master/docs/latest">Edit the docs</a>
</div> </div>
<div align="center"> <div align="center">
<a href="https://viewer.ohif.org/">Live Demo</a> | <a href="https://docs.ohif.org/demo">Demo</a> |
<a href="https://ui.ohif.org/">Component Library</a> <a href="https://ohif.canny.io/">Roadmap</a> |
<a href="https://react.ohif.org/">Component Library</a>
</div> </div>
<div align="center">
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
</div>
<div align="center">
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
</div>
<hr /> <hr />
[![NPM version][npm-version-image]][npm-url] [![CircleCI][circleci-image]][circleci-url]
[![MIT License][license-image]][license-url] [![codecov][codecov-image]][codecov-url]
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url) [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors)
<!-- [![NPM downloads][npm-downloads-image]][npm-url] --> [![code style: prettier][prettier-image]][prettier-url]
<!-- [![Pulls][docker-pulls-img]][docker-image-url] --> [![semantic-release][semantic-image]][semantic-url]
<!-- [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_shield) -->
<!-- [![Netlify Status][netlify-image]][netlify-url] --> [![NPM version][npm-version-image]][npm-url]
<!-- [![CircleCI][circleci-image]][circleci-url] --> [![NPM downloads][npm-downloads-image]][npm-url]
<!-- [![codecov][codecov-image]][codecov-url] --> [![Pulls][docker-pulls-img]][docker-image-url]
<!-- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors) --> [![MIT License][license-image]][license-url]
<!-- markdownlint-enable -->
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
> ATTENTION: If you are looking for Version 1 (the Meteor Version) of this
> repository, it lives on
> [the `v1.x` branch](https://github.com/OHIF/Viewers/tree/v1.x)
| | | | ## Why?
| :-: | :--- | :--- |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-measurements.webp?raw=true" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-segmentation.webp?raw=true" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ptct.webp?raw=true" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ecg.webp?raw=true" alt="ECG" width="350"/> | ECG Waveform | [Demo](https://viewer-dev.ohif.org/viewer?StudyInstanceUIDs=2.25.209974489360710696739324151261716440238) |
## About Building a web based medical imaging viewer from scratch is time intensive, hard
to get right, and expensive. Instead of re-inventing the wheel, you can use the
OHIF Viewer as a rock solid platform to build on top of. The Viewer is a
[React][react-url] [Progressive Web Application][pwa-url] that can be embedded
in existing applications via it's [packaged source
(ohif-viewer)][ohif-viewer-url] or hosted stand-alone. The Viewer exposes
[configuration][configuration-url] and [extensions][extensions-url] to support
workflow customization and advanced functionality at common integration points.
The OHIF Viewer can retrieve If you're interested in using the OHIF Viewer, but you're not sure it supports
and load images from most sources and formats, render sets in 2D, 3D, and your use case [check out our docs](https://docs.ohif.org/). Still not sure, or
reconstructed representations; allows for the manipulation, annotation, and you would like to propose new features? Don't hesitate to
serialization of observations; supports internationalization, OpenID Connect, [create an issue](https://github.com/OHIF/Viewers/issues) or open a pull
offline use, hotkeys, and many more features. request.
Almost everything offers some degree of customization and configuration. If it ## Getting Started
doesn't support something you need, we accept pull requests and have an ever
improving Extension System.
## Why Choose Us This readme is specific to testing and developing locally. If you're more
interested in production deployment strategies,
[you can check out our documentation on publishing](https://docs.ohif.org/).
### Community & Experience Want to play around before you dig in?
[Check out our LIVE Demo](https://viewer.ohif.org/)
The OHIF Viewer is a collaborative effort that has served as the basis for many ### Setup
active, production, and FDA Cleared medical imaging viewers. It benefits from
our extensive community's collective experience, and from the sponsored
contributions of individuals, research groups, and commercial organizations.
### Built to Adapt _Requirements:_
After more than 8-years of integrating with many companies and organizations, - [NodeJS & NPM](https://nodejs.org/en/download/)
The OHIF Viewer has been rebuilt from the ground up to better address the - [Yarn](https://yarnpkg.com/lang/en/docs/install/)
varying workflow and configuration needs of its many users. All of the Viewer's
core features are built using its own extension system. The same extensibility
that allows us to offer:
- 2D and 3D medical image viewing _Steps:_
- Multiplanar Reconstruction (MPR)
- Maximum Intensity Project (MIP)
- Whole slide microscopy viewing
- PDF and Dicom Structured Report rendering
- Segmentation rendering as labelmaps and contours
- User Access Control (UAC)
- Context specific toolbar and side panel content
- and many others
Can be leveraged by you to customize the viewer for your workflow, and to add 1. Fork this repository
any new functionality you may need (and wish to maintain privately without 2. Clone your forked repository (your `origin`)
forking).
### Support - `git clone git@github.com:YOUR_GITHUB_USERNAME/Viewers.git`
- [Report a Bug 🐛](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Report+%3Abug%3A%2CAwaiting+Reproduction&projects=&template=bug-report.yml&title=%5BBug%5D+) 3. Add `OHIF/Viewers` as a `remote` repository (the `upstream`)
- [Request a Feature 🚀](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Request+%3Ahand%3A&projects=&template=feature-request.yml&title=%5BFeature+Request%5D+)
- [Ask a Question 🤗](community.ohif.org)
- [Slack Channel](https://join.slack.com/t/cornerstonejs/shared_invite/zt-1r8xb2zau-dOxlD6jit3TN0Uwf928w9Q)
For commercial support, academic collaborations, and answers to common - `git remote add upstream git@github.com:OHIF/Viewers.git`
questions; please use [Get Support](https://ohif.org/get-support/) to contact
us.
## Developing ### Developing Locally
### Branches In your cloned repository's root folder, run:
#### `master` branch - The latest dev (beta) release ```js
// Restore dependencies
yarn install
- `master` - The latest dev release // Stands up local server to host Viewer.
// Viewer connects to our public cloud PACS by default
This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). yarn start
Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1`
### `release/*` branches - The latest stable releases
Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production.
For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch.
Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases.
Here is a schematic representation of our development workflow:
![alt text](platform/docs/docs/assets/img/github-readme-branches-Jun2024.png)
### Requirements
- [Yarn 1.20.0+](https://yarnpkg.com/en/docs/install)
- [Node 18+](https://nodejs.org/en/)
- Yarn Workspaces should be enabled on your machine:
- `yarn config set workspaces-experimental true`
### Getting Started
1. [Fork this repository][how-to-fork]
2. [Clone your forked repository][how-to-clone]
- `git clone https://github.com/YOUR-USERNAME/Viewers.git`
3. Navigate to the cloned project's directory
4. Add this repo as a `remote` named `upstream`
- `git remote add upstream https://github.com/OHIF/Viewers.git`
5. `yarn install --frozen-lockfile` to restore dependencies and link projects
:::danger
In general run `yarn install` with the `--frozen-lockfile` flag to help avoid
supply chain attacks by enforcing reproducible dependencies. That is, if the
`yarn.lock` file is clean and does NOT reference compromised packages, then
no compromised packages should land on your machine by using this flag.
:::
#### To Develop
_From this repository's root directory:_
```bash
# Enable Yarn Workspaces
yarn config set workspaces-experimental true
# Restore dependencies
yarn install --frozen-lockfile
``` ```
### Cornerstone3D Integration Testing For more advanced local development scenarios, like using your own locally
hosted PACS and test data,
[check out our Essential: Getting Started](https://docs.ohif.org/essentials/getting-started.html)
guide.
OHIF's Playwright end-to-end tests can run against a **CS3D branch** or a ### Contributing
**published CS3D version**, allowing changes that span both repositories to be
validated together before merging.
#### Setting up an integration build > Large portions of the Viewer's functionality are maintained in other
> repositories. To get a better understanding of the Viewer's architecture and
> "where things live", read
> [our docs on the Viewer's architecture](https://docs.ohif.org/advanced/architecture.html#diagram)
1. Add the **`ohif-integration`** label to your OHIF pull request. It is notoriously difficult to setup multiple dependent repositories for
2. In the PR body, add a line specifying the CS3D ref: end-to-end testing and development. That's why we recommend writing and running
unit tests when adding and modifying features. This allows us to program in
isolation without a complex setup, and has the added benefit of producing
well-tested business logic.
1. Clone this repository
2. Navigate to the project directory, and `yarn install`
3. To begin making changes, `yarn run dev`
4. To commit changes, run `yarn run cm`
When creating tests, place the test file "next to" the file you're testing.
[For example](https://github.com/OHIF/Viewers/blob/master/src/utils/index.test.js):
```js
// File
index.js
// Test for file
index.test.js
``` ```
CS3D_REF: feat/my-feature
```
- **Version ref** (e.g. `4.19+`, `4.18.2`) — the workflow resolves it to an
exact published version and swaps the CS3D dependency via npm.
- **Branch ref** (e.g. `main`, `cornerstonejs:feat/foo`) — the workflow
clones the branch, builds CS3D from source with `bun run build:esm`, and
symlinks the built packages into OHIF's `node_modules`.
- For forks, use the `<owner>:<branch>` format
(e.g. `myGithubUser:feat/foo`).
- If no `CS3D_REF` is specified, the default is `4.19+`.
3. The workflow can also be triggered manually via **workflow_dispatch** with a
`cs3d_ref` input.
#### What happens in CI As you add and modify code, `jest` will watch for uncommitted changes and run
your tests, reporting the results to your terminal. Make a pull request with
your changes to `master`, and a core team member will review your work. If you
have any questions, please don't hesitate to reach out via a GitHub issue.
The [Playwright workflow](.github/workflows/playwright.yml) runs two jobs: ## Issues
| Job | Purpose | _Looking to contribute? Look for the [Good First Issue][good-first-issue]
|-----|---------| label._
| **Playwright Tests** | Builds OHIF (with CS3D linked or version-swapped), runs the full Playwright suite, uploads test results and coverage, and deploys a Netlify preview when `ohif-integration` is active. |
| **CS3D Branch Merge Guard** | A lightweight check that **fails** when the `ohif-integration` label is present and `CS3D_REF` points to a branch (not a version). This prevents merging while still letting the Playwright tests show green so you can see whether the code actually works. |
#### Testing changes that span both repos ### 🐛 Bugs
If a feature requires changes in both Cornerstone3D and OHIF: Please file an issue for bugs, missing documentation, or unexpected behavior.
1. Create your feature branch in CS3D and push it. [**See Bugs**][bugs]
2. Create a matching branch in OHIF.
3. Add the `ohif-integration` label to the OHIF pull request.
4. In the PR body, add: `CS3D_REF: <your-cs3d-branch>`.
5. Playwright tests will build CS3D from source, link it, and run the full
suite. The merge guard will block merge until you switch to a published
version — but you can see the test results and the preview deploy while
iterating.
6. Once the CS3D side is merged and published, update the PR body to reference
the published version (e.g. `CS3D_REF: 4.19+`). The tests will run against
the registry version and the merge guard will pass.
#### Preview deploys ### 💡 Feature Requests
When `ohif-integration` is active, the Playwright workflow also builds the OHIF Please file an issue to suggest new features. Vote on feature requests by adding
viewer and deploys it to Netlify as a preview. This gives you a live URL to a 👍. This helps maintainers prioritize what to work on.
manually test the combined CS3D + OHIF changes without running anything locally.
For details on linking CS3D locally for development, see the [**See Feature Requests**][requests-feature]
[Cornerstone3D README](libs/@cornerstonejs/README.md#local-development-linking--unlinking).
## Commands ### ❓ Questions
These commands are available from the root directory. Each project directory For questions related to using the library, please visit our support community,
also supports a number of commands that can be found in their respective or file an issue on GitHub.
`README.md` and `package.json` files.
| Yarn Commands | Description | [Google Group][google-group]
| ---------------------------- | ------------------------------------------------------------- |
| **Develop** | |
| `dev` | Default development experience for Viewer |
| `dev:fast` | Our experimental fast dev mode that uses rsbuild instead of webpack |
| `test:unit` | Jest multi-project test runner; overall coverage |
| **Deploy** | |
| `build`\* | Builds production output for our PWA Viewer | |
\* - For more information on different builds, check out our [Deploy ## Roadmap
Docs][deployment-docs]
### Which config each command uses If you want to know what's planned for the very near future,
[check out our roadmap](https://ohif.canny.io/). The best way to influence when
and what is worked on is to contribute to the conversation by creating GitHub
issues, and contributing code through pull requests. OHIF's high level
priorities for the near future are:
The dev server and the production build select a different default - Feature parity with version 1
[configuration file][config-file] when `APP_CONFIG` is not set explicitly: - Extension and configuration improvements with key integration partners
- Continued Developer Experience Improvements
- Segmentation Tools, and improved VTK.js support
| Command | Default config | Data sources | `?customization=` | More granular information will make it's way to the backlog as these items
| ------------------------------- | -------------------- | ------------ | ----------------- | become scoped for development by core maintainers.
| `dev`, `dev:fast`, `start` | `config/dev.js` | Full set | Enabled |
| `build` | `config/default.js` | One demo source | Disabled |
- **`config/dev.js`** is the full-featured local-development config: every data > Don't hesitate to ask questions, propose features, or create pull requests.
source is enabled, the `?customization=` URL feature is turned on (via > We're here, we're listening, and we're ready to build the best open source
`customizationUrlPrefixes`), and it is kept at parity with the public demo > medical imaging viewer on the web.
(`config/netlify.js`) so customizations behave locally the same way they do on
the demo.
- **`config/netlify.js`** is the public demo / Netlify deploy config
(`build:viewer:ci`), with the same full data-source set and `?customization=`
enabled.
- **`config/default.js`** is a locked-down baseline and is now **only** the
default for a plain production build (`build` with no `APP_CONFIG`): a single
read-only demo data source and `?customization=` off.
Any explicit `APP_CONFIG` overrides the default, e.g. #### Roadmap Generously Powered by Canny.io
`APP_CONFIG=config/default.js pnpm run dev` or
`APP_CONFIG=config/netlify.js pnpm run build`.
## Project <a href="https://ohif.canny.io/">
<img height="30" src="docs/latest/assets/img/canny-full.png" />
</a>
The OHIF Medical Image Viewing Platform is maintained as a ## Contributors
[`monorepo`][monorepo]. This means that this repository, instead of containing a
single project, contains many projects. If you explore our project structure,
you'll see the following:
```bash Thanks goes to these wonderful people
. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
├── extensions #
│ ├── _example # Skeleton of example extension
│ ├── default # basic set of useful functionalities (datasources, panels, etc)
│ ├── cornerstone # image rendering and tools w/ Cornerstone3D
│ ├── cornerstone-dicom-sr # DICOM Structured Report rendering and export
│ ├── cornerstone-dicom-sr # DICOM Structured Report rendering and export
│ ├── cornerstone-dicom-seg # DICOM Segmentation rendering and export
│ ├── cornerstone-dicom-rt # DICOM RTSTRUCT rendering
│ ├── cornerstone-microscopy # Whole Slide Microscopy rendering
│ ├── dicom-pdf # PDF rendering
│ ├── dicom-video # DICOM RESTful Services
│ ├── measurement-tracking # Longitudinal measurement tracking
│ ├── tmtv # Total Metabolic Tumor Volume (TMTV) calculation
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
├── modes # <!-- prettier-ignore -->
│ ├── _example # Skeleton of example mode <table><tr><td align="center"><a href="https://github.com/swederik"><img src="https://avatars3.githubusercontent.com/u/607793?v=4" width="100px;" alt="Erik Ziegler"/><br /><sub><b>Erik Ziegler</b></sub></a><br /><a href="https://github.com/OHIF/Viewers/commits?author=swederik" title="Code">💻</a></td><td align="center"><a href="https://github.com/evren217"><img src="https://avatars1.githubusercontent.com/u/4920551?v=4" width="100px;" alt="Evren Ozkan"/><br /><sub><b>Evren Ozkan</b></sub></a><br /><a href="https://github.com/OHIF/Viewers/commits?author=evren217" title="Code">💻</a></td><td align="center"><a href="https://github.com/galelis"><img src="https://avatars3.githubusercontent.com/u/2378326?v=4" width="100px;" alt="Gustavo André Lelis"/><br /><sub><b>Gustavo André Lelis</b></sub></a><br /><a href="https://github.com/OHIF/Viewers/commits?author=galelis" title="Code">💻</a></td><td align="center"><a href="http://dannyrb.com/"><img src="https://avatars1.githubusercontent.com/u/5797588?v=4" width="100px;" alt="Danny Brown"/><br /><sub><b>Danny Brown</b></sub></a><br /><a href="https://github.com/OHIF/Viewers/commits?author=dannyrb" title="Code">💻</a></td><td align="center"><a href="https://github.com/all-contributors/all-contributors-bot"><img src="https://avatars3.githubusercontent.com/u/46843839?v=4" width="100px;" alt="allcontributors[bot]"/><br /><sub><b>allcontributors[bot]</b></sub></a><br /><a href="https://github.com/OHIF/Viewers/commits?author=allcontributors" title="Documentation">📖</a></td><td align="center"><a href="https://www.linkedin.com/in/siliconvalleynextgeneration/"><img src="https://avatars0.githubusercontent.com/u/1230575?v=4" width="100px;" alt="Esref Durna"/><br /><sub><b>Esref Durna</b></sub></a><br /><a href="#question-EsrefDurna" title="Answering Questions">💬</a></td></tr></table>
│ ├── basic-dev-mode # Basic development mode
│ ├── longitudinal # Longitudinal mode (measurement tracking) <!-- ALL-CONTRIBUTORS-LIST:END -->
│ ├── tmtv # Total Metabolic Tumor Volume (TMTV) calculation mode
│ └── microscopy # Whole Slide Microscopy mode This project follows the
[all-contributors](https://github.com/all-contributors/all-contributors)
├── platform # specification. Contributions of any kind welcome!
│ ├── core # Business Logic
│ ├── i18n # Internationalization Support ## License
│ ├── ui # React component library
│ ├── docs # Documentation MIT © [OHIF](https://github.com/OHIF)
│ └── viewer # Connects platform and extension projects
├── ... # misc. shared configuration
├── lerna.json # MonoRepo (Lerna) settings
├── package.json # Shared devDependencies and commands
└── README.md # This file
```
## Acknowledgments ## Acknowledgments
To acknowledge the OHIF Viewer in an academic publication, please cite To acknowledge the OHIF Viewer in an academic publication, please cite
> _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework
> for Building Web-Based Imaging Applications to Support Cancer Research_
>
> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob
> Lewis, Chris Hafey, and Gordon J. Harris
>
> _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI:
> [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131)
>
> Open-Access on Pubmed Central:
> https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/
or, for v1, please cite:
> _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer > _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer
> Imaging Research and Clinical Trials_ > Imaging Research and Clinical Trials_
> >
@ -343,7 +216,7 @@ or, for v1, please cite:
> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) > [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334)
**Note:** If you use or find this repository helpful, please take the time to **Note:** If you use or find this repository helpful, please take the time to
star this repository on GitHub. This is an easy way for us to assess adoption star this repository on Github. This is an easy way for us to assess adoption
and it can help us obtain future funding for the project. and it can help us obtain future funding for the project.
This work is supported primarily by the National Institutes of Health, National This work is supported primarily by the National Institutes of Health, National
@ -351,78 +224,41 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
under a under a
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104). [grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority),
["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from
Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer.
This project is tested with BrowserStack. Thank you for supporting open-source!
## License
MIT © [OHIF](https://github.com/OHIF)
<!-- <!--
Links Links:
--> -->
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- Badges --> <!-- ROW -->
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
[lerna-url]: https://lerna.js.org/
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square [all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
[contributing-url]: https://github.com/OHIF/Viewers/blob/react/CONTRIBUTING.md
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg [circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
[circleci-url]: https://circleci.com/gh/OHIF/Viewers [circleci-url]: https://circleci.com/gh/OHIF/Viewers
[codecov-image]: https://codecov.io/gh/OHIF/Viewers/branch/master/graph/badge.svg [codecov-image]: https://codecov.io/gh/OHIF/Viewers/branch/react/graph/badge.svg
[codecov-url]: https://codecov.io/gh/OHIF/Viewers/branch/master [codecov-url]: https://codecov.io/gh/OHIF/Viewers/branch/react
[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square [prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square
[prettier-url]: https://github.com/prettier/prettier [prettier-url]: https://github.com/prettier/prettier
[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[semantic-url]: https://github.com/semantic-release/semantic-release [semantic-url]: https://github.com/semantic-release/semantic-release
<!-- ROW --> <!-- ROW -->
[npm-url]: https://npmjs.org/package/@ohif/app [npm-url]: https://npmjs.org/package/ohif-viewer
[npm-downloads-image]: https://img.shields.io/npm/dm/@ohif/app.svg?style=flat-square [npm-downloads-image]: https://img.shields.io/npm/dm/ohif-viewer.svg?style=flat-square
[npm-version-image]: https://img.shields.io/npm/v/@ohif/app.svg?style=flat-square [npm-version-image]: https://img.shields.io/npm/v/ohif-viewer.svg?style=flat-square
[docker-pulls-img]: https://img.shields.io/docker/pulls/ohif/viewer.svg?style=flat-square [docker-pulls-img]: https://img.shields.io/docker/pulls/ohif/viewer.svg?style=flat-square
[docker-image-url]: https://hub.docker.com/r/ohif/app [docker-image-url]: https://hub.docker.com/r/ohif/viewer
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
[license-url]: LICENSE [license-url]: LICENSE
[percy-image]: https://percy.io/static/images/percy-badge.svg <!-- DOCS -->
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
<!-- Links -->
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
[ohif-architecture]: https://docs.ohif.org/architecture/index.html
[ohif-extensions]: https://docs.ohif.org/architecture/index.html
[deployment-docs]: https://docs.ohif.org/deployment/
[config-file]: https://docs.ohif.org/configuration/configurationFiles
[react-url]: https://reactjs.org/ [react-url]: https://reactjs.org/
[pwa-url]: https://developers.google.com/web/progressive-web-apps/ [pwa-url]: https://developers.google.com/web/progressive-web-apps/
[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app [ohif-viewer-url]: https://www.npmjs.com/package/ohif-viewer
[configuration-url]: https://docs.ohif.org/configuring/ [configuration-url]: https://docs.ohif.org/essentials/configuration.html
[extensions-url]: https://docs.ohif.org/extensions/ [extensions-url]: https://docs.ohif.org/advanced/extensions.html
<!-- Platform --> <!-- Misc. -->
[platform-core]: platform/core/README.md [react-viewer]: https://github.com/OHIF/Viewers/tree/react
[core-npm]: https://www.npmjs.com/package/@ohif/core <!-- Issue Boilerplate -->
[platform-i18n]: platform/i18n/README.md [bugs]: https://github.com/OHIF/Viewers/labels/bug
[i18n-npm]: https://www.npmjs.com/package/@ohif/i18n [requests-feature]: https://github.com/OHIF/Viewers/labels/enhancement
[platform-ui]: platform/ui/README.md [good-first-issue]: https://github.com/OHIF/Viewers/labels/good%20first%20issue
[ui-npm]: https://www.npmjs.com/package/@ohif/ui [google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform
[platform-viewer]: platform/app/README.md
[viewer-npm]: https://www.npmjs.com/package/@ohif/app
<!-- Extensions -->
[extension-cornerstone]: extensions/cornerstone/README.md
[cornerstone-npm]: https://www.npmjs.com/package/@ohif/extension-cornerstone
[extension-dicom-html]: extensions/dicom-html/README.md
[html-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-html
[extension-dicom-microscopy]: extensions/dicom-microscopy/README.md
[microscopy-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-microscopy
[extension-dicom-pdf]: extensions/dicom-pdf/README.md
[pdf-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-pdf
[extension-vtk]: extensions/vtk/README.md
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license)

View File

@ -1,8 +0,0 @@
/* Used by webpack, babel and eslint */
const path = require('path');
module.exports = {
'@codinsky/parse-js': path.resolve(__dirname, 'packages/parse/src'),
'@codinsky/curate': path.resolve(__dirname, 'packages/curate/src'),
};

View File

@ -1,62 +0,0 @@
// https://babeljs.io/docs/en/options#babelrcroots
module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
plugins: [
['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-transform-private-property-in-object', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
'@babel/plugin-transform-class-static-block',
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
targets: { node: 'current' },
bugfixes: true,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
// jest's babel coverage provider injects babel-plugin-istanbul when
// --collectCoverage is set; adding it here too makes babel 7 (pulled in
// by jest 30) throw "Duplicate plugin/preset detected".
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-class-static-block',
'@babel/plugin-transform-for-of',
['babel-plugin-transform-import-meta', { module: 'ES6' }],
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@ -1 +0,0 @@
6dd150d401ad73d60632a23378b7dfd4b5142690

View File

@ -0,0 +1,77 @@
version: '3.5'
services:
ldap:
image: dcm4che/slapd-dcm4chee:2.4.44-15.0
logging:
driver: json-file
options:
max-size: '10m'
ports:
- '389:389'
env_file: ./dcm4che/docker-compose-dcm4che.env
volumes:
- ./dcm4che/etc/localtime:/etc/localtime:ro
- ./dcm4che/etc/timezone:/etc/timezone:ro
- ./dcm4che/dcm4che-arc/ldap:/var/lib/ldap
- ./dcm4che/dcm4che-arc/slapd.d:/etc/ldap/slapd.d
networks:
- dcm4che_default
db:
image: dcm4che/postgres-dcm4chee:11.1-15
logging:
driver: json-file
options:
max-size: '10m'
ports:
- '5432:5432'
env_file: ./dcm4che/docker-compose-dcm4che.env
volumes:
- ./dcm4che/etc/localtime:/etc/localtime:ro
- ./dcm4che/etc/timezone:/etc/timezone:ro
- ./dcm4che/dcm4che-arc/db:/var/lib/postgresql/data
networks:
- dcm4che_default
arc:
image: dcm4che/dcm4chee-arc-psql:5.15.0
logging:
driver: json-file
options:
max-size: '10m'
ports:
- '8080:8080'
- '8443:8443'
- '9990:9990'
- '11112:11112'
- '2575:2575'
env_file: ./dcm4che/docker-compose-dcm4che.env
environment:
WILDFLY_CHOWN: /opt/wildfly/standalone /storage
WILDFLY_WAIT_FOR: ldap:389 db:5432
depends_on:
- ldap
- db
volumes:
- ./dcm4che/etc/localtime:/etc/localtime:ro
- ./dcm4che/etc/timezone:/etc/timezone:ro
- ./dcm4che/dcm4che-arc/wildfly:/opt/wildfly/standalone
- ./dcm4che/dcm4che-arc/storage:/storage
networks:
- dcm4che_default
viewer:
container_name: ohif-viewer
build:
context: ../
dockerfile: Dockerfile
ports:
- '80:80'
# depends_on:
# - orthanc
environment:
- NODE_ENV=production
- REACT_APP_CONFIG=config/local_dcm4chee
restart: always
networks:
- dcm4che_default
networks: dcm4che_default:

View File

@ -0,0 +1,49 @@
events {
worker_connections 4096; ## Default: 1024
}
http {
server {
listen 80 default_server;
server_name localhost;
#
# Wide-open CORS config for nginx
#
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Allow-Headers' 'Authorization';
add_header 'Access-Control-Allow-Credentials' true;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header 'Access-Control-Allow-Headers' 'Authorization';
add_header 'Access-Control-Allow-Credentials' true;
}
proxy_pass http://orthanc:8042;
}
}
}

View File

@ -0,0 +1,48 @@
worker_processes 1;
events { worker_connections 1024; }
http {
upstream orthanc-server {
server orthanc:8042;
}
server {
listen [::]:80 default_server;
listen 80;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://orthanc:8042;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
# CORS Magic
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow_Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
}
}
}

View File

@ -0,0 +1,26 @@
version: '3.5'
services:
proxy:
image: nginx:1.15-alpine
ports:
- 8899:80
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
# LINK: https://hub.docker.com/r/jodogne/orthanc-plugins/
# TODO: Update to use Postgres
# https://github.com/mrts/docker-postgresql-multiple-databases
orthanc:
image: jodogne/orthanc-plugins:1.5.6
hostname: orthanc
volumes:
# Config
- ./config/orthanc.json:/etc/orthanc/orthanc.json:ro
# Persist data
- ./volumes/orthanc-db/:/var/lib/orthanc/db/
# ports:
# - '4242:4242' # DICOM
# - '8042:8042' # Web
restart: unless-stopped

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,16 @@
# Output
dist/
# Dependencies
node_modules/
# Root
README.md
Dockerfile
# Misc. Config
.git
.DS_Store
.gitignore
.vscode
.circleci

View File

@ -0,0 +1,5 @@
# Docker ENV and ARG Variables
# ----------------------------
# https://vsupalov.com/docker-arg-env-variable-guide/
#
#

View File

@ -0,0 +1,259 @@
worker_processes 2;
error_log /var/logs/nginx/mydomain.error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf; # See /usr/share/doc/nginx/README.dynamic.
events {
worker_connections 1024; ## Default: 1024
use epoll; # http://nginx.org/en/docs/events.html
multi_accept on; # http://nginx.org/en/docs/ngx_core_module.html#multi_accept
}
# Core Modules Docs:
# http://nginx.org/en/docs/http/ngx_http_core_module.html
http {
include '/usr/local/openresty/nginx/conf/mime.types';
default_type application/octet-stream;
keepalive_timeout 65;
keepalive_requests 100000;
tcp_nopush on;
tcp_nodelay on;
# lua_ settings
#
lua_package_path '/usr/local/openresty/lualib/?.lua;;';
lua_shared_dict discovery 1m; # cache for discovery metadata documents
lua_shared_dict jwks 1m; # cache for JWKs
# lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
variables_hash_max_size 2048;
server_names_hash_bucket_size 128;
server_tokens off;
resolver 8.8.8.8 valid=30s ipv6=off;
resolver_timeout 11s;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# No idea what this is doing
# https://stackoverflow.com/a/5877989/1867984
# upstream upstream_server {
# # server 10.100.4.200:1010 max_fails=3 fail_timeout=30s;
# server 127.0.0.1:
# }
# Nginx `listener` block
server {
listen [::]:80 default_server;
listen 80;
# listen 443 ssl;
access_log /var/logs/nginx/mydomain.access.log;
# Domain to protect
server_name 127.0.0.1 localhost; # mydomain.com;
proxy_intercept_errors off;
# ssl_certificate /etc/letsencrypt/live/mydomain.co.uk/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/mydomain.co.uk/privkey.pem;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_comp_level 9;
etag on;
# https://github.com/bungle/lua-resty-session/issues/15
set $session_check_ssi off;
lua_code_cache off;
set $session_secret Eeko7aeb6iu5Wohch9Loo1aitha0ahd1;
set $session_storage cookie;
server_tokens off; # Hides server version num
# [PROTECTED] Reverse Proxy for `orthanc` admin
#
location /pacs-admin/ {
access_by_lua_block {
local opts = {
redirect_uri = "http://127.0.0.1/pacs-admin/admin",
discovery = "http://127.0.0.1/auth/realms/ohif/.well-known/openid-configuration",
token_endpoint_auth_method = "client_secret_basic",
client_id = "pacs",
client_secret = "66279641-eba6-47f5-9fdb-70c4ac74d548",
client_jwt_assertion_expires_in = 60 * 60,
ssl_verify = "no",
scope = "openid email profile",
refresh_session_interval = 900,
redirect_uri_scheme = "http",
redirect_after_logout_uri = "/",
session_contents = {id_token=true}
}
-- call authenticate for OpenID Connect user authentication
local res, err = require("resty.openidc").authenticate(opts)
if err or not res then
ngx.print(err)
ngx.status = 200
ngx.say(err and err or "no access_token provided")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- Or set cookie?
-- ngx.req.set_header("Authorization", "Bearer " .. res.access_token)
ngx.req.set_header("X-USER", res.id_token.sub)
}
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
expires 0;
add_header Cache-Control private;
proxy_pass http://orthanc:8042/;
}
# [PROTECTED] Reverse Proxy for `orthanc` APIs (including DICOMWeb)
#
location /pacs/ {
access_by_lua_block {
local opts = {
discovery = "http://127.0.0.1/auth/realms/ohif/.well-known/openid-configuration",
}
-- call bearer_jwt_verify for OAuth 2.0 JWT validation
local res, err = require("resty.openidc").bearer_jwt_verify(opts)
if err or not res then
ngx.status = 403
ngx.say(err and err or "no access_token provided")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
expires 0;
add_header Cache-Control private;
proxy_pass http://orthanc:8042/;
# By default, this endpoint is protected by CORS (cross-origin-resource-sharing)
# You can add headers to allow other domains to request this resource.
# See the "Updating CORS Settings" example below
}
# Keycloak
#
location /auth/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://keycloak:8080/auth/;
}
# Do not cache sw.js, required for offline-first updates.
location /sw.js {
add_header Cache-Control "no-cache";
proxy_cache_bypass $http_pragma;
proxy_cache_revalidate on;
expires off;
access_log off;
}
# Single Page App
# Try files, fallback to index.html
#
location / {
alias /var/www/html/;
index index.html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# EXAMPLE: Reverse Proxy, no auth
# [UNPROTECTED] reverse proxy for `orthanc`
#
# location /pacs/ {
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $remote_addr;
# proxy_set_header Host $host;
#
# proxy_pass http://orthanc:8042/;
#
# # OR
# # rewrite ^/pacs(.*) /$1 break;
# # proxy_pass http://orthanc:8042;
# }
# EXAMPLE: Modifying headers to allow requests from other domains
# IE. Updating CORS settings
#
# location / {
# if ($request_method = 'OPTIONS') {
# add_header 'Access-Control-Allow-Origin' '*';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# #
# # Custom headers and headers various browsers *should* be OK with but aren't
# #
# add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
# #
# # Tell client that this pre-flight info is valid for 20 days
# #
# add_header 'Access-Control-Allow-Headers' 'Authorization';
# add_header 'Access-Control-Allow-Credentials' true;
# add_header 'Access-Control-Max-Age' 1728000;
# add_header 'Content-Length' 0;
# return 204;
# }
# if ($request_method = 'POST') {
# add_header 'Access-Control-Allow-Origin' '*';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
# add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
# }
# if ($request_method = 'GET') {
# add_header 'Access-Control-Allow-Origin' '*';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
# add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
# add_header 'Access-Control-Allow-Headers' 'Authorization';
# add_header 'Access-Control-Allow-Credentials' true;
# }
#
# # proxy_http_version 1.1;
#
# # proxy_set_header Host $host;
# # proxy_set_header X-Real-IP $remote_addr;
# # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# # proxy_set_header X-Forwarded-Proto $scheme;
#
# proxy_pass http://orthanc:8042;
# }
# EXAMPLE: Redirect server error pages to the static page /40x.html
#
# error_page 404 /404.html;
# location = /40x.html {
# }
# EXAMPLE: Redirect server error pages to the static page /50x.html
#
# error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
# Reference:
# - https://docs.docker.com/compose/compose-file
# - https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/
version: '3.5'
services:
# Exposed server that's handling incoming web requests
# Underlying image: openresty/openresty:alpine-fat
ohif_viewer:
build:
# Project root
context: ./../../
# Relative to context
dockerfile: ./docker/OpenResty-Orthanc-Keycloak/dockerfile
image: webapp:latest
container_name: webapp
volumes:
# Nginx config
- ./config/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
# Logs
- ./logs/nginx:/var/logs/nginx
# Let's Encrypt
# - letsencrypt_certificates:/etc/letsencrypt
# - letsencrypt_challenges:/var/www/letsencrypt
ports:
- '443:443' # SSL
- '80:80' # Web
depends_on:
- keycloak
- orthanc
restart: on-failure
# LINK: https://hub.docker.com/r/jodogne/orthanc-plugins/
# TODO: Update to use Postgres
# https://github.com/mrts/docker-postgresql-multiple-databases
orthanc:
image: jodogne/orthanc-plugins:1.5.6
hostname: orthanc
container_name: orthanc
volumes:
# Config
- ./config/orthanc.json:/etc/orthanc/orthanc.json:ro
# Persist data
- ./volumes/orthanc-db/:/var/lib/orthanc/db/
restart: unless-stopped
# LINK: https://hub.docker.com/r/jboss/keycloak
keycloak:
image: jboss/keycloak:6.0.1
hostname: keycloak
container_name: keycloak
volumes:
# Theme: https://www.keycloak.org/docs/latest/server_development/index.html#_themes
- ./volumes/keycloak-themes/ohif:/opt/jboss/keycloak/themes/ohif
# Previous Realm Config
- ./config/ohif-keycloak-realm.json:/tmp/ohif-keycloak-realm.json
environment:
# Database
DB_VENDOR: postgres
DB_ADDR: postgres
DB_PORT: 5432
DB_DATABASE: keycloak
DB_SCHEMA: public
DB_USER: keycloak
DB_PASSWORD: password
# Keycloak
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: password
KEYCLOAK_IMPORT: /tmp/ohif-keycloak-realm.json
# KEYCLOAK_WELCOME_THEME: <theme-name>
# KEYCLOAK_DEFAULT_THEME: <theme-name>
# KEYCLOAK_HOSTNAME: (recommended in prod)
# KEYCLOAK_LOGLEVEL: DEBUG
PROXY_ADDRESS_FORWARDING: 'true'
depends_on:
- postgres
restart: unless-stopped
# LINK: https://hub.docker.com/_/postgres/
postgres:
image: postgres:11.2
hostname: postgres
container_name: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
restart: unless-stopped
volumes:
postgres_data:
driver: local

View File

@ -0,0 +1,65 @@
# docker-compose
# --------------
# This dockerfile is used by the `docker-compose.yml` adjacent file. When
# running `docker-compose build`, this dockerfile helps build the "webapp" image.
# All paths are relative to the `context`, which is the project root directory.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile has two stages:
#
# 1. Building the React application for production
# 2. Setting up our Nginx (OpenResty*) image w/ step one's output
#
# * OpenResty is functionally identical to Nginx with the addition of Lua out of
# the box.
# Stage 1: Build the application
FROM node:11.2.0-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV REACT_APP_CONFIG=config/docker_openresty-orthanc-keycloak.js
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package.json /usr/src/app/package.json
COPY yarn.lock /usr/src/app/yarn.lock
ADD . /usr/src/app/
RUN yarn install
RUN yarn run build:web
# Stage 2: Bundle the built application into a Docker container
# which runs openresty (nginx) using Alpine Linux
# LINK: https://hub.docker.com/r/openresty/openresty
FROM openresty/openresty:1.15.8.1rc1-0-alpine-fat
RUN mkdir /var/log/nginx
RUN apk add --no-cache openssl
RUN apk add --no-cache openssl-dev
RUN apk add --no-cache git
RUN apk add --no-cache gcc
# !!!
RUN luarocks install lua-resty-openidc
#
RUN luarocks install lua-resty-jwt
RUN luarocks install lua-resty-session
RUN luarocks install lua-resty-http
# !!!
RUN luarocks install lua-resty-openidc
RUN luarocks install luacrypto
# Copy build output to image
COPY --from=builder /usr/src/app/build /var/www/html
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,212 @@
body {
background-color: #040507;
background-image: url('../img/background.jpg');
background-size: cover;
background-repeat: no-repeat;
width: 100vw;
height: 100vh;
overflow: hidden;
color: #fff;
font-family: sans-serif;
text-shadow: 0px 0px 10px #000;
}
a {
color: #fff;
}
div#kc-content {
position: absolute;
top: 20%;
left: 50%;
width: 550px;
margin-left: -180px;
}
div#kc-form {
float: left;
width: 350px;
}
div#kc-form label {
display: block;
font-size: 16px;
}
div#info-area {
position: fixed;
bottom: 0;
left: 0;
margin-top: 40px;
background-color: rgba(0, 0, 0, 0.4);
padding: 20px;
width: 100%;
}
div#info-area p {
margin-right: 30px;
display: inline;
text-shadow: none;
}
input[type='text'],
input[type='password'] {
color: #333;
font-size: 18px;
margin-bottom: 20px;
background-color: rgba(256, 256, 256, 0.7);
border: 0px solid rgba(0, 0, 0, 0.2);
box-shadow: inset 0 0 2px 2px rgba(0, 0, 0, 0.15);
padding: 10px;
width: 296px;
}
input[type='text']:hover,
input[type='password']:hover {
background-color: rgba(256, 256, 256, 0.9);
}
input[type='submit'] {
border: none;
background: -webkit-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
);
background: -moz-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
);
background: -ms-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
);
background: -o-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
);
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5);
color: rgba(0, 0, 0, 0.6);
font-size: 14px;
font-weight: bold;
padding: 10px;
margin-top: 20px;
margin-right: 10px;
width: 150px;
}
input[type='submit']:hover {
background-color: rgba(255, 255, 255, 0.8);
}
div#kc-form-options div {
display: inline-block;
margin-right: 20px;
font-size: 12px;
}
div#kc-form-options div label {
font-size: 12px;
}
div#kc-feedback {
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
width: 100%;
text-align: center;
}
div#kc-feedback-wrapper {
padding: 1em;
}
div.feedback-success {
background-color: rgba(155, 155, 255, 0.1);
}
div.feedback-warning {
background-color: rgba(255, 175, 0, 0.1);
}
div.feedback-error {
background-color: rgba(255, 0, 0, 0.1);
}
div#kc-header {
display: none;
}
div#kc-registration {
margin-bottom: 20px;
}
div#social-login {
border-left: 1px solid rgba(255, 255, 255, 0.2);
float: right;
width: 150px;
padding: 20px 0 200px 40px;
}
div.social-login span {
display: none;
}
div#kc-social-providers ul {
list-style: none;
margin: 0;
padding: 0;
}
div#kc-social-providers ul li {
margin-bottom: 20px;
}
div#kc-social-providers ul li span {
display: inline;
width: 100px;
}
a.zocial {
border: none;
background: -webkit-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
) !important;
background: -moz-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
) !important;
background: -ms-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
) !important;
background: -o-linear-gradient(
top,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0.1)
) !important;
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.5);
color: rgba(0, 0, 0, 0.6);
width: 130px;
text-shadow: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
}

Some files were not shown because too many files have changed in this diff Show More