feat: expose UiNotifications service (#1172)

* playing around

* Change extension init and preRegistration signature

* Clear test code

* Update core index test

* Fix test

* Tricking the bug

* Renaming file back to trick some weird bug
This commit is contained in:
Igor Octaviano 2019-11-13 17:02:14 -03:00 committed by Danny Brown
parent c59c5b3f14
commit 5c04e34c8f
13 changed files with 165 additions and 25 deletions

View File

@ -11,7 +11,7 @@ export default {
* LIFECYCLE HOOKS * LIFECYCLE HOOKS
*/ */
preRegistration(extensionConfiguration) {}, preRegistration({ serviceManager, configuration: extensionConfiguration }) {},
/** /**
* MODULE GETTERS * MODULE GETTERS

View File

@ -24,8 +24,8 @@ export default {
* @param {object} [configuration={}] * @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/ */
preRegistration(configuration = {}) { preRegistration({ serviceManager, configuration = {} }) {
init(configuration); init({ serviceManager, configuration });
}, },
getViewportModule() { getViewportModule() {
return OHIFCornerstoneViewport; return OHIFCornerstoneViewport;

View File

@ -4,16 +4,18 @@ import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js'; import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string'; import queryString from 'query-string';
function fallbackMetaDataProvider (type, imageId) { function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) { if (!imageId.includes('wado?requestType=WADO')) {
return return;
} }
// If you call for an WADO-URI imageId and get no // If you call for an WADO-URI imageId and get no
// metadata, try reformatting to WADO-RS imageId // metadata, try reformatting to WADO-RS imageId
const qs = queryString.parse(imageId); const qs = queryString.parse(imageId);
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${qs.seriesUID}/instances/${qs.objectUID}/frames/${qs.frame || 1}`; const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
qs.seriesUID
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
return cornerstone.metaData.get(type, wadoRsImageId); return cornerstone.metaData.get(type, wadoRsImageId);
} }
@ -21,13 +23,12 @@ function fallbackMetaDataProvider (type, imageId) {
// Add this fallback provider with a low priority so it is handled last // Add this fallback provider with a low priority so it is handled last
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1); cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
/** /**
* *
* @param {object} configuration * @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig * @param {Object|Array} configuration.csToolsConfig
*/ */
export default function init(configuration = {}) { export default function init({ serviceManager, configuration = {} }) {
const { csToolsConfig } = configuration; const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils; const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider(); const metadataProvider = new OHIF.cornerstone.MetadataProvider();
@ -97,6 +98,7 @@ export default function init(configuration = {}) {
]; ];
tools.forEach(tool => csTools.addTool(tool)); tools.forEach(tool => csTools.addTool(tool));
csTools.setToolActive('Pan', { mouseButtonMask: 4 }); csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });

View File

@ -2,12 +2,13 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js'; import log from './../log.js';
export default class ExtensionManager { export default class ExtensionManager {
constructor({ commandsManager }) { constructor({ commandsManager, servicesManager }) {
this.modules = {}; this.modules = {};
this.registeredExtensionIds = []; this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES); this.moduleTypeNames = Object.values(MODULE_TYPES);
// //
this._commandsManager = commandsManager; this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this.moduleTypeNames.forEach(moduleType => { this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = []; this.modules[moduleType] = [];
@ -66,7 +67,10 @@ export default class ExtensionManager {
// preRegistrationHook // preRegistrationHook
if (extension.preRegistration) { if (extension.preRegistration) {
extension.preRegistration(configuration); extension.preRegistration({
serviceManager: this._servicesManager,
configuration,
});
} }
// Register Modules // Register Modules

View File

@ -1,6 +1,7 @@
import './lib'; import './lib';
import { ExtensionManager, MODULE_TYPES } from './extensions'; import { ExtensionManager, MODULE_TYPES } from './extensions';
import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/'; import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb'; import DICOMWeb from './DICOMWeb';
@ -18,12 +19,15 @@ import ui from './ui';
import user from './user.js'; import user from './user.js';
import utils from './utils/'; import utils from './utils/';
import { createUiNotificationService } from './services';
const OHIF = { const OHIF = {
MODULE_TYPES, MODULE_TYPES,
// //
CommandsManager, CommandsManager,
ExtensionManager, ExtensionManager,
HotkeysManager, HotkeysManager,
ServicesManager,
// //
utils, utils,
studies, studies,
@ -41,6 +45,8 @@ const OHIF = {
viewer: {}, viewer: {},
measurements, measurements,
hangingProtocols, hangingProtocols,
//
createUiNotificationService,
}; };
export { export {
@ -49,6 +55,7 @@ export {
CommandsManager, CommandsManager,
ExtensionManager, ExtensionManager,
HotkeysManager, HotkeysManager,
ServicesManager,
// //
utils, utils,
studies, studies,
@ -65,6 +72,8 @@ export {
DICOMWeb, DICOMWeb,
measurements, measurements,
hangingProtocols, hangingProtocols,
//
createUiNotificationService,
}; };
export { OHIF }; export { OHIF };

View File

@ -8,6 +8,9 @@ describe('Top level exports', () => {
'CommandsManager', 'CommandsManager',
'ExtensionManager', 'ExtensionManager',
'HotkeysManager', 'HotkeysManager',
'ServicesManager',
//
'createUiNotificationService',
// //
'utils', 'utils',
'studies', 'studies',

View File

@ -0,0 +1,9 @@
export default class ServicesManager {
constructor() {
this.services = {};
}
register(service) {
this.services[service.name] = service;
}
}

View File

@ -0,0 +1,84 @@
/**
* A UI Notification
*
* @typedef {Object} Notification
* @property {string} title -
* @property {string} message -
* @property {number} [duration=5000] - in ms
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicApi = {
name: 'UINotificationService',
hide,
show,
setServiceImplementation,
};
const uiNotificationServiceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUiNotificationService() {
return uiNotificationServicePublicApi;
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
*
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
*/
function show({
title,
message,
duration = 5000,
position = 'bottomRight',
type = 'info',
autoClose = true,
}) {
return uiNotificationServiceImplementation._show({
title,
message,
duration,
position,
type,
autoClose,
});
}
/**
* Hides/dismisses the notification, if currently shown
*
* @param {number} id - id of the notification to hide/dismiss
* @returns undefined
*/
function hide(id) {
return uiNotificationServiceImplementation._hide({ id });
}
/**
*
*
* @param {*} {
* hide: hideImplementation,
* show: showImplementation,
* }
*/
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
}) {
if (hideImplementation) {
uiNotificationServiceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiNotificationServiceImplementation._show = showImplementation;
}
}
export default createUiNotificationService;

View File

@ -0,0 +1,4 @@
import ServicesManager from './ServicesManager.js';
import createUiNotificationService from './UINotificationService';
export { createUiNotificationService, ServicesManager };

View File

@ -1,4 +1,10 @@
import React, { useState, createContext, useContext } from 'react'; import React, {
useState,
createContext,
useContext,
useCallback,
useEffect,
} from 'react';
import SnackbarContainer from '../components/snackbar/SnackbarContainer'; import SnackbarContainer from '../components/snackbar/SnackbarContainer';
import SnackbarTypes from '../components/snackbar/SnackbarTypes'; import SnackbarTypes from '../components/snackbar/SnackbarTypes';
@ -6,7 +12,7 @@ const SnackbarContext = createContext(null);
export const useSnackbarContext = () => useContext(SnackbarContext); export const useSnackbarContext = () => useContext(SnackbarContext);
const SnackbarProvider = ({ children }) => { const SnackbarProvider = ({ children, service }) => {
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
title: '', title: '',
message: '', message: '',
@ -19,7 +25,11 @@ const SnackbarProvider = ({ children }) => {
const [count, setCount] = useState(1); const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]); const [snackbarItems, setSnackbarItems] = useState([]);
const show = options => { useEffect(() => {
service.setServiceImplementation({ hide, show });
}, [service, hide, show]);
const show = useCallback(options => {
if (!options || (!options.title && !options.message)) { if (!options || (!options.title && !options.message)) {
console.warn( console.warn(
'Snackbar cannot be rendered without required parameters: title | message' 'Snackbar cannot be rendered without required parameters: title | message'
@ -37,9 +47,9 @@ const SnackbarProvider = ({ children }) => {
setSnackbarItems(state => [...state, newItem]); setSnackbarItems(state => [...state, newItem]);
setCount(count + 1); setCount(count + 1);
}; });
const hide = id => { const hide = useCallback(id => {
const hideItem = items => { const hideItem = items => {
const newItems = items.map(item => { const newItems = items.map(item => {
if (item.id === id) { if (item.id === id) {
@ -57,7 +67,7 @@ const SnackbarProvider = ({ children }) => {
setTimeout(() => { setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]); setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000); }, 1000);
}; });
const hideAll = () => { const hideAll = () => {
// reset count // reset count

View File

@ -6,7 +6,9 @@ import './config';
import { import {
CommandsManager, CommandsManager,
ExtensionManager, ExtensionManager,
ServicesManager,
HotkeysManager, HotkeysManager,
createUiNotificationService,
utils, utils,
} from '@ohif/core'; } from '@ohif/core';
import React, { Component } from 'react'; import React, { Component } from 'react';
@ -41,9 +43,16 @@ const commandsManagerConfig = {
getActiveContexts: () => getActiveContexts(store.getState()), getActiveContexts: () => getActiveContexts(store.getState()),
}; };
// Services
const UINotificationService = createUiNotificationService();
const commandsManager = new CommandsManager(commandsManagerConfig); const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager); const hotkeysManager = new HotkeysManager(commandsManager);
const extensionManager = new ExtensionManager({ commandsManager }); const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// ~~~~ END APP SETUP // ~~~~ END APP SETUP
// TODO[react] Use a provider when the whole tree is React // TODO[react] Use a provider when the whole tree is React
@ -76,9 +85,11 @@ class App extends Component {
super(props); super(props);
this._appConfig = props; this._appConfig = props;
const { servers, extensions, hotkeys, oidc } = props; const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc); this.initUserManager(oidc);
_initServices([UINotificationService]);
_initExtensions(extensions, hotkeys); _initExtensions(extensions, hotkeys);
_initServers(servers); _initServers(servers);
initWebWorkers(); initWebWorkers();
@ -100,7 +111,7 @@ class App extends Component {
<UserManagerContext.Provider value={userManager}> <UserManagerContext.Provider value={userManager}>
<Router basename={routerBasename}> <Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}> <WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider> <SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}> <ModalProvider modal={OHIFModal}>
<OHIFStandaloneViewer userManager={userManager} /> <OHIFStandaloneViewer userManager={userManager} />
</ModalProvider> </ModalProvider>
@ -121,7 +132,7 @@ class App extends Component {
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<Router basename={routerBasename}> <Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}> <WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider> <SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}> <ModalProvider modal={OHIFModal}>
<OHIFStandaloneViewer /> <OHIFStandaloneViewer />
</ModalProvider> </ModalProvider>
@ -168,6 +179,10 @@ class App extends Component {
} }
} }
function _initServices(services) {
services.forEach(service => servicesManager.register(service));
}
/** /**
* @param * @param
*/ */
@ -214,4 +229,4 @@ function _makeAbsoluteIfNecessary(url, base_url) {
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App; const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
export default ExportedApp; export default ExportedApp;
export { commandsManager, extensionManager, hotkeysManager }; export { commandsManager, extensionManager, hotkeysManager, servicesManager };

View File

@ -7,8 +7,8 @@ export default {
*/ */
id: 'measurements-table', id: 'measurements-table',
preRegistration(configuration = {}) { preRegistration({ serviceManager, configuration = {} }) {
init(configuration); init({ serviceManager, configuration });
}, },
getPanelModule() { getPanelModule() {
return { return {

View File

@ -34,7 +34,7 @@ const MEASUREMENT_ACTION_MAP = {
* @export * @export
* @param {*} configuration * @param {*} configuration
*/ */
export default function init(configuration) { export default function init({ serviceManager, configuration = {} }) {
// If these tools were already added by a different extension, we want to replace // If these tools were already added by a different extension, we want to replace
// them with the same tools that have an alternative configuration. By passing in // them with the same tools that have an alternative configuration. By passing in
// our custom `getMeasurementLocationCallback`, we can... // our custom `getMeasurementLocationCallback`, we can...