Merge pull request #2 from OHIF/v3-stable

V3 stable
This commit is contained in:
Salim Kanoun 2022-12-28 21:25:05 +01:00 committed by GitHub
commit 799d648a90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1364 additions and 953 deletions

View File

@ -1,10 +1,16 @@
server {
listen ${PORT};
# listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
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 {

View File

@ -1,6 +1,6 @@
#!/bin/sh
envsubst `${PORT}` < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
then

View File

@ -63,11 +63,10 @@ RUN yarn run build
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
#RUN apk add --no-cache bash
ENV PORT=3000
ENV PORT=80
RUN rm /etc/nginx/conf.d/default.conf
USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN envsubst `${PORT}` < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
ENTRYPOINT ["/usr/src/entrypoint.sh"]

View File

@ -45,7 +45,7 @@
"dependencies": {
"@babel/runtime": "7.16.3",
"classnames": "^2.2.6",
"@cornerstonejs/core": "^0.21.5",
"@cornerstonejs/tools": "^0.29.8"
"@cornerstonejs/core": "^0.22.3",
"@cornerstonejs/tools": "^0.30.6"
}
}

View File

@ -43,9 +43,9 @@
},
"dependencies": {
"@babel/runtime": "7.17.9",
"@cornerstonejs/core": "^0.21.5",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.5",
"@cornerstonejs/tools": "^0.29.8",
"@cornerstonejs/core": "^0.22.3",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.11",
"@cornerstonejs/tools": "^0.30.6",
"@kitware/vtk.js": "25.9.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",

View File

@ -70,6 +70,7 @@ export default async function init({
DisplaySetService,
UIDialogService,
UIModalService,
UINotificationService,
CineService,
CornerstoneViewportService,
HangingProtocolService,
@ -79,6 +80,15 @@ export default async function init({
window.services = servicesManager.services;
if (!window.crossOriginIsolated) {
UINotificationService.show({
title: 'Cross Origin Isolation',
message:
'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
type: 'warning',
});
}
if (cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(UIModalService, HangingProtocolService);
}

View File

@ -1332,9 +1332,7 @@ class SegmentationService {
segDisplaySetInstanceUID
);
const {
FrameOfReferenceUID: segFrameOfReferenceUID,
} = segDisplaySet.instance;
const segFrameOfReferenceUID = segDisplaySet.instance?.FrameOfReferenceUID;
viewportDisplaySetInstanceUIDs.forEach(displaySetInstanceUID => {
// check if the displaySet is sharing the same frameOfReferenceUID

View File

@ -136,7 +136,7 @@ class CornerstoneViewportService implements IViewportService {
public destroy() {
this._removeResizeObserver();
this.viewportGridResizeObserver = null;
this.renderingEngine.destroy();
this.renderingEngine?.destroy?.();
this.viewportsDisplaySets.clear();
this.renderingEngine = null;
cache.purgeCache();

View File

@ -82,6 +82,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
<div className="w-1/2">
<InputRange
value={instanceNumber}
key={selectedDisplaySetInstanceUID}
onChange={value => {
setInstanceNumber(parseInt(value));
}}

View File

@ -234,6 +234,7 @@ function PanelStudyBrowser({
return (
<StudyBrowser
tabs={tabs}
servicesManager={servicesManager}
activeTabName={activeTabName}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}

View File

@ -202,6 +202,7 @@ function ViewerLayout({
<div>
<Header
menuOptions={menuOptions}
isReturnEnabled={!!appConfig.showStudyList}
onClickReturnButton={onClickReturnButton}
WhiteLabeling={appConfig.whiteLabeling}
>

View File

@ -0,0 +1,29 @@
import React from 'react';
/**
*
* Note: this is an example of how the customization module can be used
* using the customization module. Below, we are adding a new custom route
* to the application at the path /custom and rendering a custom component
* Real world use cases of the having a custom route would be to add a
* custom page for the user to view their profile, or to add a custom
* page for login etc.
*/
export default function getCustomizationModule() {
return [
{
name: 'helloPage',
value: {
id: 'customRoutes',
routes: [
{
path: '/custom',
children: () => (
<h1 style={{ color: 'white' }}>Hello Custom Route</h1>
),
},
],
},
},
];
}

View File

@ -6,6 +6,7 @@ import getToolbarModule from './getToolbarModule';
import commandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
import getCustomizationModule from './getCustomizationModule';
import { id } from './id.js';
import init from './init';
@ -36,6 +37,8 @@ const defaultExtension = {
},
];
},
getCustomizationModule,
};
export default defaultExtension;

View File

@ -32,8 +32,8 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.2.6",
"@cornerstonejs/core": "^0.21.5",
"@cornerstonejs/tools": "^0.29.8",
"@cornerstonejs/core": "^0.22.3",
"@cornerstonejs/tools": "^0.30.6",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.28.3",
"prop-types": "^15.6.2",

View File

@ -349,6 +349,7 @@ function PanelStudyBrowserTracking({
return (
<StudyBrowser
tabs={tabs}
servicesManager={servicesManager}
activeTabName={activeTabName}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}

View File

@ -86,6 +86,8 @@ function TrackedCornerstoneViewport(props) {
cineService.playClip(element, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(element);
}
};

View File

@ -140,7 +140,6 @@ function modeFactory({ modeConfiguration }) {
} = servicesManager.services;
ToolBarService.reset();
MeasurementService.clearMeasurements();
ToolGroupService.destroy();
},
validationTags: {

View File

@ -67,7 +67,13 @@ function modeFactory() {
* Lifecycle hooks
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
const { ToolBarService, ToolGroupService } = servicesManager.services;
const {
MeasurementService,
ToolBarService,
ToolGroupService,
} = servicesManager.services;
MeasurementService.clearMeasurements();
// Init Default and SR ToolGroups
initToolGroups(extensionManager, ToolGroupService, commandsManager);
@ -120,20 +126,16 @@ function modeFactory() {
const {
ToolGroupService,
SyncGroupService,
MeasurementService,
ToolBarService,
SegmentationService,
CornerstoneViewportService,
HangingProtocolService,
} = servicesManager.services;
ToolBarService.reset();
MeasurementService.clearMeasurements();
ToolGroupService.destroy();
SyncGroupService.destroy();
SegmentationService.destroy();
CornerstoneViewportService.destroy();
HangingProtocolService.reset();
},
validationTags: {
study: [],

View File

@ -136,21 +136,17 @@ function modeFactory({ modeConfiguration }) {
const {
ToolGroupService,
SyncGroupService,
MeasurementService,
ToolBarService,
SegmentationService,
CornerstoneViewportService,
HangingProtocolService,
} = servicesManager.services;
unsubscriptions.forEach(unsubscribe => unsubscribe());
ToolBarService.reset();
MeasurementService.clearMeasurements();
ToolGroupService.destroy();
SyncGroupService.destroy();
SegmentationService.destroy();
CornerstoneViewportService.destroy();
HangingProtocolService.reset();
},
validationTags: {
study: [],

View File

@ -32,14 +32,17 @@ export default function setFusionActiveVolume(
toolNames.EllipticalROI
);
// Todo: this should not take into account the loader id
const volumeId = `cornerstoneStreamingImageVolume:${displaySets[0].displaySetInstanceUID}`;
const windowLevelConfig = {
...wlToolConfig,
volumeId: displaySets[0].displaySetInstanceUID,
volumeId,
};
const ellipticalROIConfig = {
...ellipticalToolConfig,
volumeId: displaySets[0].displaySetInstanceUID,
volumeId,
};
ToolGroupService.setToolConfiguration(

View File

@ -40,13 +40,12 @@ export default class ExtensionManager {
_extensionLifeCycleHooks,
} = this;
const {
MeasurementService,
ViewportGridService,
} = _servicesManager.services;
MeasurementService.clearMeasurements();
ViewportGridService.reset();
// The onModeEnter of the service must occur BEFORE the extension
// onModeEnter in order to reset the state to a standard state
// before the extension restores and cached data.
for (const service of Object.values(_servicesManager.services)) {
service?.onModeEnter?.();
}
registeredExtensionIds.forEach(extensionId => {
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
@ -69,14 +68,6 @@ export default class ExtensionManager {
_extensionLifeCycleHooks,
} = this;
const {
MeasurementService,
ViewportGridService,
} = _servicesManager.services;
MeasurementService.clearMeasurements();
ViewportGridService.reset();
registeredExtensionIds.forEach(extensionId => {
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
@ -87,6 +78,16 @@ export default class ExtensionManager {
});
}
});
// The service onModeExit calls must occur after the extension ones
// so that extension ones can store/restore data.
for (const service of Object.values(_servicesManager.services)) {
try {
service?.onModeExit?.();
} catch (e) {
console.warn('onModeExit caught', e);
}
}
}
/**
@ -200,6 +201,7 @@ export default class ExtensionManager {
case MODULE_TYPES.SOP_CLASS_HANDLER:
case MODULE_TYPES.CONTEXT:
case MODULE_TYPES.LAYOUT_TEMPLATE:
case MODULE_TYPES.CUSTOMIZATION:
case MODULE_TYPES.UTILITY:
// Default for most extension points,
// Just adds each entry ready for consumption by mode.

View File

@ -235,6 +235,9 @@ describe('ExtensionManager.js', () => {
getUtilityModule: () => {
return [{}];
},
getCustomizationModule: () => {
return [{}];
},
};
await extensionManager.registerExtension(extension);

View File

@ -1,5 +1,6 @@
export default {
COMMANDS: 'commandsModule',
CUSTOMIZATION: 'customizationModule',
DATA_SOURCE: 'dataSourcesModule',
PANEL: 'panelModule',
SOP_CLASS_HANDLER: 'sopClassHandlerModule',

View File

@ -24,6 +24,7 @@ describe('Top level exports', () => {
'OHIF',
//
'CineService',
'CustomizationServiceRegistration',
'UIDialogService',
'UIModalService',
'UINotificationService',

View File

@ -27,6 +27,7 @@ import {
HangingProtocolService,
pubSubServiceInterface,
UserAuthenticationService,
CustomizationServiceRegistration,
} from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
@ -57,6 +58,7 @@ const OHIF = {
viewer: {},
//
CineService,
CustomizationServiceRegistration,
UIDialogService,
UIModalService,
UINotificationService,
@ -92,6 +94,7 @@ export {
DICOMWeb,
//
CineService,
CustomizationServiceRegistration,
UIDialogService,
UIModalService,
UINotificationService,

View File

@ -0,0 +1,169 @@
import CustomizationService from './CustomizationService';
import log from '../../log';
jest.mock('../../log.js', () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}));
const extensionManager = {
registeredExtensionIds: [],
moduleEntries: {},
getModuleEntry: function (id) {
return this.moduleEntries[id];
},
};
const commandsManager = {};
const ohifOverlayItem = {
id: 'ohif.overlayItem',
content: function (props) {
return {
label: this.label,
value: props[this.attribute],
ver: 'default',
};
},
};
const testItem = {
id: 'testItem',
customizationType: 'ohif.overlayItem',
attribute: 'testAttribute',
label: 'testItemLabel',
};
describe('CustomizationService.ts', () => {
let customizationService;
let configuration;
beforeEach(() => {
log.warn.mockClear();
jest.clearAllMocks();
configuration = {};
customizationService = new CustomizationService({
configuration,
commandsManager,
});
});
describe('init', () => {
it('init succeeds', () => {
customizationService.init(extensionManager);
});
it('configurationRegistered', () => {
configuration.testItem = testItem;
customizationService.init(extensionManager);
expect(customizationService.getGlobalCustomization('testItem')).toBe(
testItem
);
});
it('defaultRegistered', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
extensionManager.moduleEntries[
'@testExtension.customizationModule.default'
] = { name: 'default', value: [testItem] };
customizationService.init(extensionManager);
expect(customizationService.getGlobalCustomization('testItem')).toBe(
testItem
);
});
});
describe('customizationType', () => {
it('inherits type', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
extensionManager.moduleEntries[
'@testExtension.customizationModule.default'
] = { name: 'default', value: [ohifOverlayItem] };
configuration.testItem = testItem;
customizationService.init(extensionManager);
const item = customizationService.getGlobalCustomization('testItem');
const props = { testAttribute: 'testAttrValue' };
const result = item.content(props);
expect(result.label).toBe(testItem.label);
expect(result.value).toBe(props.testAttribute);
expect(result.ver).toBe('default');
});
it('inline default inherits type', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
extensionManager.moduleEntries[
'@testExtension.customizationModule.default'
] = { name: 'default', value: [ohifOverlayItem] };
configuration.testItem = testItem;
customizationService.init(extensionManager);
const item = customizationService.getGlobalCustomization('testItem2', {
id: 'testItem2',
customizationType: 'ohif.overlayItem',
label: 'otherLabel',
attribute: 'otherAttr',
});
// Customizes the default value, as this is testItem2
const props = { otherAttr: 'other attribute value' };
const result = item.content(props);
expect(result.label).toBe('otherLabel');
expect(result.value).toBe(props.otherAttr);
expect(result.ver).toBe('default');
});
});
describe('mode customization', () => {
it('onModeEnter can add extensions', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
extensionManager.moduleEntries[
'@testExtension.customizationModule.default'
] = { name: 'default', value: [ohifOverlayItem] };
customizationService.init(extensionManager);
expect(
customizationService.getModeCustomization('testItem')
).toBeUndefined();
customizationService.addModeCustomizations([testItem]);
expect(
customizationService.getGlobalCustomization('testItem')
).toBeUndefined();
const item = customizationService.getModeCustomization('testItem');
const props = { testAttribute: 'testAttrValue' };
const result = item.content(props);
expect(result.label).toBe(testItem.label);
expect(result.value).toBe(props.testAttribute);
expect(result.ver).toBe('default');
});
it('global customizations override modes', () => {
extensionManager.registeredExtensionIds.push('@testExtension');
extensionManager.moduleEntries[
'@testExtension.customizationModule.default'
] = { name: 'default', value: [ohifOverlayItem] };
configuration.testItem = testItem;
customizationService.init(extensionManager);
// Add a mode customization that would otherwise fail below
customizationService.addModeCustomizations([
{ ...testItem, label: 'other' },
]);
const item = customizationService.getModeCustomization('testItem');
const props = { testAttribute: 'testAttrValue' };
const result = item.content(props);
expect(result.label).toBe(testItem.label);
expect(result.value).toBe(props.testAttribute);
});
});
});

View File

@ -0,0 +1,264 @@
import merge from 'lodash.merge';
import { PubSubService } from '../_shared/pubSubServiceInterface';
import { Customization, NestedStrings, Obj } from './types';
const EVENTS = {
MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified',
GLOBAL_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:globalModified',
};
const flattenNestedStrings = (
strs: NestedStrings | string,
ret?: Record<string, string>
): Record<string, string> => {
if (!ret) ret = {};
if (!strs) return ret;
if (Array.isArray(strs)) {
for (const val of strs) {
flattenNestedStrings(val, ret);
}
} else {
ret[strs] = strs;
}
return ret;
};
/**
* The CustomizationService allows for retrieving of custom components
* and configuration for mode and global values.
* The intent of the items is to provide a react component. This can be
* done by straight out providing an entire react component or else can be
* done by configuring a react component, or configuring a part of a react
* component. These are intended to be fairly indistinguishable in use of
* it, although the internals of how that is implemented may need to know
* about the customization service.
*
* A customization value can be:
* 1. React function, taking (React, props) and returning a rendered component
* For example, createLogoComponentFn renders a component logo for display
* 2. Custom UI component configuration, as defined by the component which uses it.
* For example, context menus define a complex structure allowing site-determined
* context menus to be set.
* 3. A string name, being the extension id for retrieving one of the above.
*
* The default values for the extension come from the app_config value 'whiteLabeling',
* The whiteLabelling can have lists of extensions to load for the default global and
* mode extensions. These are:
* 'globalExtensions' which is a list of extension id's to load for global values
* 'modeExtensions' which is a list of extension id's to load for mode values
* They default to the list ['*'] if not otherwise provided, which means to check
* every module for the given id and to load it/add it to the extensions.
*/
export default class CustomizationService extends PubSubService {
commandsManager: Record<string, unknown>;
extensionManager: Record<string, unknown>;
modeCustomizations: Record<string, Customization> = {};
globalCustomizations: Record<string, Customization> = {};
configuration: UICustomizationConfiguration;
constructor({ configuration, commandsManager }) {
super(EVENTS);
this.commandsManager = commandsManager;
this.configuration = configuration || {};
}
public init(extensionManager: ExtensionManager): void {
this.extensionManager = extensionManager;
this.initDefaults();
this.addReferences(this.configuration);
}
initDefaults(): void {
this.extensionManager.registeredExtensionIds.forEach(extensionId => {
const key = `${extensionId}.customizationModule.default`;
const defaultCustomizations = this.findExtensionValue(key);
if (!defaultCustomizations) return;
const { value } = defaultCustomizations;
this.addReference(value, true);
});
}
findExtensionValue(value: string): Obj | void {
const entry = this.extensionManager.getModuleEntry(value);
return entry;
}
public onModeEnter(): void {
super.reset();
this.modeCustomizations = {};
}
/**
*
* @param {*} interaction - can be undefined to run nothing
* @param {*} extraOptions to include in the commands run
*/
recordInteraction(
interaction: Customization | void,
extraOptions?: Record<string, unknown>
): void {
if (!interaction) return;
const commandsManager = this.commandsManager;
const { commands = [] } = interaction;
commands.forEach(({ commandName, commandOptions, context }) => {
if (commandName) {
commandsManager.runCommand(
commandName,
{
interaction,
...commandOptions,
...extraOptions,
},
context
);
} else {
console.warn('No command name supplied in', interaction);
}
});
}
public getModeCustomizations(): Record<string, Customization> {
return this.modeCustomizations;
}
public setModeCustomization(
customizationId: string,
customization: Customization
): void {
this.modeCustomizations[customizationId] = merge(
this.modeCustomizations[customizationId] || {},
customization
);
this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, {
buttons: this.modeCustomizations,
button: this.modeCustomizations[customizationId],
});
}
/** Mode customizations are changes to the behaviour of the extensions
* when running in a given mode. Reset clears mode customizations.
* Note that global customizations over-ride mode customizations.
* @param defautlValue to return if no customization specified.
*/
public getModeCustomization(
customizationId: string,
defaultValue?: Customization
): Customization | void {
const customization =
this.globalCustomizations[customizationId] ??
this.modeCustomizations[customizationId] ??
defaultValue;
return this.applyType(customization);
}
/** Applies any inheritance due to UI Type customization */
public applyType(customization: Customization): Customization {
if (!customization) return customization;
const { customizationType } = customization;
if (!customizationType) return customization;
const parent = this.getModeCustomization(customizationType);
return parent
? Object.assign(Object.create(parent), customization)
: customization;
}
public addModeCustomizations(modeCustomizations): void {
if (!modeCustomizations) {
return;
}
this.addReferences(modeCustomizations, false);
this._broadcastModeCustomizationModified();
}
_broadcastModeCustomizationModified(): void {
this._broadcastEvent(EVENTS.MODE_CUSTOMIZATION_MODIFIED, {
modeCustomizations: this.modeCustomizations,
globalCustomizations: this.globalCustomizations,
});
}
/** Global customizations are those that affect parts of the GUI other than
* the modes. They include things like settings for the search screen.
* Reset does NOT clear global customizations.
*/
getGlobalCustomization(
id: string,
defaultValue?: Customization
): Customization | void {
return this.applyType(this.globalCustomizations[id] ?? defaultValue);
}
setGlobalCustomization(id: string, value: Customization): void {
this.globalCustomizations[id] = value;
this._broadcastGlobalCustomizationModified();
}
protected setConfigGlobalCustomization(
configuration: AppConfigCustomization
): void {
this.globalCustomizations = {};
const keys = flattenNestedStrings(configuration.globalCustomizations);
this.readCustomizationTypes(
v => keys[v.name] && v.customization,
this.globalCustomizations
);
// TODO - iterate over customizations, loading them from the extension
// manager.
this._broadcastGlobalCustomizationModified();
}
_broadcastGlobalCustomizationModified(): void {
this._broadcastEvent(EVENTS.GLOBAL_CUSTOMIZATION_MODIFIED, {
modeCustomizations: this.modeCustomizations,
globalCustomizations: this.globalCustomizations,
});
}
/**
* A single reference is either an an array, or a single customization value,
* whose id is the id in the object, or the parent id.
* This allows for general use to register customizationModule entries.
*/
addReference(
value?: Obj | Obj[] | string,
isGlobal = true,
id?: string
): void {
if (!value) return;
if (typeof value === 'string') {
const extensionValue = this.findExtensionValue(value);
this.addReferences(extensionValue);
} else if (Array.isArray(value)) {
this.addReferences(value, isGlobal);
} else {
const useId = value.id || id;
this[isGlobal ? 'setGlobalCustomization' : 'setModeCustomization'](
useId as string,
value
);
}
}
/** References are:
* list of customizations, added in order
* object containing a customization id and value
* This format allows for the original whitelist format.
*/
addReferences(references?: Obj | Obj[], isGlobal = true): void {
if (!references) return;
if (Array.isArray(references)) {
references.forEach(item => {
this.addReference(item, isGlobal);
});
} else {
for (const key of Object.keys(references)) {
const value = references[key];
this.addReference(value, isGlobal, key);
}
}
}
}

View File

@ -0,0 +1,11 @@
import CustomizationService from './CustomizationService';
const CustomizationServiceRegistration = {
name: 'customizationService',
create: ({ configuration = {}, commandsManager }) => {
return new CustomizationService({ configuration, commandsManager });
},
};
export default CustomizationServiceRegistration;
export { CustomizationService, CustomizationServiceRegistration };

View File

@ -0,0 +1,39 @@
import Command from '../../types/Command';
import { ComponentType } from 'react';
export type Obj = Record<string, unknown>;
export interface BaseCustomization extends Obj {
id: string;
customizationType?: string;
description?: string;
label?: string;
commands?: Command[];
}
export interface LabelCustomization extends BaseCustomization {
label: string;
}
export interface CodeCustomization extends BaseCustomization {
code: string;
}
export interface CommandCustomization extends BaseCustomization {
commands: Command[];
}
export type Customization =
| BaseCustomization
| LabelCustomization
| CommandCustomization
| CodeCustomization;
export default Customization;
export type ComponentReturn = {
component: ComponentType;
props?: Obj;
};
export type NestedStrings = string[] | NestedStrings[];

View File

@ -49,7 +49,15 @@ function createStudyMetadata(StudyInstanceUID) {
);
if (existingSeries) {
existingSeries.instances.push(...instances);
// Only add instances not already present, so generate a map
// of existing instances and filter the to add by things
// already present.
const sopMap = {};
existingSeries.instances.forEach(
it => (sopMap[it.SOPInstanceUID] = it)
);
const newInstances = instances.filter(it => !sopMap[it.SOPInstanceUID]);
existingSeries.instances.push(...newInstances);
} else {
const series = createSeriesMetadata(instances);
this.series.push(series);

View File

@ -53,7 +53,12 @@ export default class DisplaySetService {
const activeDisplaySets = this.activeDisplaySets;
displaySets.forEach(displaySet => {
activeDisplaySets.push(displaySet);
// This test makes adding display sets an N^2 operation, so it might
// become important to do this in an efficient manner for large
// numbers of display sets.
if (!activeDisplaySets.includes(displaySet)) {
activeDisplaySets.push(displaySet);
}
});
}
@ -194,6 +199,17 @@ export default class DisplaySetService {
}
};
/**
* The onModeExit returns the display set service to the initial state,
* that is without any display sets. To avoid recreating display sets,
* the mode specific onModeExit is called before this method and should
* store the active display sets and the cached data.
*/
onModeExit() {
this.getDisplaySetCache().length = 0;
this.activeDisplaySets.length = 0;
}
makeDisplaySetForInstances(instancesSrc, settings) {
let instances = instancesSrc;
const instance = instances[0];

View File

@ -122,6 +122,11 @@ class HangingProtocolService {
this.displaySetMatchDetails = new Map();
}
/** Leave the hanging protocol in the initialized state */
public onModeExit() {
this.reset();
}
public getActiveProtocol(): {
protocol: HangingProtocol.Protocol;
stage: number;

View File

@ -570,6 +570,15 @@ class MeasurementService {
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
}
/**
* Called after the mode.onModeExit is called to reset the state.
* To store measurements for later use, store them in the mode.onModeExit
* and restore them in the mode onModeEnter.
*/
onModeExit() {
this.clearMeasurements();
}
jumpToMeasurement(viewportIndex, measurementUID) {
const measurement = this.measurements[measurementUID];

View File

@ -52,11 +52,19 @@ export default class ToolBarService {
this.buttons = {};
}
onModeEnter() {
this.reset();
}
/**
*
* @param {*} interaction
* @param {*} interaction - can be undefined to run nothing
* @param {*} options is an optional set of extra commandOptions
* used for calling the specified interaction. That is, the command is
* called with {...commandOptions,...options}
*/
recordInteraction(interaction) {
recordInteraction(interaction, options) {
if (!interaction) return;
const commandsManager = this._commandsManager;
const { groupId, itemId, interactionType, commands } = interaction;
@ -64,7 +72,14 @@ export default class ToolBarService {
case 'action': {
commands.forEach(({ commandName, commandOptions, context }) => {
if (commandName) {
commandsManager.runCommand(commandName, commandOptions, context);
commandsManager.runCommand(
commandName,
{
...commandOptions,
...options,
},
context
);
}
});
break;
@ -171,6 +186,10 @@ export default class ToolBarService {
}
}
getButton(id) {
return this.buttons[id];
}
setButtons(buttons) {
this.buttons = buttons;
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {

View File

@ -24,6 +24,7 @@ class ViewportGridService {
restoreCachedLayout: restoreCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
onModeExit: onModeExitImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
@ -50,6 +51,9 @@ class ViewportGridService {
if (restoreCachedLayoutImplementation) {
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
}
if (onModeExitImplementation) {
this.serviceImplementation._onModeExit = onModeExitImplementation;
}
if (setImplementation) {
this.serviceImplementation._set = setImplementation;
}
@ -92,6 +96,16 @@ class ViewportGridService {
this.serviceImplementation._reset();
}
/**
* The onModeExit must set the state of the viewport grid to a standard/clean
* state. To implement store/recover of the viewport grid, perform
* a state store in the mode or extension onModeExit, and recover that
* data if appropriate in the onModeEnter of the mode or extension.
*/
public onModeExit(): void {
this.serviceImplementation._onModeExit();
}
public setCachedLayout({ cacheId, cachedLayout }) {
this.serviceImplementation._setCachedLayout({ cacheId, cachedLayout });
}

View File

@ -1,4 +1,5 @@
import guid from '../../utils/guid';
import * as Types from '../../Types';
/**
* Consumer must implement:
@ -24,7 +25,7 @@ function subscribe(eventName, callback) {
const listenerId = guid();
const subscription = { id: listenerId, callback };
console.info(`Subscribing to '${eventName}'.`);
// console.info(`Subscribing to '${eventName}'.`);
if (Array.isArray(this.listeners[eventName])) {
this.listeners[eventName].push(subscription);
} else {
@ -86,3 +87,21 @@ function _broadcastEvent(eventName, callbackProps) {
});
}
}
/** Export a PubSubService class to be used instead of the individual items */
export class PubSubService {
constructor(EVENTS) {
this.EVENTS = EVENTS;
this.subscribe = subscribe;
this._broadcastEvent = _broadcastEvent;
this._unsubscribe = _unsubscribe;
this._isValidEvent = _isValidEvent;
this.listeners = {};
this.unsubscriptions = [];
}
reset() {
this.unsubscriptions.forEach(unsub => unsub());
this.unsubscriptions = [];
}
}

View File

@ -10,12 +10,20 @@ import ToolBarService from './ToolBarService';
import ViewportGridService from './ViewportGridService';
import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
import pubSubServiceInterface, {
PubSubService,
} from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
import {
CustomizationService,
CustomizationServiceRegistration,
} from './CustomizationService';
export {
MeasurementService,
ServicesManager,
CustomizationService,
CustomizationServiceRegistration,
UIDialogService,
UIModalService,
UINotificationService,
@ -27,5 +35,6 @@ export {
HangingProtocolService,
CineService,
pubSubServiceInterface,
PubSubService,
UserAuthenticationService,
};

View File

@ -0,0 +1,7 @@
export interface Command {
commandName: string;
commandOptions?: Record<string, unknown>;
context?: string;
}
export default Command;

View File

@ -5,13 +5,21 @@ import {
} from './StudyMetadata';
import Consumer from './Consumer';
import { ExtensionManager } from '../extensions';
import { CustomizationService, PubSubService } from '../services';
import * as HangingProtocol from './HangingProtocol';
import Command from './Command';
export * from '../services/CustomizationService/types';
export type {
ExtensionManager,
HangingProtocol,
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
Consumer,
PubSubService,
CustomizationService,
Command,
};

View File

@ -12,16 +12,16 @@
* Returns undefined if the palette data is absent.
*/
export default function fetchPaletteColorLookupTableData(
item, tag, descriptorTag
item,
tag,
descriptorTag
) {
const { PaletteColorLookupTableUID } = item;
const paletteData = item[tag];
if (paletteData === undefined && PaletteColorLookupTableUID === undefined) return;
if (paletteData === undefined && PaletteColorLookupTableUID === undefined)
return;
// performance optimization - read UID and cache by UID
return _getPaletteColor(
item[tag],
item[descriptorTag]
)
return _getPaletteColor(item[tag], item[descriptorTag]);
}
function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) {
@ -36,13 +36,12 @@ function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) {
if (bits === 16) {
let j = 0;
for (let i = 0; i < numLutEntries; i++) {
lut[i] = arraybuffer[j++] + arraybuffer[j++] << 8;
lut[i] = (arraybuffer[j++] + arraybuffer[j++]) << 8;
}
} else {
for (let i = 0; i < numLutEntries; i++) {
lut[i] = byteArray[i];
}
}
return lut;
};
@ -53,20 +52,33 @@ function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) {
if (paletteColorLookupTableData.InlineBinary) {
try {
const arraybuffer = Uint8Array.from(atob(paletteColorLookupTableData.InlineBinary), c =>
c.charCodeAt(0)
const arraybuffer = Uint8Array.from(
atob(paletteColorLookupTableData.InlineBinary),
c => c.charCodeAt(0)
);
return (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(arraybuffer));
return (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(
arraybuffer
));
} catch (e) {
console.log("Couldn't decode", paletteColorLookupTableData.InlineBinary, e);
console.log(
"Couldn't decode",
paletteColorLookupTableData.InlineBinary,
e
);
return undefined;
}
}
if (paletteColorLookupTableData.retrieveBulkData) {
return paletteColorLookupTableData.retrieveBulkData().then(val =>
(paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(val)));
return paletteColorLookupTableData
.retrieveBulkData()
.then(
val =>
(paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(
val
))
);
}
throw new Error(`No data found for ${paletteColorLookupTableData} palette`)
console.error(`No data found for ${paletteColorLookupTableData} palette`);
}

View File

@ -0,0 +1,48 @@
---
sidebar_position: 4
---
# Docker
The OHIF source code provides a Dockerfile to create and run a Docker image that containerizes an [nginx](https://www.nginx.com/) web server serving the OHIF Viewer.
:::info Good to Know
The OHIF Viewer Docker image for the `v3-stable` branch is not yet published. The available image in [Docker Hub](https://hub.docker.com/r/ohif/viewer) is based on the `master` branch.
:::
## Prequisites
The machine on which to build and run the Docker container must have:
1. All of the [requirements](./build-for-production.md#build-for-production) for building a production version of OHIF.
2. A checked out branch of the OHIF Viewer.
3. [Docker](https://docs.docker.com/get-docker/) installed.
## Building the Docker Image
The docker image can be built from a terminal window as such:
1. Switch to the OHIF Viewer code root directory.
2. Issue the following docker command. Note that what follows `-t` flag is the `{name}:{tag}` for the Docker image and is arbitrary when creating a local Docker image.
```sh
docker build . -t ohif-viewer-image
```
## Running the Docker Container
Once the Docker image has been built, it can be run as a container from the command line as in the block below. Note that the last argument to the command is the name of the Docker image and the table below describes the other arguments.
|Flag|Description|
|----|-----------|
|-d|Run the container in the background and print the container ID|
|-p {host-port}:{nginx-port}/tcp|Publish the `nginx` listen port on the given host port|
|--name|An arbitrary name for the container.|
```sh
docker run -d -p 3000:80/tcp --name ohif-viewer-container ohif-viewer-image
```
### Configuring the `nginx` Listen Port
The Dockerfile and entry point use the `${PORT}` environment variable as the port that the `nginx` server uses to serve the web server. The default value for `${PORT}` is `80`. One way to set this environment variable is to use the `-e` switch when running the container with `docker run`. The block below gives an example where the listen port is set to `8080` and publised on the host as `3000`.
```sh
docker run -d -e PORT=8080 -p 3000:8080/tcp --name ohif-viewer-container ohif-viewer-image
```

View File

@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---
# Google Cloud Healthcare

View File

@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---
# Nginx + Image Archive

View File

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 6
---
# User Account Control

View File

@ -206,8 +206,10 @@ used to initialize data.
[`onModeExit`](./lifecycle#onModeExit): Similarly to onModeEnter, this hook is
called when navigating away from a mode, or before a modes data or datasource
is changed. This can be used to clean up data (e.g. remove annotations that do
not need to be persisted)
is changed. This can be used to cache data for re-use later, but since it
isn't known which mode will be entered next, the state after exiting should be
clean, that is, the same as the state on a clean start. This is called BEFORE
service clean up, and after mode specific onModeExit handling.
## Modules

View File

@ -51,6 +51,7 @@ By default, `OHIF-v3` registers the following services in the `appInit`.
```js title="platform/viewer/src/appInit.js"
servicesManager.registerServices([
CustomizationService,
UINotificationService,
UIModalService,
UIDialogService,
@ -86,9 +87,16 @@ export default {
and the implementation of `ToolBarService` lies in the same folder at
`./ToolbarSerivce.js`.
> Note, the create method is critical for any custom service that you write and
> Note: The create method is critical for any custom service that you write and
> want to add to the list of services
> Note: For typescript definitions, the service type should be exported
> as part of the Types export on the module. This is recommended going forward
> and existing services will be migrated. As well, the capitalization of the
> name should be lower camel case, with the type being upper camel case. In
> the above example, the service instance should be `toolBarService` with the
> class being `ToolBarService`.
## Accessing Services
Throughout the app you can use `services` property of the service manager to
@ -135,13 +143,15 @@ export default {
and the logic for your service shall be
```js title="extensions/customExtension/src/services/backEndService/index.js"
import backEndService from './backEndService';
// Canonical name of upper camel case BackEndService for the class
import BackEndService from './BackEndService';
export default function WrappedBackEndService(serviceManager) {
return {
name: 'myService',
// Note the canonical name of lower camel case backEndService
name: 'backEndService',
create: ({ configuration = {} }) => {
return new backEndService(serviceManager);
return new BackEndService(serviceManager);
},
};
}
@ -149,8 +159,8 @@ export default function WrappedBackEndService(serviceManager) {
with implementation of
```js
export default class backEndService {
```ts
export default class BackEndService {
constructor(serviceManager) {
this.serviceManager = serviceManager;
}
@ -160,3 +170,35 @@ export default class backEndService {
}
}
```
with a registration of
```ts title="types/index.ts"
import BackEndService from "../services/BackEndService/BackEndService";
export { BackEndService };
```
# Service Mode Lifecycle
Services may implement initialization and cleanup for mode specific data.
In order to prevent defects where there are differences between initial
and subsequent displays of a study, the contract of the service is that the
state the service is in on mode entry shall be the same whether the mode was
entered or was exited and entered again.
To implement storage/recovery of state, the mode must store the data on
exiting the mode, and restore the data in it's onModeEnter. For example,
the mode may decide to preserve measurement data in the onModeExit, and
to restore it in the onModeEnter. This does not violate the contract since
it is the mode's decision to apply the stored state, and to cache it.
## onModeEnter
A service may implement an onModeEnter call to initialize the service to
be ready for entering a mode.
This is called before the mode `onModeEnter` is called.
## onModeExit
When entering a mode, the service contract states that the service needs to
be in the same state whether it is a fresh load or has previously entered the mode.
The onModeExit allows a service to clean itself up after the mode 'onModeExit'
has stored any persistent data.

View File

@ -16,7 +16,11 @@ Currently, there are two hooks that are called for modes:
This hook gets run after the defined route has been entered by the mode. This
hook can be used to initialize the data, services and appearance of the viewer
upon the first render.
upon the first render, in any way that is custom to the mode.
This is called after service `onModeEnter` calls so that the entry into a mode
is done in a predefined/fixed state. That allows any restoring of existing state
to be performed.
For instance, in `longitudinal` mode we are using this hook to initialize the
`ToolBarService` and set the window level/width tool to be active and add
@ -64,9 +68,13 @@ function modeFactory() {
## onModeExit
This hook is called when the viewer navigate away from the route in the url.
This is the place for cleaning up data, and services by unsubscribing to the
events.
This hook is called when the viewer navigates away from the route in the url.
It is called BEFORE the service specific onModeExit calls are performed, and
thus still has access to stateful data which can be cached or stored before
the services clean themselves up.
This is the place for cleaning up NON-service specific data, and services
by unsubscribing to the events. The cleanup of the service itself is intended
to occur in the service `onModeEnter`.
For instance, it can be used to reset the `ToolBarService` which reset the
toggled buttons.

View File

@ -19,6 +19,7 @@ We maintain the following non-ui Services:
- [Hanging Protocol Service](../data/HangingProtocolService.md)
- [Toolbar Service](../data/ToolBarService.md)
- [Measurement Service](../data/MeasurementService.md)
- [Customization Service](customization-service.md)
## Service Architecture

View File

@ -125,6 +125,17 @@ The following services is available in the `OHIF-v3`.
cine
</td>
</tr>
<tr>
<td>
<a href="./ui/customization-service">
CustomizationService
</a>
</td>
<td>UI Service</td>
<td>
customizationService (NEW)
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-dialog-service">

View File

@ -0,0 +1,324 @@
---
sidebar_position: 7
sidebar_label: Customization Service
---
# Customization Service
There are a lot of places where users may want to configure certain elements
differently between different modes or for different deployments. A mode
example might be the use of a custom overlay showing mode related DICOM header
information such as radiation dose or patient age.
The use of this service enables these to be defined in a typed fashion by
providing an easy way to set default values for this, but to allow a
non-default value to be specified by the configuration or mode.
This service is a UI service in that part of the registration allows for registering
UI components and types to deal with, but it does not directly provide an UI
displayable elements unless customized to do so.
## Registering Customizations
There are several ways to register customizations. The
`APP_CONFIG.customizationService`
field is used as a per-configuration entry. This object can list single
configurations by id, or it can list sets of customizations by referring to
the `customizationModule` in an extension. For example, the fictitious
customization 'customIcons' might be defined as below in the APP_CONFIG:
```js
window.config = {
...,
customizationService: [
{
id: 'customIcons',
backArrow: 'https://customIcons.org/backArrow.svg',
},
],
...
}
```
As well, extensions can register default customizations by providing a 'default'
name key within the extension. These are simply customizations loaded when
the extension is loaded. For example, the previous customization could have
been added in an extension as:
```js
getCustomizationModule: () => [
{
name: 'default',
value: {
id: 'customIcons',
backArrow: 'https://customIcons.org/backArrow.svg',
},
},
],
```
Note the name of this is default (thus loaded automatically instead of by
reference), and the value is a customization of customIcons.
The type and parameters of a customization are defined by the user of the
customization, based on the customization id. For example, `cornerstoneOverlay`
is a customization that is a React component, so it requires a react content,
and optionally contentProps which are used to supply values to the content.
The extension can also supply a default parent instance to inherit values from.
This allows the content or other parameters to be pre-filled, and only the
required values changed. The parent to use is specified by the `customizationType` field,
and is simply the id of another customization object. An example of this might
be a demographics overlay field, where the base version needs an actual component,
while the typed version just needs the attribute and label to use.
```js
getCustomizationModule: () => [
{
name: 'default',
value: [
// This first value defines the base type
{
id: imageDemographicOverlay,
content: function({image}) {
return (<p>{image[this.attribute]}</p>);
}
},
// The second one defines an instance.
// It may or may not use the previous type definition - it will use it
// if nothing replaces the previous definition, otherwise it will use the new one.
{
id: PatientIDOverlayItem,
customizationType: 'imageDemographicOverlay',
attribute: 'PatientID',
},
]
}
]
```
Mode-specific customizations are no different from the global ones,
except that the mode customizations are cleared before the mode `onModeEnter`
is called, and they can have new values registered in the `onModeEnter`
The following example shows first the registration of the default instances,
and then shows how they might be used.
```js
// In the cornerstone extension getCustomizationModule:
const getCustomizationModule = () => ([
{
name: 'default',
value: [
{
id: 'ohif.cornerstoneOverlay',
content: CornerstoneOverlay,
// Requires items on instances
},
{
id: 'ohif.overlayItem',
content: CornerstoneOverlayItem,
// Requires attribute and label on instances
},
],
},
]);
```
Then, in the configuration file one might have a custom overlay definition:
```js
// in the APP_CONFIG file set the top right area to show the patient name
// using PN: as a prefix when the study has a non-empty patient name.
customizationService: {
cornerstoneOverlayTopRight: {
id: 'cornerstoneOverlayTopRight',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
id: 'PatientNameOverlay',
// Note the ohif.overlayItem is a prototype instance for this object
// The ohif.overlayItem is defined up above
customizationType: 'ohif.overlayItem',
attribute: 'PatientName',
label: 'PN:',
},
],
},
},
```
In the mode customization, the overlay is then further customized
with a bottom-right overlay, which extends the customizationService configuration.
```js
// Import the type from the extension itself
import OverlayUICustomization from '@ohif/cornerstone-extension';
// In the mode itself, customizations can be registered:
onModeEnter() {
...
// Note how the object can be strongly typed
const bottomRight: OverlayUICustomization = {
id: 'cornerstoneOverlayBottomRight',
// Note the type is the previously registered ohif.cornerstoneOverlay
customizationType: 'ohif.cornerstoneOverlay',
// The cornerstoneOverlay definition requires an items list here.
items: [
// Custom definitions for hte context menu here.
],
};
customizationService.addModeCustomizations(bottomRight);
```
## Mode Customizations
The mode customizations are retrieved via the `getModeCustomization` function,
providing an id, and optionally a default value. The retrieval will return,
in order:
1. Global customization with the given id.
2. Mode customization with the id.
3. The default value specified.
The return value then inherits the `customizationType` instance, so that the
value can be typed and have default values and functionality provided. The object
can then be used in a way defined by the extension provided that customization
point.
```ts
cornerstoneOverlay = uiConfigurationService.getModeCustomization("cornerstoneOverlay", {customizationType: "ohif.cornerstoneOverlay", ...});
const { component: overlayComponent, props} = uiConfigurationService.getComponent(cornerstoneOverlay);
return (<defaultComponent {...props} overlay={cornerstoneOverlay}....></defaultComponent>);
```
This example shows fetching the default component to render this object. The
returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This
object can be a React component or other object such as a commands list, for
example (this example comes from the context menu customizations as that one
uses commands lists):
```ts
cornerstoneContextMenu = uiConfigurationService.getModeCustomization("cornerstoneContextMenu", defaultMenu);
uiConfigurationService.recordInteraction(cornerstoneContextMenu, extraProps);
```
## Global Customizations
Global customizations are retrieved in the same was as mode customizations, except
that the `getGlobalCustomization` is called instead of the mode call.
## Types
Some types for the customization service are provided by the `@ohif/ui` types
export. Additionally, extensions can provide a Types export with custom
typing, allowing for better typing for the extension specific capabilities.
This allows for having strong typing when declaring customizations, for example:
```ts
import { Types } from '@ohif/ui';
const customContextMenu: Types.UIContextMenu =
{
id: 'cornerstoneContextMenu',
customizationType: 'ohif.contextMenu',
// items will be type checked to be in accordance with UIContextMenu.items
items: [ ... ]
},
```
## Inheritance
JavaScript property inheritance can be supplied by defining customizations
with id corresponding to the customizationType value. For example:
```js
getCustomizationModule = () => ([
{
name: 'default',
value: [
{
id: 'ohif.overlayItem',
content: function (props) {
return (<p><b>{this.label}</b> {props.instance[this.attribute]}</p>)
},
},
],
}
])
```
defines an overlay item which has a React content object as the render value.
This can then be used by specifying a customizationType of `ohif.overlayItem`, for example:
```js
const overlayItem: Types.UIOverlayItem = {
id: 'anOverlayItem',
customizationType: 'ohif.overlayItem',
attribute: 'PatientName',
label: 'PN:',
};
```
# Customizations
This section can be used to specify various customization capabilities.
## Text color for StudyBrowser tabs
This is the recommended pattern for deep customization of class attributes,
making it fine grained, and have it apply a set of attributes, mostly from
tailwind. In this case it is a double indirection, as the buttons class
uses it's own internal class names.
* Name: 'class:StudyBrowser'
* Attributes:
** `true` for the is active true text color
** `false` fo rhte is active false text color.
** Values are button colors, from the Button class, eg default, white, black
## customRoutes
* Name: `customRoutes` global
* Attributes:
** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add.
** Should any element of routes match an existing baked in element, the baked in one will be replaced.
** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes)
### Example
```js
{
id: 'customRoutes',
routes: [
{
path: '/myroute',
children: MyRouteReactFunction,
}
],
}
```
There is a usage of this example commented out in config/default.js that
looks like the code below. This example is provided by the default extension,
again with commented out code. Uncomment the getCustomizationModule customRoutes
code in the default module to activate this, and then go to: `http://localhost:3000/custom`
to see the custom route.
Note the name of this is the customization module name, which usually won't match
the id, and in fact there can be multiple customization objects defined for a single
customization module, to allow for customizing sets of related values.
```js
customizationService: [
// Shows a custom route -access via http://localhost:3000/custom
'@ohif/extension-default.customizationModule.helloPage',
],
```
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
<!-- prettier-ignore-end -->

View File

@ -44,7 +44,6 @@
"react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8",
"react-with-direction": "^1.3.1",
"lottie-react": "^2.3.1",
"swiper": "^8.4.2"
},
"devDependencies": {

View File

@ -44,9 +44,12 @@ const variants = {
'text-primary-main hover:opacity-80 active:opacity-100 focus:opacity-80',
secondary:
'text-secondary-light hover:opacity-80 active:opacity-100 focus:opacity-80',
white: 'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
black:
translucent:
'text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
white:
'text-black hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black',
black:
'text-white hover:bg-primary-main focus:bg-primary-main hover:border-black focus:border-black',
primaryActive:
'text-primary-active hover:opacity-80 active:opacity-100 focus:opacity-80',
primaryLight:
@ -227,11 +230,13 @@ Button.propTypes = {
color: PropTypes.oneOf([
'default',
'primary',
'primaryActive',
'secondary',
'white',
'black',
'inherit',
'light',
'translucent',
]),
border: PropTypes.oneOf([
'none',

View File

@ -1,793 +0,0 @@
import React from 'react';
import Lottie from 'lottie-react';
import classNames from 'classnames';
const LoadingAnimation = {
v: '5.9.6',
fr: 60,
ip: 0,
op: 106,
w: 215,
h: 215,
nm: 'ohif-progress',
ddd: 0,
assets: [],
layers: [
{
ddd: 0,
ind: 1,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.797] },
o: { x: [0.274], y: [0.135] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.97] },
o: { x: [0.446], y: [0.039] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.667, y: 0.667 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [130.5, 131.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.006 },
t: 9,
s: [130.5, 131.75, 0],
to: [2.917, 2.875, 0],
ti: [-2.917, -2.875, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [148, 149, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.811 },
o: { x: 0.167, y: 0.17 },
t: 52,
s: [148, 149, 0],
to: [-0.881, -0.866, 0],
ti: [1.995, 1.961, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0 },
t: 76,
s: [108.4, 113.023, 0],
to: [-1.196, -1.176, 0],
ti: [-2.169, -1.843, 0],
},
{
i: { x: 0.833, y: 0.832 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.4, 113, 0],
to: [1.272, 1.081, 0],
ti: [-1.819, -1.575, 0],
},
{
i: { x: 0.833, y: 0.941 },
o: { x: 0.167, y: 0.008 },
t: 95,
s: [115.214, 118.607, 0],
to: [7.038, 6.091, 0],
ti: [1.198, 1.177, 0],
},
{ t: 106, s: [130.5, 131.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 3,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [1.005] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [81, 132, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.011 },
t: 9,
s: [81, 132, 0],
to: [-3, 3, 0],
ti: [3, -3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [63, 150, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.174 },
t: 52,
s: [63, 150, 0],
to: [0.906, -0.906, 0],
ti: [-2.052, 2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.002 },
t: 76,
s: [108.685, 113.315, 0],
to: [1.217, -1.217, 0],
ti: [2.674, -1.792, 0],
},
{
i: { x: 0.833, y: 0.834 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.7, 113.3, 0],
to: [-1.593, 1.067, 0],
ti: [2.246, -1.581, 0],
},
{
i: { x: 0.833, y: 0.942 },
o: { x: 0.167, y: 0.014 },
t: 95,
s: [99.965, 118.742, 0],
to: [-8.674, 6.104, 0],
ti: [-1.236, 1.236, 0],
},
{ t: 106, s: [81, 132, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 5,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.943] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [131, 84.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.014 },
t: 9,
s: [131, 84.75, 0],
to: [3, -3.125, 0],
ti: [-3, 3.125, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [149, 66, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.175 },
t: 52,
s: [149, 66, 0],
to: [-0.906, 0.946, 0],
ti: [2.052, -2.143, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.013 },
t: 76,
s: [109.315, 112.893, 0],
to: [-1.222, 1.276, 0],
ti: [-2.102, 2.732, 0],
},
{
i: { x: 0.833, y: 0.835 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.9, 0],
to: [1.242, -1.615, 0],
ti: [-1.796, 2.277, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.016 },
t: 95,
s: [115.852, 104.071, 0],
to: [6.96, -8.823, 0],
ti: [1.236, -1.29, 0],
},
{ t: 106, s: [131, 84.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 7,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.823], y: [1] },
o: { x: [0.446], y: [-0.058] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [80.5, 85, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.005 },
t: 9,
s: [80.5, 85, 0],
to: [-2.75, -3, 0],
ti: [2.75, 3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [64, 67, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.177 },
t: 52,
s: [64, 67, 0],
to: [0.831, 0.906, 0],
ti: [-1.881, -2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.025 },
t: 76,
s: [109.295, 112.685, 0],
to: [1.112, 1.213, 0],
ti: [2.768, 2.657, 0],
},
{
i: { x: 0.833, y: 0.837 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.7, 0],
to: [-1.648, -1.581, 0],
ti: [2.299, 2.234, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.019 },
t: 95,
s: [100.116, 104.028, 0],
to: [-8.935, -8.681, 0],
ti: [-1.136, -1.239, 0],
},
{ t: 106, s: [80.5, 85, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
],
markers: [],
};
const LoadingIndicator = ({ className }) => {
return (
<div
className={classNames(
'absolute z-50 top-0 left-0 flex flex-col items-center justify-center',
className
)}
>
<Lottie animationData={LoadingAnimation} />;
</div>
);
};
export default LoadingIndicator;

View File

@ -1,2 +0,0 @@
import LoadingIndicator from './LoadingIndicator';
export default LoadingIndicator;

View File

@ -26,8 +26,10 @@ const StudyBrowser = ({
onDoubleClickThumbnail,
onClickUntrack,
activeDisplaySetInstanceUIDs,
servicesManager,
}) => {
const { t } = useTranslation('StudyBrowser');
const { customizationService } = servicesManager?.services || {};
const getTabContent = () => {
const tabData = tabs.find(tab => tab.name === activeTabName);
@ -82,8 +84,13 @@ const StudyBrowser = ({
const isActive = activeTabName === name;
const isDisabled = !studies.length;
// Apply the contrasting color for brighter button color visibility
// const color = isActive ? 'black' : 'default';
const color = 'default';
const classStudyBrowser = customizationService?.getModeCustomization(
'class:StudyBrowser'
) || {
true: 'default',
false: 'default',
};
const color = classStudyBrowser[`${isActive}`];
return (
<Button
key={name}

View File

@ -59,7 +59,7 @@ const StudyListPagination = ({
<Button
size="initial"
className="px-4 py-2 text-base"
color="white"
color="translucent"
border="primary"
variant="outlined"
onClick={() => navigateToPage(1)}
@ -69,7 +69,7 @@ const StudyListPagination = ({
<Button
size="initial"
className="py-2 px-2 text-base"
color="white"
color="translucent"
border="primary"
variant="outlined"
onClick={() => navigateToPage(currentPage - 1)}
@ -79,7 +79,7 @@ const StudyListPagination = ({
<Button
size="initial"
className="py-2 px-4 text-base"
color="white"
color="translucent"
border="primary"
variant="outlined"
onClick={() => navigateToPage(currentPage + 1)}

View File

@ -68,7 +68,6 @@ import InputRange from './InputRange';
import InputNumber from './InputNumber';
import CheckBox from './CheckBox';
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicator from './LoadingIndicator';
export {
AboutModal,
@ -101,7 +100,6 @@ export {
ImageScrollbar,
Label,
LayoutSelector,
LoadingIndicator,
LoadingIndicatorProgress,
MeasurementTable,
Modal,

View File

@ -302,6 +302,7 @@ export function ViewportGridProvider({ children, service }) {
setDisplaySetsForViewports,
setLayout,
reset,
onModeExit: reset,
setCachedLayout,
restoreCachedLayout,
set,

View File

@ -62,7 +62,6 @@ export {
InputText,
Label,
LayoutSelector,
LoadingIndicator,
LoadingIndicatorProgress,
MeasurementTable,
Modal,

View File

@ -3,6 +3,10 @@ window.config = {
// whiteLabelling: {},
extensions: [],
modes: [],
customizationService: {
// Shows a custom route -access via http://localhost:3000/custom
// helloPage: '@ohif/extension-default.customizationModule.helloPage',
},
showStudyList: true,
maxNumberOfWebWorkers: 4,
// below flag is for performance reasons, but it might not work for all servers

View File

@ -1,10 +1,12 @@
window.config = {
routerBasename: '/',
// whiteLabelling: {},
customizationService: [
'@ohif/extension-default.customizationModule.helloPage',
],
extensions: [],
modes: [],
showStudyList: true,
maxNumberOfWebWorkers: 3,
maxNumberOfWebWorkers: 4,
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showLoadingIndicator: true,
@ -51,25 +53,6 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
// whiteLabeling: {
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
// createLogoComponentFn: function (React) {
// return React.createElement(
// 'a',
// {
// target: '_self',
// rel: 'noopener noreferrer',
// className: 'text-purple-600 line-through',
// href: '/',
// },
// React.createElement('img',
// {
// src: './customLogo.svg',
// className: 'w-8 h-8',
// }
// ))
// },
// },
defaultDataSourceName: 'dicomweb',
hotkeys: [
{

View File

@ -1,6 +1,12 @@
window.config = {
routerBasename: '/',
customizationService: [
'@ohif/extension-default.customizationModule.helloPage',
{
id: 'class:StudyBrowser',
true: 'black',
false: 'default',
},
],
extensions: [],
modes: [],

View File

@ -50,18 +50,14 @@ function App({ config, defaultExtensions, defaultModes }) {
// Set appConfig
const appConfigState = init.appConfig;
const { routerBasename, modes, dataSources, oidc } = appConfigState;
// Use config to create routes
const appRoutes = createRoutes({
const {
routerBasename,
modes,
dataSources,
extensionManager,
servicesManager,
commandsManager,
hotkeysManager,
routerBasename,
});
oidc,
showStudyList,
} = appConfigState;
const {
UIDialogService,
UIModalService,
@ -70,6 +66,7 @@ function App({ config, defaultExtensions, defaultModes }) {
ViewportGridService,
CineService,
UserAuthenticationService,
customizationService,
} = servicesManager.services;
const providers = [
@ -89,6 +86,21 @@ function App({ config, defaultExtensions, defaultModes }) {
let authRoutes = null;
// Should there be a generic call to init on the extension manager?
customizationService.init(extensionManager);
// Use config to create routes
const appRoutes = createRoutes({
modes,
dataSources,
extensionManager,
servicesManager,
commandsManager,
hotkeysManager,
routerBasename,
showStudyList,
});
if (oidc) {
authRoutes = (
<OpenIdConnectRoutes
@ -115,9 +127,7 @@ App.propTypes = {
PropTypes.shape({
routerBasename: PropTypes.string.isRequired,
oidc: PropTypes.array,
whiteLabeling: PropTypes.shape({
createLogoComponentFn: PropTypes.func,
}),
whiteLabeling: PropTypes.object,
extensions: PropTypes.array,
}),
]).isRequired,

View File

@ -15,6 +15,7 @@ import {
CineService,
UserAuthenticationService,
errorHandler,
CustomizationServiceRegistration,
// utils,
} from '@ohif/core';
@ -50,6 +51,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
UIViewportDialogService,
MeasurementService,
DisplaySetService,
[CustomizationServiceRegistration, appConfig.customizationService],
ToolBarService,
ViewportGridService,
HangingProtocolService,

View File

@ -452,6 +452,7 @@ function _getViewportComponent(
}
}
console.log("Can't show displaySet", SOPClassHandlerId, displaySets[0]);
UINotificationService.show({
title: 'Viewport Not Supported Yet',
message: `Cannot display SOPClassId of ${displaySets[0].SOPClassUID} yet`,

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Icon } from '@ohif/ui';
// this is a debug component that is used to list various things that might
// be useful for debugging such as cross origin errors, etc.
function Debug() {
return (
<div style={{ width: '100%', height: '100%' }}>
<div className="h-screen w-screen flex justify-center items-center ">
<div className="py-8 px-8 mx-auto bg-secondary-dark drop-shadow-md space-y-2 rounded-lg">
<img
className="block mx-auto h-14"
src="./ohif-logo.svg"
alt="OHIF"
/>
<div className="text-center space-y-2 pt-4">
<div className="flex flex-col justify-center items-center">
<p className="text-xl text-primary-active font-semibold mt-4">
Debug Information
</p>
<div className="flex space-x-2 mt-4 items-center">
<p className="text-md text-white">
Cross Origin Isolated (COOP/COEP)
</p>
<Icon
name={
window.crossOriginIsolated
? 'notifications-success'
: 'notifications-error'
}
className="w-5 h-5"
/>
{!window.crossOriginIsolated && (
<div className="text-md text-white flex-1">
We use SharedArrayBuffer to render volume data (e.g., MPR).
If you are seeing this message, it means that your browser
has not enabled COOP/COEP. Please see the following link for
more information:{' '}
<a
href="https://web.dev/coop-coep/"
target="_blank"
rel="noopener noreferrer"
className="text-primary-active"
>
Learn More
</a>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Debug;

View File

@ -312,11 +312,22 @@ export default function ModeRoute({
});
return () => {
extensionManager.onModeExit();
mode?.onModeExit({ servicesManager, extensionManager });
// The mode.onModeExit must be done first to allow it to store
// information, and must be in a try/catch to ensure subscriptions
// are unsubscribed.
try {
mode?.onModeExit?.({ servicesManager, extensionManager });
} catch (e) {
console.warn('mode exit failure', e);
}
// The unsubscriptions must occur before the extension onModeExit
// in order to prevent exceptions during cleanup caused by spurious events
unsubscriptions.forEach(unsub => {
unsub();
});
// The extension manager must be called after the mode, this is
// expected to cleanup the state to a standard setup.
extensionManager.onModeExit();
};
}, [
mode,

View File

@ -2,15 +2,20 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { useAppConfig } from '@state';
const NotFound = ({
message = 'Sorry, this page does not exist.',
showGoBackButton = true,
}) => {
const [appConfig] = useAppConfig();
const { showStudyList } = appConfig;
return (
<div className="w-full h-full flex justify-center items-center text-white">
<div>
<h4>{message}</h4>
{showGoBackButton && (
{showGoBackButton && showStudyList && (
<h5>
<Link to={'/'}>Go back to the Study List</Link>
</h5>

View File

@ -6,11 +6,11 @@ import { ErrorBoundary } from '@ohif/ui';
import DataSourceWrapper from './DataSourceWrapper';
import WorkList from './WorkList';
import Local from './Local';
import Debug from './Debug';
import NotFound from './NotFound';
import buildModeRoutes from './buildModeRoutes';
import PrivateRoute from './PrivateRoute';
// TODO: Make these configurable
// TODO: Include "routes" debug route if dev build
const bakedInRoutes = [
// WORK LIST
@ -20,14 +20,25 @@ const bakedInRoutes = [
private: true,
props: { children: WorkList },
},
{
path: '/debug',
children: Debug,
},
{
path: '/local',
children: Local,
},
// NOT FOUND (404)
{ component: NotFound },
];
// NOT FOUND (404)
const notFoundRoute = { component: NotFound };
const WorkListRoute = {
path: '/',
children: DataSourceWrapper,
private: true,
props: { children: WorkList },
};
const createRoutes = ({
modes,
dataSources,
@ -36,6 +47,7 @@ const createRoutes = ({
commandsManager,
hotkeysManager,
routerBasename,
showStudyList,
}) => {
const routes =
buildModeRoutes({
@ -47,7 +59,18 @@ const createRoutes = ({
hotkeysManager,
}) || [];
const allRoutes = [...routes, ...bakedInRoutes];
const { customizationService } = servicesManager.services;
const customRoutes = customizationService.getGlobalCustomization(
'customRoutes'
);
const allRoutes = [
...routes,
...(showStudyList ? [WorkListRoute] : []),
...(customRoutes?.routes || []),
...bakedInRoutes,
customRoutes?.notFoundRoute || notFoundRoute,
];
function RouteWithErrorBoundary({ route, ...rest }) {
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@ -2383,28 +2383,28 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b"
integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg==
"@cornerstonejs/core@^0.21.4":
version "0.21.4"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.21.4.tgz#d1dab2c985c382495a8ec91a28f2d62872cdb350"
integrity sha512-8D8QyK5bjqxefSVr3vLzsp2MUprmABAbX4qpvpp1XRmC76i1dKKR8Yo9G9jdreR5xwKvJN7pY6/n1F2ZYSN48w==
"@cornerstonejs/core@^0.22.3":
version "0.22.3"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.22.3.tgz#57bf1482ce91d094342bf116636dea1335bff0b3"
integrity sha512-WqV6TnsTpsgezSj9SmnSgGCdY3u/EwYr6nKD8KnRQI8lqRTmMIKzbcIvPWAKJ/sU+A9Da6A5tweZvrhrcSorGg==
dependencies:
detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0"
"@cornerstonejs/streaming-image-volume-loader@^0.6.5":
version "0.6.5"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.6.5.tgz#f3f2ab362bf494e4072a146b45acedce0197fb36"
integrity sha512-dowoZYz0tMVtuUQl2WIZjOC/2TC0zMgas8KtpVcxJcGEu9HWA9oatgPlY0WqtFniDU7Oxnyy7OqYawtEoSSwdw==
"@cornerstonejs/streaming-image-volume-loader@^0.6.11":
version "0.6.11"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.6.11.tgz#53800b12d588300320aa479eef24f168a38826bd"
integrity sha512-UrgiWIsTSPFWEgCKxsKZUZ5hgir8ymdXes2UB8OT7yX6M4QeTmKi26nVNP21vebKMMPBootK//geSToqBoBs4Q==
dependencies:
"@cornerstonejs/core" "^0.21.4"
"@cornerstonejs/core" "^0.22.3"
cornerstone-wado-image-loader "^4.2.1"
"@cornerstonejs/tools@^0.29.6":
version "0.29.6"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.29.6.tgz#6cfcb6b56284c4d1265330993ecafa3f48c429b8"
integrity sha512-Rmz4Nx6IfH+31C7fsGxLWwQXQ+w1nN5WmILb9/N7WB4M5mYUZvYiopXdWII4mzh91nHXcEfEiJimaihyuCVLJQ==
"@cornerstonejs/tools@^0.30.6":
version "0.30.6"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.30.6.tgz#a6e5209f00fe00a919f20fe1387f06062b289a16"
integrity sha512-GEm2xWOlBwgS/3aZDBZObyu+KJESyOmkCo3hA+tellLKCVr207wBUUDndEmkNX6PJ/mR4o9Md6ZjMfNF7CAYiA==
dependencies:
"@cornerstonejs/core" "^0.21.4"
"@cornerstonejs/core" "^0.22.3"
lodash.clonedeep "4.5.0"
lodash.get "^4.4.2"
@ -15797,18 +15797,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-react@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/lottie-react/-/lottie-react-2.3.1.tgz#697060417bab027edb57ed45bda9d62e920d4040"
integrity sha512-8cxd6XZZtECT6LoAhCftRdYrEpHxiouvB5EPiYA+TtCG5LHNYAdMS9IVIHcxKtWnpo7x16QfCLj1XLXZpaN81A==
dependencies:
lottie-web "^5.9.4"
lottie-web@^5.9.4:
version "5.9.6"
resolved "https://registry.npmjs.org/lottie-web/-/lottie-web-5.9.6.tgz#62ae68563355d3e04aa75d53dec3dd4bea0e57c9"
integrity sha512-JFs7KsHwflugH5qIXBpB4905yC1Sub2MZWtl/elvO/QC6qj1ApqbUZJyjzJseJUtVpgiDaXQLjBlIJGS7UUUXA==
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"