Merge branch 'feat/v2-main' into feat/v2-main-docusaurus-netlify-monorepo

This commit is contained in:
Erik Ziegler committed 2021-07-08 10:41:49 +02:00
commit de412f2eef
61 files changed
+1979 -628

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
echo "Updating config..." echo "Updating config..."
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js # - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js sed -i -e "s+https://healthcare.googleapis.com/v1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
fi fi
# - Copy google.js to overwrite app-config.js # - Copy google.js to overwrite app-config.js
+1
View File
@@ -26,6 +26,7 @@
"autoFix": true "autoFix": true
} }
], ],
"jest.autoRun": "off",
"prettier.disableLanguages": ["html"], "prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf", "prettier.endOfLine": "lf",
"workbench.colorCustomizations": {}, "workbench.colorCustomizations": {},
+1 -1
View File
@@ -101,7 +101,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
), ),
}), }),
new UnusedFilesWebpackPlugin({ new UnusedFilesWebpackPlugin({
failOnUnused: true, failOnUnused: false,
globOptions: { globOptions: {
ignore: [ ignore: [
"node_modules/**/*", "node_modules/**/*",
+22 -13
View File
@@ -21,28 +21,37 @@
# Stage 1: Build the application # Stage 1: Build the application
# docker build -t ohif/viewer:latest . # docker build -t ohif/viewer:latest .
FROM node:14.3.0-slim as builder FROM node:14.3.0-slim as json-copier
RUN mkdir /usr/src/app RUN mkdir /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Copy Files
COPY .docker /usr/src/app/.docker COPY ["package.json", "yarn.lock", "./"]
COPY .webpack /usr/src/app/.webpack
COPY extensions /usr/src/app/extensions COPY extensions /usr/src/app/extensions
COPY modes /usr/src/app/modes COPY modes /usr/src/app/modes
COPY platform /usr/src/app/platform COPY platform /usr/src/app/platform
COPY .browserslistrc /usr/src/app/.browserslistrc
COPY aliases.config.js /usr/src/app/aliases.config.js # Find and remove non-package.json files
COPY babel.config.js /usr/src/app/babel.config.js RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
COPY lerna.json /usr/src/app/lerna.json RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
COPY package.json /usr/src/app/package.json RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
COPY postcss.config.js /usr/src/app/postcss.config.js
COPY yarn.lock /usr/src/app/yarn.lock # Copy Files
FROM node:14.3.0-slim as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY --from=json-copier /usr/src/app .
# Run the install before copying the rest of the files # Run the install before copying the rest of the files
RUN yarn config set workspaces-experimental true RUN yarn config set workspaces-experimental true
RUN yarn install RUN yarn install --frozen-lockfile
COPY . .
# To restore workspaces symlinks
RUN yarn install --frozen-lockfile
ENV PATH /usr/src/app/node_modules/.bin:$PATH ENV PATH /usr/src/app/node_modules/.bin:$PATH
ENV QUICK_BUILD true ENV QUICK_BUILD true
@@ -51,7 +60,7 @@ ENV QUICK_BUILD true
RUN yarn run build RUN yarn run build
# Stage 2: Bundle the built application into a Docker container # Stage 3: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux # which runs Nginx using Alpine Linux
FROM nginx:1.15.5-alpine FROM nginx:1.15.5-alpine
RUN apk add --no-cache bash RUN apk add --no-cache bash
+1 -1
View File
@@ -57,7 +57,7 @@ function ExampleContextProvider({ children }) {
return ( return (
<ExampleContext.Provider value={{ example: 'value' }}> <ExampleContext.Provider value={{ example: 'value' }}>
{children} {children}
</TrackedMeasurementsContext.Provider> </ExampleContext.Provider>
); );
} }
+7 -5
View File
@@ -63,9 +63,8 @@ All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
## Viewport Module ## Viewport Module
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport] Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport].
and is connected the redux store. This module is the most prone to change as we This module is the most prone to change as we hammer out our Viewport interface.
hammer out our Viewport interface.
## Tool Configuration ## Tool Configuration
@@ -87,10 +86,12 @@ Tools can be configured through extension configuration using the tools key:
## Annotate Tools Configuration ## Annotate Tools Configuration
*We currently support one property for annotation tools.* _We currently support one property for annotation tools._
### Hide handles ### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
This extension configuration allows you to toggle on/off handle rendering for
all annotate tools:
```js ```js
... ...
@@ -119,3 +120,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools [cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
[cornerstone]: https://github.com/cornerstonejs/cornerstone [cornerstone]: https://github.com/cornerstonejs/cornerstone
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
```
+4 -1
View File
@@ -5,7 +5,7 @@ import csTools from 'cornerstone-tools';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import getTools, { toolsGroupedByType } from './utils/getTools.js'; import getTools, { toolsGroupedByType } from './utils/getTools.js';
import initCornerstoneTools from './initCornerstoneTools.js'; import initCornerstoneTools from './initCornerstoneTools.js';
import './initWADOImageLoader.js'; import initWADOImageLoader from './initWADOImageLoader.js';
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById'; import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import { setEnabledElement } from './state'; import { setEnabledElement } from './state';
@@ -69,6 +69,7 @@ export default function init({
MeasurementService, MeasurementService,
DisplaySetService, DisplaySetService,
ToolBarService, ToolBarService,
UserAuthenticationService,
} = servicesManager.services; } = servicesManager.services;
const tools = getTools(); const tools = getTools();
@@ -275,6 +276,8 @@ export default function init({
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999); cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
initWADOImageLoader(UserAuthenticationService);
// ~~ // ~~
const defaultCsToolsConfig = csToolsConfig || { const defaultCsToolsConfig = csToolsConfig || {
globalToolSyncEnabled: false, // hold on to your pants! globalToolSyncEnabled: false, // hold on to your pants!
@@ -1,18 +1,45 @@
import cornerstone from 'cornerstone-core'; import cornerstone from 'cornerstone-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import dicomParser from 'dicom-parser'; import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core'
//import { initWebWorkers } from './utils/index.js'; let initialized = false;
cornerstoneWADOImageLoader.external.cornerstone = cornerstone; function initWebWorkers() {
cornerstoneWADOImageLoader.external.dicomParser = dicomParser; const config = {
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
startWebWorkersOnDemand: true,
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
usePDFJS: false,
strict: false,
},
},
};
cornerstoneWADOImageLoader.configure({ if (!initialized) {
beforeSend: function(xhr) { cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
/*const headers = OHIF.DICOMWeb.getAuthorizationHeader(); initialized = true;
}
}
if (headers.Authorization) { export default function initWADOImageLoader(UserAuthenticationService) {
xhr.setRequestHeader('Authorization', headers.Authorization); cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
}*/ cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
},
}); cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers && headers.Authorization) {
xhr.setRequestHeader('Authorization', headers.Authorization);
}
},
errorInterceptor: error => {
errorHandler.getHTTPErrorHandler(error)
},
});
initWebWorkers();
}
@@ -1,22 +0,0 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
};
export default setCornerstoneLayout;
@@ -7,7 +7,7 @@ import {
processSeriesResults, processSeriesResults,
} from './qido.js'; } from './qido.js';
import dcm4cheeReject from './dcm4cheeReject'; import dcm4cheeReject from './dcm4cheeReject';
import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler } from '@ohif/core';
import getImageId from './utils/getImageId'; import getImageId from './utils/getImageId';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
@@ -38,7 +38,7 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE) * @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking * @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
*/ */
function createDicomWebApi(dicomWebConfig) { function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
const { const {
qidoRoot, qidoRoot,
wadoRoot, wadoRoot,
@@ -46,15 +46,20 @@ function createDicomWebApi(dicomWebConfig) {
supportsFuzzyMatching, supportsFuzzyMatching,
supportsWildcard, supportsWildcard,
supportsReject, supportsReject,
requestOptions,
} = dicomWebConfig; } = dicomWebConfig;
const qidoConfig = { const qidoConfig = {
url: qidoRoot, url: qidoRoot,
// headers: DICOMWeb.getAuthorizationHeader(server), headers: UserAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
}; };
const wadoConfig = { const wadoConfig = {
url: wadoRoot, url: wadoRoot,
headers: UserAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
}; };
// TODO -> Two clients sucks, but its better than 1000. // TODO -> Two clients sucks, but its better than 1000.
@@ -78,7 +83,12 @@ function createDicomWebApi(dicomWebConfig) {
query: { query: {
studies: { studies: {
mapParams: mapParams.bind(), mapParams: mapParams.bind(),
search: async function (origParams) { search: async function(origParams) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, { mapParams(origParams, {
supportsFuzzyMatching, supportsFuzzyMatching,
@@ -98,7 +108,12 @@ function createDicomWebApi(dicomWebConfig) {
}, },
series: { series: {
// mapParams: mapParams.bind(), // mapParams: mapParams.bind(),
search: async function (studyInstanceUid) { search: async function(studyInstanceUid) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
const results = await seriesInStudy( const results = await seriesInStudy(
qidoDicomWebClient, qidoDicomWebClient,
studyInstanceUid studyInstanceUid
@@ -109,14 +124,20 @@ function createDicomWebApi(dicomWebConfig) {
// processResults: processResults.bind(), // processResults: processResults.bind(),
}, },
instances: { instances: {
search: (studyInstanceUid, queryParameters) => search: (studyInstanceUid, queryParameters) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
}
qidoSearch.call( qidoSearch.call(
undefined, undefined,
qidoDicomWebClient, qidoDicomWebClient,
studyInstanceUid, studyInstanceUid,
null, null,
queryParameters queryParameters
), );
},
}, },
}, },
retrieve: { retrieve: {
@@ -125,6 +146,11 @@ function createDicomWebApi(dicomWebConfig) {
// Conduct query, return a promise like others // Conduct query, return a promise like others
// Await this call and add to DicomMetadataStore after receiving result // Await this call and add to DicomMetadataStore after receiving result
metadata: (queryParams, callback) => { metadata: (queryParams, callback) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true); let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true);
StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs); StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs);
@@ -164,6 +190,11 @@ function createDicomWebApi(dicomWebConfig) {
}, },
store: { store: {
dicom: async dataset => { dicom: async dataset => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
const meta = { const meta = {
FileMetaInformationVersion: FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value, dataset._meta.FileMetaInformationVersion.Value,
@@ -196,6 +227,11 @@ function createDicomWebApi(dicomWebConfig) {
sortFunction, sortFunction,
madeInClient = false, madeInClient = false,
} = {}) => { } = {}) => {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
wadoDicomWebClient.headers = headers;
}
if (!StudyInstanceUID) { if (!StudyInstanceUID) {
throw new Error( throw new Error(
'Unable to query for SeriesMetadata without StudyInstanceUID' 'Unable to query for SeriesMetadata without StudyInstanceUID'
@@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource, dataSource,
StudyInstanceUID StudyInstanceUID
) { ) {
if (StudyInstanceUID === undefined) return; if (StudyInstanceUID === undefined) {
return;
}
// TODO: The `DicomMetadataStore` should short-circuit both of these requests // TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source // Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore? // We could also force this to "await" these values being available in the DICOMStore?
@@ -11,6 +11,8 @@ import {
} from '@ohif/ui'; } from '@ohif/ui';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { hotkeys } from '@ohif/core';
const { availableLanguages, defaultLanguage, currentLanguage } = i18n; const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
@@ -115,7 +117,11 @@ function ViewerLayout({
currentLanguage: currentLanguage(), currentLanguage: currentLanguage(),
availableLanguages, availableLanguages,
defaultLanguage, defaultLanguage,
onCancel: hide, onCancel: () => {
hotkeys.stopRecord();
hotkeys.unpause();
hide();
},
onSubmit: ({ hotkeyDefinitions, language }) => { onSubmit: ({ hotkeyDefinitions, language }) => {
i18n.changeLanguage(language.value); i18n.changeLanguage(language.value);
hotkeysManager.setHotkeys(hotkeyDefinitions); hotkeysManager.setHotkeys(hotkeyDefinitions);
@@ -1,37 +0,0 @@
import React, { useState } from 'react';
const HelloWorldContext = React.createContext({
message: 'HelloWorldContextTesting',
setMessage: () => { },
});
HelloWorldContext.displayName = 'HelloWorldContext';
function HelloWorldContextProvider({ children }) {
const [message, setMessage] = useState('HelloWorldContextTesting');
return (
<HelloWorldContext.Provider
value={{
message,
setMessage,
}}
>
{children}
</HelloWorldContext.Provider>
);
}
function getContextModule() {
return [
{
name: 'HelloWorldContext',
context: HelloWorldContext,
provider: HelloWorldContextProvider,
},
];
}
export { HelloWorldContext };
export default getContextModule;
-2
View File
@@ -1,4 +1,3 @@
import getContextModule from './getContextModule.js';
import getDataSourcesModule from './getDataSourcesModule.js'; import getDataSourcesModule from './getDataSourcesModule.js';
import getLayoutTemplateModule from './getLayoutTemplateModule.js'; import getLayoutTemplateModule from './getLayoutTemplateModule.js';
import getPanelModule from './getPanelModule.js'; import getPanelModule from './getPanelModule.js';
@@ -13,7 +12,6 @@ export default {
* Only required property. Should be a unique value across all extensions. * Only required property. Should be a unique value across all extensions.
*/ */
id, id,
getContextModule,
getDataSourcesModule, getDataSourcesModule,
getHangingProtocolModule, getHangingProtocolModule,
getLayoutTemplateModule, getLayoutTemplateModule,
@@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource, dataSource,
StudyInstanceUID StudyInstanceUID
) { ) {
if (StudyInstanceUID === undefined) return; if (StudyInstanceUID === undefined) {
return;
}
// TODO: The `DicomMetadataStore` should short-circuit both of these requests // TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source // Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore? // We could also force this to "await" these values being available in the DICOMStore?
+1
View File
@@ -41,6 +41,7 @@
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.6.0",
"isomorphic-base64": "^1.0.2", "isomorphic-base64": "^1.0.2",
"lodash.merge": "^4.6.1", "lodash.merge": "^4.6.1",
"lodash.clonedeep": "^4.5.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"mousetrap": "^1.6.3", "mousetrap": "^1.6.3",
"query-string": "^6.14.0", "query-string": "^6.14.0",
@@ -10,7 +10,7 @@ import user from '../user';
* @param {string|function} [server.requestOptions.auth] * @param {string|function} [server.requestOptions.auth]
* @returns {Object} { Authorization } * @returns {Object} { Authorization }
*/ */
export default function getAuthorizationHeader({ requestOptions } = {}) { export default function getAuthorizationHeader({ requestOptions } = {}, user) {
const headers = {}; const headers = {};
// Check for OHIF.user since this can also be run on the server // Check for OHIF.user since this can also be run on the server
@@ -64,11 +64,11 @@ describe('getAuthorizationHeader', () => {
it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => { it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => {
user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN'); user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN');
const authentication = getAuthorizationHeader({}); const authentication = getAuthorizationHeader({}, user);
const exptecteHeaderBasedOnUserAccessToekn = { const expectedHeaderBasedOnUserAccessToken = {
Authorization: 'Bearer MOCKED_TOKEN', Authorization: 'Bearer MOCKED_TOKEN',
}; };
expect(authentication).toEqual(exptecteHeaderBasedOnUserAccessToekn); expect(authentication).toEqual(expectedHeaderBasedOnUserAccessToken);
}); });
}); });
@@ -439,7 +439,7 @@ class MetadataProvider {
_getUIDsFromImageID(imageId) { _getUIDsFromImageID(imageId) {
if (imageId.includes('wadors:')) { if (imageId.includes('wadors:')) {
const strippedImageId = imageId.split('studies/')[1]; const strippedImageId = imageId.split('/studies/')[1];
const splitImageId = strippedImageId.split('/'); const splitImageId = strippedImageId.split('/');
return { return {
@@ -186,6 +186,7 @@ export default class ExtensionManager {
case MODULE_TYPES.SOP_CLASS_HANDLER: case MODULE_TYPES.SOP_CLASS_HANDLER:
case MODULE_TYPES.CONTEXT: case MODULE_TYPES.CONTEXT:
case MODULE_TYPES.LAYOUT_TEMPLATE: case MODULE_TYPES.LAYOUT_TEMPLATE:
case MODULE_TYPES.HANGING_PROTOCOL:
// Default for most extension points, // Default for most extension points,
// Just adds each entry ready for consumption by mode. // Just adds each entry ready for consumption by mode.
extensionModule.forEach(element => { extensionModule.forEach(element => {
@@ -194,13 +195,6 @@ export default class ExtensionManager {
] = element; ] = element;
}); });
break; break;
case MODULE_TYPES.HANGING_PROTOCOL:
extensionModule.forEach(element => {
this.modulesMap[
`${extensionId}.${moduleType}.${element.name}`
] = element;
});
break;
default: default:
throw new Error(`Module type invalid: ${moduleType}`); throw new Error(`Module type invalid: ${moduleType}`);
} }
@@ -277,13 +271,16 @@ export default class ExtensionManager {
}; };
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) { _initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
const { UserAuthenticationService } = this._servicesManager.services;
extensionModule.forEach(element => { extensionModule.forEach(element => {
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`; const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
dataSources.forEach(dataSource => { dataSources.forEach(dataSource => {
if (dataSource.namespace === namespace) { if (dataSource.namespace === namespace) {
const dataSourceInstance = element.createDataSource( const dataSourceInstance = element.createDataSource(
dataSource.configuration dataSource.configuration,
UserAuthenticationService
); );
if (this.dataSourceMap[dataSource.sourceName]) { if (this.dataSourceMap[dataSource.sourceName]) {
@@ -303,27 +300,9 @@ export default class ExtensionManager {
} }
_initHangingProtocolModule(extensionModule, extensionId) { _initHangingProtocolModule(extensionModule, extensionId) {
extensionModule.forEach(element => {
const namespace = `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`;
dataSources.forEach(dataSource => {
if (dataSource.namespace === namespace) {
const dataSourceInstance = element.createDataSource(
dataSource.configuration
);
if (this.dataSourceMap[dataSource.sourceName]) {
this.dataSourceMap[dataSource.sourceName].push(dataSourceInstance);
} else {
this.dataSourceMap[dataSource.sourceName] = [dataSourceInstance];
}
}
});
});
extensionModule.forEach(element => { extensionModule.forEach(element => {
this.modulesMap[ this.modulesMap[
`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}` `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`
] = element; ] = element;
}); });
} }
@@ -15,6 +15,10 @@ describe('ExtensionManager.js', () => {
}; };
servicesManager = { servicesManager = {
registerService: jest.fn(), registerService: jest.fn(),
services: {
// Required for DataSource Module initiation
UserAuthenticationService: jest.fn(),
},
}; };
appConfig = { appConfig = {
testing: true, testing: true,
@@ -100,7 +104,6 @@ describe('ExtensionManager.js', () => {
extensionManager.registerExtension(undefinedExtension); extensionManager.registerExtension(undefinedExtension);
}).toThrow('Attempting to register a null/undefined extension.'); }).toThrow('Attempting to register a null/undefined extension.');
expect(() => { expect(() => {
extensionManager.registerExtension(nullExtension); extensionManager.registerExtension(nullExtension);
}).toThrow('Attempting to register a null/undefined extension.'); }).toThrow('Attempting to register a null/undefined extension.');
@@ -185,7 +188,7 @@ describe('ExtensionManager.js', () => {
hotkeysManager: undefined, hotkeysManager: undefined,
appConfig, appConfig,
configuration: extensionConfiguration, configuration: extensionConfiguration,
extensionManager extensionManager,
}); });
} }
}); });
@@ -220,7 +223,7 @@ describe('ExtensionManager.js', () => {
}, },
getContextModule: () => { getContextModule: () => {
return [{}]; return [{}];
} },
}; };
extensionManager.registerExtension(extension); extensionManager.registerExtension(extension);
+3
View File
@@ -26,6 +26,7 @@ import {
ViewportGridService, ViewportGridService,
HangingProtocolService, HangingProtocolService,
pubSubServiceInterface, pubSubServiceInterface,
UserAuthenticationService,
} from './services'; } from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource'; import IWebApiDataSource from './DataSources/IWebApiDataSource';
@@ -66,6 +67,7 @@ const OHIF = {
ToolBarService, // TODO: TYPO ToolBarService, // TODO: TYPO
ViewportGridService, ViewportGridService,
HangingProtocolService, HangingProtocolService,
UserAuthenticationService,
IWebApiDataSource, IWebApiDataSource,
DicomMetadataStore, DicomMetadataStore,
pubSubServiceInterface, pubSubServiceInterface,
@@ -101,6 +103,7 @@ export {
ToolBarService, ToolBarService,
ViewportGridService, ViewportGridService,
HangingProtocolService, HangingProtocolService,
UserAuthenticationService,
IWebApiDataSource, IWebApiDataSource,
DicomMetadataStore, DicomMetadataStore,
pubSubServiceInterface, pubSubServiceInterface,
+2 -1
View File
@@ -34,9 +34,10 @@ describe('Top level exports', () => {
'ToolBarService', 'ToolBarService',
'ViewportGridService', 'ViewportGridService',
'HangingProtocolService', 'HangingProtocolService',
'UserAuthenticationService',
'IWebApiDataSource', 'IWebApiDataSource',
'DicomMetadataStore', 'DicomMetadataStore',
'pubSubServiceInterface' 'pubSubServiceInterface',
].sort(); ].sort();
const exports = Object.keys(OHIF).sort(); const exports = Object.keys(OHIF).sort();
@@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash.clonedeep';
import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy.js'; import sortBy from '../../utils/sortBy.js';
import ProtocolEngine from './ProtocolEngine'; import ProtocolEngine from './ProtocolEngine';
@@ -56,7 +56,7 @@ export default class ToolBarService {
*/ */
recordInteraction(interaction) { recordInteraction(interaction) {
const commandsManager = this._commandsManager; const commandsManager = this._commandsManager;
const { groupId, itemId, interactionType } = interaction; const { groupId, itemId, interactionType, commandName, commandOptions } = interaction;
switch (interactionType) { switch (interactionType) {
case 'action': { case 'action': {
@@ -66,7 +66,7 @@ export default class ToolBarService {
this.state.primaryToolId = itemId; this.state.primaryToolId = itemId;
// TODO: Force run this for all contexts? Even inactive? // TODO: Force run this for all contexts? Even inactive?
// or... They'll just detect primaryToolId when they spin up and apply... // or... They'll just detect primaryToolId when they spin up and apply...
commandsManager.runCommand('setToolActive', interaction.commandOptions); commandsManager.runCommand('setToolActive', commandOptions);
break; break;
} }
case 'toggle': { case 'toggle': {
@@ -74,7 +74,9 @@ export default class ToolBarService {
this.state.toggles[itemId] === undefined this.state.toggles[itemId] === undefined
? true ? true
: !this.state.toggles[itemId]; : !this.state.toggles[itemId];
interaction.commandOptions.toggledState = this.state.toggles[itemId]; if (commandOptions) {
commandOptions.toggledState = this.state.toggles[itemId];
}
break; break;
} }
default: default:
@@ -86,11 +88,8 @@ export default class ToolBarService {
// NOTE: Should probably just do this for tools as well? // NOTE: Should probably just do this for tools as well?
// But would be nice if we could enforce at least the command name? // But would be nice if we could enforce at least the command name?
let unsubscribe; let unsubscribe;
if (interaction.commandName) { if (commandName) {
unsubscribe = commandsManager.runCommand( unsubscribe = commandsManager.runCommand(commandName, commandOptions);
interaction.commandName,
interaction.commandOptions
);
} }
// Storing the unsubscribe for later reseting // Storing the unsubscribe for later reseting
@@ -0,0 +1,92 @@
const name = 'UserAuthenticationService';
const publicAPI = {
name,
getState: _getState,
setUser: _setUser,
getUser: _getUser,
getAuthorizationHeader: _getAuthorizationHeader,
handleUnauthenticated: _handleUnauthenticated,
setServiceImplementation,
reset: _reset,
set: _set,
};
const serviceImplementation = {
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
_setUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
_getUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
_getAuthorizationHeader: () =>
console.warn('_getAuthorizationHeader() NOT IMPLEMENTED'),
_handleUnauthenticated: () =>
console.warn('_handleUnauthenticated() NOT IMPLEMENTED'),
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
_set: () => console.warn('set() NOT IMPLEMENTED'),
};
function _getState() {
return serviceImplementation._getState();
}
function _setUser(user) {
return serviceImplementation._setUser(user);
}
function _getUser() {
return serviceImplementation._getUser();
}
function _getAuthorizationHeader() {
return serviceImplementation._getAuthorizationHeader();
}
function _handleUnauthenticated() {
return serviceImplementation._handleUnauthenticated();
}
function _set(state) {
return serviceImplementation._set(state);
}
function _reset() {
return serviceImplementation._reset({});
}
function setServiceImplementation({
getState: getStateImplementation,
setUser: setUserImplementation,
getUser: getUserImplementation,
getAuthorizationHeader: getAuthorizationHeaderImplementation,
handleUnauthenticated: handleUnauthenticatedImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
serviceImplementation._getState = getStateImplementation;
}
if (setUserImplementation) {
serviceImplementation._setUser = setUserImplementation;
}
if (getUserImplementation) {
serviceImplementation._getUser = getUserImplementation;
}
if (getAuthorizationHeaderImplementation) {
serviceImplementation._getAuthorizationHeader = getAuthorizationHeaderImplementation;
}
if (handleUnauthenticatedImplementation) {
serviceImplementation._handleUnauthenticated = handleUnauthenticatedImplementation;
}
if (resetImplementation) {
serviceImplementation._reset = resetImplementation;
}
if (setImplementation) {
serviceImplementation._set = setImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -0,0 +1,3 @@
import UserAuthenticationService from './UserAuthenticationService';
export default UserAuthenticationService;
+2
View File
@@ -11,6 +11,7 @@ import ViewportGridService from './ViewportGridService';
import CineService from './CineService'; import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService'; import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface from './_shared/pubSubServiceInterface'; import pubSubServiceInterface from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
export { export {
MeasurementService, MeasurementService,
@@ -26,4 +27,5 @@ export {
HangingProtocolService, HangingProtocolService,
CineService, CineService,
pubSubServiceInterface, pubSubServiceInterface,
UserAuthenticationService,
}; };
+37 -49
View File
@@ -1,41 +1,28 @@
"use strict";
/** /**
* This extension allows you to record a sequence using Mousetrap. * This extension allows you to record a sequence using Mousetrap.
* {@link https://craig.is/killing/mice}
* *
* @author Dan Tao <daniel.tao@gmail.com> * @author Dan Tao <daniel.tao@gmail.com>
*/ */
module.exports = function (Mousetrap, options) { export default function (Mousetrap) {
/**
* the default configurations, we merge with user options
*
* @type {Array}
*/
var _config = Object.assign({
timeout: 1000
}, options);
/** /**
* the sequence currently being recorded * the sequence currently being recorded
* *
* @type {Array} * @type {Array}
*/ */
var _recordedSequence = [], var _recordedSequence = [],
/** /**
* a callback to invoke after recording a sequence * a callback to invoke after recording a sequence
* *
* @type {Function|null} * @type {Function|null}
*/ */
_recordedSequenceCallback = null, _recordedSequenceCallback = null,
/** /**
* a list of all of the keys currently held down * a list of all of the keys currently held down
* *
* @type {Array} * @type {Array}
*/ */
_currentRecordedKeys = [], _currentRecordedKeys = [],
/** /**
* temporary state where we remember if we've already captured a * temporary state where we remember if we've already captured a
* character key in the current combo * character key in the current combo
@@ -43,28 +30,19 @@ module.exports = function (Mousetrap, options) {
* @type {boolean} * @type {boolean}
*/ */
_recordedCharacterKey = false, _recordedCharacterKey = false,
/** /**
* a handle for the timer of the current recording * a handle for the timer of the current recording
* *
* @type {null|number} * @type {null|number}
*/ */
_recordTimer = null, _recordTimer = null,
/** /**
* the original handleKey method to override when Mousetrap.record() is * the original handleKey method to override when Mousetrap.record() is
* called * called
* *
* @type {Function} * @type {Function}
*/ */
_origHandleKey = Mousetrap.prototype.handleKey, _origHandleKey = Mousetrap.prototype.handleKey;
/**
* the timeout that timer will wait for a key
*
* @type {number}
*/
_recordTimeout = _config.timeout;
/** /**
* handles a character key event * handles a character key event
@@ -83,19 +61,19 @@ module.exports = function (Mousetrap, options) {
} }
// remember this character if we're currently recording a sequence // remember this character if we're currently recording a sequence
if (e.type === 'keydown') { if (e.type == 'keydown') {
if (character.length === 1 && _recordedCharacterKey) { if (character.length === 1 && _recordedCharacterKey) {
_recordCurrentCombo(); _recordCurrentCombo();
} }
for (var i = 0; i < modifiers.length; ++i) { for (let i = 0; i < modifiers.length; ++i) {
_recordKey(modifiers[i]); _recordKey(modifiers[i]);
} }
_recordKey(character); _recordKey(character);
// once a key is released, all keys that were held down at the time // once a key is released, all keys that were held down at the time
// count as a keypress // count as a keypress
} else if (e.type === 'keyup' && _currentRecordedKeys.length > 0) { } else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) {
_recordCurrentCombo(); _recordCurrentCombo();
} }
} }
@@ -107,10 +85,8 @@ module.exports = function (Mousetrap, options) {
* @returns void * @returns void
*/ */
function _recordKey(key) { function _recordKey(key) {
var i;
// one-off implementation of Array.indexOf, since IE6-9 don't support it // one-off implementation of Array.indexOf, since IE6-9 don't support it
for (i = 0; i < _currentRecordedKeys.length; ++i) { for (let i = 0; i < _currentRecordedKeys.length; ++i) {
if (_currentRecordedKeys[i] === key) { if (_currentRecordedKeys[i] === key) {
return; return;
} }
@@ -133,7 +109,7 @@ module.exports = function (Mousetrap, options) {
_recordedSequence.push(_currentRecordedKeys); _recordedSequence.push(_currentRecordedKeys);
_currentRecordedKeys = []; _currentRecordedKeys = [];
_recordedCharacterKey = false; _recordedCharacterKey = false;
_restartRecordTimer(); _finishRecording();
} }
/** /**
@@ -146,10 +122,8 @@ module.exports = function (Mousetrap, options) {
* @returns void * @returns void
*/ */
function _normalizeSequence(sequence) { function _normalizeSequence(sequence) {
var i; for (let i = 0; i < sequence.length; ++i) {
sequence[i].sort(function (x, y) {
for (i = 0; i < sequence.length; ++i) {
sequence[i].sort(function(x, y) {
// modifier keys always come first, in alphabetical order // modifier keys always come first, in alphabetical order
if (x.length > 1 && y.length === 1) { if (x.length > 1 && y.length === 1) {
return -1; return -1;
@@ -194,7 +168,7 @@ module.exports = function (Mousetrap, options) {
*/ */
function _restartRecordTimer() { function _restartRecordTimer() {
clearTimeout(_recordTimer); clearTimeout(_recordTimer);
_recordTimer = setTimeout(_finishRecording, _recordTimeout); _recordTimer = setTimeout(_finishRecording, 1000);
} }
/** /**
@@ -204,27 +178,41 @@ module.exports = function (Mousetrap, options) {
* @param {Function} callback * @param {Function} callback
* @returns void * @returns void
*/ */
Mousetrap.prototype.record = function(callback, timeout) { Mousetrap.prototype.record = function (callback) {
var self = this; var self = this;
self.recording = true; self.recording = true;
_recordedSequenceCallback = function () {
// if the user doesn't want to change the timeout
// we still need to guarantee that it gets the default timeout
_recordTimeout = timeout || _config.timeout;
_recordedSequenceCallback = function() {
self.recording = false; self.recording = false;
callback.apply(self, arguments); callback.apply(self, arguments);
}; };
}; };
Mousetrap.prototype.handleKey = function() { /**
* stop recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.stopRecord = function () {
var self = this;
self.recording = false;
};
/**
* start recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.startRecording = function () {
var self = this;
self.recording = true;
};
Mousetrap.prototype.handleKey = function () {
var self = this; var self = this;
_handleKey.apply(self, arguments); _handleKey.apply(self, arguments);
}; };
Mousetrap.init(); Mousetrap.init();
}
return Mousetrap;
};
@@ -260,8 +260,7 @@ following resources helpful:
We chose to use a generic OpenID Connect library on the client, but it's worth We chose to use a generic OpenID Connect library on the client, but it's worth
noting that Keycloak comes packaged with its own: noting that Keycloak comes packaged with its own:
- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps - [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki)
[oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki))
- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) - [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
If you're not already drowning in links, here are some good security resources If you're not already drowning in links, here are some good security resources
+4 -1
View File
@@ -25,6 +25,9 @@ export {
ViewportGridContext, ViewportGridContext,
ViewportGridProvider, ViewportGridProvider,
useViewportGrid, useViewportGrid,
UserAuthenticationContext,
UserAuthenticationProvider,
useUserAuthentication,
} from './src/contextProviders'; } from './src/contextProviders';
/** COMPONENTS */ /** COMPONENTS */
@@ -92,7 +95,7 @@ export {
ViewportDownloadForm, ViewportDownloadForm,
ViewportGrid, ViewportGrid,
ViewportPane, ViewportPane,
WindowLevelMenuItem WindowLevelMenuItem,
} from './src/components'; } from './src/components';
/** VIEWS */ /** VIEWS */
+1 -1
View File
@@ -38,7 +38,7 @@
"react-dnd-html5-backend": "10.0.2", "react-dnd-html5-backend": "10.0.2",
"react-dom": "16.11.0", "react-dom": "16.11.0",
"react-draggable": "4.4.3", "react-draggable": "4.4.3",
"react-error-boundary": "2.2.x", "react-error-boundary": "^3.1.3",
"react-modal": "3.11.2", "react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0", "react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8", "react-select": "3.0.8",
+13 -61
View File
@@ -1,65 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="265" height="24" viewBox="0 0 265 24"> <?xml version="1.0" encoding="UTF-8"?>
<g fill="none" fill-rule="evenodd"> <svg width="250px" height="24px" viewBox="0 0 250 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g> <title>Artboard</title>
<path fill="#FFF" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01m7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82" transform="translate(1 1) translate(33.803 4.985)"/> <g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path stroke="#FFF" stroke-width=".75" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01zm7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82z" transform="translate(1 1) translate(33.803 4.985)"/> <g id="Group-6" transform="translate(1.000000, 0.000000)" fill="#FFFFFF" fill-rule="nonzero">
<path fill="#FFF" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967m.964 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193" transform="translate(1 1) translate(33.803 4.985)"/> <g id="Group-2" transform="translate(31.404013, 3.539799)">
<path stroke="#FFF" stroke-width=".75" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967zm.965 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193z" transform="translate(1 1) translate(33.803 4.985)"/> <path d="M5.304586,14.0730008 C7.89749931,14.0730008 9.77785629,12.0738844 9.77785629,8.86738093 C9.77785629,5.66087746 7.89749931,3.66176109 5.304586,3.66176109 C2.7116727,3.66176109 0.831315717,5.66087746 0.831315717,8.86738093 C0.831315717,12.0738844 2.7116727,14.0730008 5.304586,14.0730008 Z M5.304586,12.9249934 C3.46381549,12.9249934 2.0189096,11.4998807 2.0189096,8.86738093 C2.0189096,6.23488116 3.46381549,4.80976851 5.304586,4.80976851 C7.14535652,4.80976851 8.59026241,6.23488116 8.59026241,8.86738093 C8.59026241,11.4998807 7.14535652,12.9249934 5.304586,12.9249934 Z M12.8458173,16.7846735 L12.8458173,12.7666475 L12.9447834,12.7666475 C13.2020954,13.1823054 13.6969262,14.092794 15.1616253,14.092794 C17.0617755,14.092794 18.387922,12.5687152 18.387922,10.153941 C18.387922,7.75895998 17.0617755,6.23488116 15.1418321,6.23488116 C13.6573398,6.23488116 13.2020954,7.14536981 12.9447834,7.54123443 L12.8062308,7.54123443 L12.8062308,6.33384732 L11.6780166,6.33384732 L11.6780166,16.7846735 L12.8458173,16.7846735 Z M15.0032795,13.0437527 C13.5979601,13.0437527 12.826024,11.8561589 12.826024,10.1341477 C12.826024,8.43192984 13.5781668,7.28392243 15.0032795,7.28392243 C16.4877718,7.28392243 17.2201214,8.530896 17.2201214,10.1341477 C17.2201214,11.7571927 16.4679786,13.0437527 15.0032795,13.0437527 Z M23.3560312,14.092794 C24.8999033,14.092794 26.0281174,13.320858 26.3843956,12.1728506 L25.2561814,11.8561589 C24.959283,12.6478881 24.2714682,13.0437527 23.3560312,13.0437527 C21.9853499,13.0437527 21.0402231,12.1580056 20.9857918,10.5300124 L26.503155,10.5300124 L26.503155,10.0351816 C26.503155,7.2047495 24.8207303,6.23488116 23.2372718,6.23488116 C21.1787758,6.23488116 19.8130428,7.85792614 19.8130428,10.1935274 C19.8130428,12.5291287 21.1589825,14.092794 23.3560312,14.092794 Z M25.3155611,9.52055757 L20.9857918,9.52055757 C21.0649647,8.337912 21.9012287,7.28392243 23.2372718,7.28392243 C24.5040386,7.28392243 25.3155611,8.23399753 25.3155611,9.52055757 Z M29.4523542,13.9344482 L29.4523542,9.36221172 C29.4523542,8.03606522 30.2737733,7.28392243 31.3920909,7.28392243 C32.4757703,7.28392243 33.1338952,7.99153045 33.1338952,9.18407264 L33.1338952,13.9344482 L34.3016959,13.9344482 L34.3016959,9.10489971 C34.3016959,7.16516304 33.2674995,6.23488116 31.7285758,6.23488116 C30.5805684,6.23488116 29.8680121,6.74950518 29.5117339,7.5214412 L29.4127677,7.5214412 L29.4127677,6.33384732 L28.2845536,6.33384732 L28.2845536,13.9344482 L29.4523542,13.9344482 Z M41.7439624,13.9344482 L41.7439624,9.40179818 L47.1475145,9.40179818 L47.1475145,13.9344482 L48.3746949,13.9344482 L48.3746949,3.80031371 L47.1475145,3.80031371 L47.1475145,8.31317046 L41.7439624,8.31317046 L41.7439624,3.80031371 L40.516782,3.80031371 L40.516782,13.9344482 L41.7439624,13.9344482 Z M53.8574298,14.092794 C55.4013019,14.092794 56.529516,13.320858 56.8857942,12.1728506 L55.75758,11.8561589 C55.4606816,12.6478881 54.7728668,13.0437527 53.8574298,13.0437527 C52.4867485,13.0437527 51.5416217,12.1580056 51.4871904,10.5300124 L57.0045536,10.5300124 L57.0045536,10.0351816 C57.0045536,7.2047495 55.3221289,6.23488116 53.7386704,6.23488116 C51.6801744,6.23488116 50.3144414,7.85792614 50.3144414,10.1935274 C50.3144414,12.5291287 51.6603811,14.092794 53.8574298,14.092794 Z M55.8169597,9.52055757 L51.4871904,9.52055757 C51.5663633,8.337912 52.4026273,7.28392243 53.7386704,7.28392243 C55.0054372,7.28392243 55.8169597,8.23399753 55.8169597,9.52055757 Z M61.0225873,14.1125872 C62.3487338,14.1125872 63.0414969,13.4000309 63.2790157,12.9052001 L63.3383954,12.9052001 L63.3383954,13.9344482 L64.506196,13.9344482 L64.506196,8.92676063 C64.506196,6.5119864 62.6654255,6.23488116 61.6955572,6.23488116 C60.5475497,6.23488116 59.2411965,6.63074579 58.6473995,8.01627199 L59.7558205,8.41213661 C60.0131325,7.85792614 60.6217744,7.2641292 61.7351436,7.2641292 C62.8089264,7.2641292 63.3383954,7.8331846 63.3383954,8.80800124 L63.3383954,8.8475877 C63.3383954,9.4116948 62.7643917,9.36221172 61.3788655,9.5403508 C59.9685977,9.72343819 58.429674,10.0351816 58.429674,11.7769859 C58.429674,13.2614783 59.5776814,14.1125872 61.0225873,14.1125872 Z M61.2007264,13.063546 C60.2704445,13.063546 59.5974746,12.6478881 59.5974746,11.8363656 C59.5974746,10.9456702 60.4089971,10.668565 61.3194858,10.5498056 C61.8143166,10.4904259 63.1404631,10.3518733 63.3383954,10.1143545 L63.3383954,11.183189 C63.3383954,12.1332641 62.5862526,13.063546 61.2007264,13.063546 Z M67.8116731,13.9344482 L67.8116731,3.80031371 L66.6438725,3.80031371 L66.6438725,13.9344482 L67.8116731,13.9344482 Z M72.5224654,14.0334143 C72.9579165,14.0334143 73.2350217,13.9542414 73.3933675,13.8948617 L73.1558488,12.8458204 C73.0568826,12.8656137 72.8985368,12.9052001 72.6412248,12.9052001 C72.1266007,12.9052001 71.63177,12.7468543 71.63177,11.7571927 L71.63177,7.32350889 L73.2548149,7.32350889 L73.2548149,6.33384732 L71.63177,6.33384732 L71.63177,4.51287004 L70.4639693,4.51287004 L70.4639693,6.33384732 L69.3159619,6.33384732 L69.Line truncated
<path fill="#FFF" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428m1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/> </g>
<path stroke="#FFF" stroke-width=".75" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428zm1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/> <g id="Group-5" transform="translate(0.000000, -0.000000)">
<path fill="#FFF" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193" transform="translate(1 1) translate(33.803 4.985)"/> <path d="M21.2307692,0 L16.2352941,0 C14.7058902,0 13.4660633,1.23982685 13.4660633,2.76923077 L13.4660633,7.76470588 C13.4660633,9.2941098 14.7058902,10.5339367 16.2352941,10.5339367 L21.2307692,10.5339367 C22.7601732,10.5339367 24,9.2941098 24,7.76470588 L24,2.76923077 C24,1.23982685 22.7601732,0 21.2307692,0 Z M16.2352941,1.10769231 L21.2307692,1.10769231 C22.1484116,1.10769231 22.8923077,1.85158842 22.8923077,2.76923077 L22.8923077,7.76470588 C22.8923077,8.68234824 22.1484116,9.42624434 21.2307692,9.42624434 L16.2352941,9.42624434 C15.3176518,9.42624434 14.5737557,8.68234824 14.5737557,7.76470588 L14.5737557,2.76923077 C14.5737557,1.85158842 15.3176518,1.10769231 16.2352941,1.10769231 Z" id="Rectangle"></path>
<path stroke="#FFF" stroke-width=".75" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193z" transform="translate(1 1) translate(33.803 4.985)"/> <path d="M7.76470588,0 L2.76923077,0 C1.23982685,0 0,1.23982685 0,2.76923077 L0,7.76470588 C0,9.2941098 1.23982685,10.5339367 2.76923077,10.5339367 L7.76470588,10.5339367 C9.2941098,10.5339367 10.5339367,9.2941098 10.5339367,7.76470588 L10.5339367,2.76923077 C10.5339367,1.23982685 9.2941098,0 7.76470588,0 Z M2.76923077,1.10769231 L7.76470588,1.10769231 C8.68234824,1.10769231 9.42624434,1.85158842 9.42624434,2.76923077 L9.42624434,7.76470588 C9.42624434,8.68234824 8.68234824,9.42624434 7.76470588,9.42624434 L2.76923077,9.42624434 C1.85158842,9.42624434 1.10769231,8.68234824 1.10769231,7.76470588 L1.10769231,2.76923077 C1.10769231,1.85158842 1.85158842,1.10769231 2.76923077,1.10769231 Z" id="Rectangle"></path>
<path fill="#FFF" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193" transform="translate(1 1) translate(33.803 4.985)"/> <path d="M21.2307692,13.4660633 L16.2352941,13.4660633 C14.7058902,13.4660633 13.4660633,14.7058902 13.4660633,16.2352941 L13.4660633,21.2307692 C13.4660633,22.7601732 14.7058902,24 16.2352941,24 L21.2307692,24 C22.7601732,24 24,22.7601732 24,21.2307692 L24,16.2352941 C24,14.7058902 22.7601732,13.4660633 21.2307692,13.4660633 Z M16.2352941,14.5737557 L21.2307692,14.5737557 C22.1484116,14.5737557 22.8923077,15.3176518 22.8923077,16.2352941 L22.8923077,21.2307692 C22.8923077,22.1484116 22.1484116,22.8923077 21.2307692,22.8923077 L16.2352941,22.8923077 C15.3176518,22.8923077 14.5737557,22.1484116 14.5737557,21.2307692 L14.5737557,16.2352941 C14.5737557,15.3176518 15.3176518,14.5737557 16.2352941,14.5737557 Z" id="Rectangle"></path>
<path stroke="#FFF" stroke-width=".75" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193z" transform="translate(1 1) translate(33.803 4.985)"/> <path d="M7.76470588,13.4660633 L2.76923077,13.4660633 C1.23982685,13.4660633 0,14.7058902 0,16.2352941 L0,21.2307692 C0,22.7601732 1.23982685,24 2.76923077,24 L7.76470588,24 C9.2941098,24 10.5339367,22.7601732 10.5339367,21.2307692 L10.5339367,16.2352941 C10.5339367,14.7058902 9.2941098,13.4660633 7.76470588,13.4660633 Z M2.76923077,14.5737557 L7.76470588,14.5737557 C8.68234824,14.5737557 9.42624434,15.3176518 9.42624434,16.2352941 L9.42624434,21.2307692 C9.42624434,22.1484116 8.68234824,22.8923077 7.76470588,22.8923077 L2.76923077,22.8923077 C1.85158842,22.8923077 1.10769231,22.1484116 1.10769231,21.2307692 L1.10769231,16.2352941 C1.10769231,15.3176518 1.85158842,14.5737557 2.76923077,14.5737557 Z" id="Rectangle"></path>
<path fill="#FFF" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428m1.544 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/>
<path stroke="#FFF" stroke-width=".75" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428zm1.543 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/>
<path fill="#FFF" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
<path stroke="#FFF" stroke-width=".75" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
<path fill="#FFF" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232" transform="translate(1 1) translate(33.803 4.985)"/>
<path stroke="#FFF" stroke-width=".75" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232z" transform="translate(1 1) translate(33.803 4.985)"/>
<path fill="#FFF" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
<path stroke="#FFF" stroke-width=".75" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
<path fill="#FFF" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193" transform="translate(1 1) translate(33.803 4.985)"/>
<path stroke="#FFF" stroke-width=".75" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193z" transform="translate(1 1) translate(33.803 4.985)"/>
<g>
<path fill="#FFF" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89m2.122 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89zm2.123 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23m.81 9.223h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23zm.81 9.223h-2.739c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89m2.161 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89zm2.161 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.94 2.625c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.94 2.624c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891m6.367 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891zm6.368 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23m.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23zm.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.978 2.625c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.978 2.624c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path fill="#FFF" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
<path stroke="#FFF" stroke-width=".75" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
</g> </g>
</g>
<g fill="#FFF" fill-rule="nonzero">
<path d="M22.85 21.43c0 .713-.58 1.294-1.293 1.294h-8.308c-.713 0-1.293-.58-1.293-1.293v-8.308c0-.713.58-1.293 1.293-1.293h8.308c.712 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273h-8.24v8.24h8.24v-8.24zM22.85 8.937c0 .713-.58 1.293-1.293 1.293h-8.308c-.713 0-1.293-.58-1.293-1.293V.63c0-.712.58-1.292 1.293-1.292h8.308c.712 0 1.293.58 1.293 1.292v8.308zM21.523.663h-8.24v8.24h8.24V.663zM10.23 21.43c0 .713-.58 1.294-1.293 1.294H.63c-.712 0-1.292-.58-1.292-1.293v-8.308c0-.713.58-1.293 1.292-1.293h8.308c.713 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273H.663v8.24h8.24v-8.24zM10.23 8.937c0 .713-.58 1.293-1.293 1.293H.63c-.712 0-1.292-.58-1.292-1.293V.63c0-.712.58-1.292 1.292-1.292h8.308c.713 0 1.293.58 1.293 1.292v8.308zm-1.327-.034V.663H.663v8.24h8.24z" transform="translate(1 1)"/>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+3 -3
View File
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
// TODO: This may fail if package is split from PWA build // TODO: This may fail if package is split from PWA build
import { useHistory } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui'; import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) { function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
const { t } = useTranslation('Header'); const { t } = useTranslation('Header');
const history = useHistory(); const navigate = useNavigate();
// TODO: this should be passed in as a prop instead and the react-router-dom // TODO: this should be passed in as a prop instead and the react-router-dom
// dependency should be dropped // dependency should be dropped
const onReturnHandler = () => { const onReturnHandler = () => {
if (isReturnEnabled) { if (isReturnEnabled) {
history.push('/'); navigate('/');
} }
}; };
@@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
//import { hotkeys } from '@ohif/core'; import { hotkeys } from '@ohif/core';
import Input from '../Input'; import Input from '../Input';
//import { getKeys, formatKeysForInput } from './utils'; import { getKeys, formatKeysForInput } from './utils';
import { formatKeysForInput } from './utils';
/** /**
* HotkeyField * HotkeyField
@@ -25,16 +27,16 @@ const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
/*hotkeys.record(sequence => { hotkeys.record(sequence => {
const keys = getKeys({ sequence, modifierKeys }); const keys = getKeys({ sequence, modifierKeys });
hotkeys.unpause(); hotkeys.unpause();
onChange(keys); onChange(keys);
});*/ });
}; };
const onFocus = () => { const onFocus = () => {
/*hotkeys.pause(); hotkeys.pause();
hotkeys.startRecording();*/ hotkeys.startRecording();
}; };
return ( return (
@@ -35,9 +35,10 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
setErrors(prevState => { setErrors(prevState => {
const errors = { ...prevState, [id]: error }; const errors = { ...prevState, [id]: error };
onChange(id, definition, errors);
return errors; return errors;
}); });
onChange(id, definition, { ...errors, [id]: error });
}; };
return ( return (
@@ -84,7 +85,7 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
onChange={onChangeHandler} onChange={onChangeHandler}
className='text-lg h-8' className='text-lg h-8'
/> />
{error && <span className='p-2 text-red-600 text-sm'>{error}</span>} {error && <span className='p-2 text-left text-red-600 text-sm'>{error}</span>}
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,147 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
const DEFAULT_STATE = {
user: null,
enabled: false,
};
export const UserAuthenticationContext = createContext(DEFAULT_STATE);
export function UserAuthenticationProvider({ children, service }) {
const userAuthenticationReducer = (state, action) => {
switch (action.type) {
case 'SET_USER': {
return {
...state,
...{ user: action.payload.user },
};
}
case 'RESET': {
return {
user: null,
};
}
case 'SET': {
return {
...state,
...action.payload,
};
}
default:
return action.payload;
}
};
const [userAuthenticationState, dispatch] = useReducer(
userAuthenticationReducer,
DEFAULT_STATE
);
const getState = useCallback(() => userAuthenticationState, [
userAuthenticationState,
]);
const setUser = useCallback(
user =>
dispatch({
type: 'SET_USER',
payload: {
user,
},
}),
[dispatch]
);
const getUser = useCallback(() => userAuthenticationState.user, [
userAuthenticationState,
]);
const reset = useCallback(
() =>
dispatch({
type: 'RESET',
payload: {},
}),
[dispatch]
);
const set = useCallback(
payload =>
dispatch({
type: 'SET',
payload,
}),
[dispatch]
);
/**
* Sets the implementation of the UserAuthenticationService that can be used by extensions.
*
* @returns void
*/
// TODO: should this be a useEffect or not?
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setUser,
getUser,
reset,
set,
});
}
}, [getState, service, setUser, getUser, reset, set]);
// TODO: This may not be correct, but I think we need to set the implementation for the service
// immediately when this runs, since otherwise the authentication redirects will fail.
// (useEffect only runs after the child components - in this case, routing logic - has failed)
if (service) {
service.setServiceImplementation({
getState,
setUser,
getUser,
reset,
set,
});
}
const api = {
getState,
setUser,
getUser,
getAuthorizationHeader: service.getAuthorizationHeader,
handleUnauthenticated: service.handleUnauthenticated,
reset,
set,
};
return (
<UserAuthenticationContext.Provider value={[userAuthenticationState, api]}>
{children}
</UserAuthenticationContext.Provider>
);
}
export default UserAuthenticationProvider;
const UserAuthenticationConsumer = UserAuthenticationContext.Consumer;
export { UserAuthenticationConsumer };
UserAuthenticationProvider.propTypes = {
children: PropTypes.any,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}).isRequired,
};
export const useUserAuthentication = () =>
useContext(UserAuthenticationContext);
+9 -7
View File
@@ -2,7 +2,7 @@ export {
default as DialogProvider, default as DialogProvider,
useDialog, useDialog,
withDialog, withDialog,
} from './DialogProvider' } from './DialogProvider';
export { default as DragAndDropProvider } from './DragAndDropProvider'; export { default as DragAndDropProvider } from './DragAndDropProvider';
@@ -19,17 +19,13 @@ export {
useImageViewer, useImageViewer,
} from './ImageViewerProvider'; } from './ImageViewerProvider';
export { export { CineContext, default as CineProvider, useCine } from './CineProvider';
CineContext,
default as CineProvider,
useCine,
} from './CineProvider';
export { export {
default as SnackbarProvider, default as SnackbarProvider,
useSnackbar, useSnackbar,
withSnackbar, withSnackbar,
} from './SnackbarProvider' } from './SnackbarProvider';
export { export {
default as ViewportDialogProvider, default as ViewportDialogProvider,
@@ -41,3 +37,9 @@ export {
ViewportGridProvider, ViewportGridProvider,
useViewportGrid, useViewportGrid,
} from './ViewportGridProvider'; } from './ViewportGridProvider';
export {
UserAuthenticationContext,
UserAuthenticationProvider,
useUserAuthentication,
} from './UserAuthenticationProvider';
+4
View File
@@ -110,6 +110,10 @@ module.exports = (env, argv) => {
historyApiFallback: { historyApiFallback: {
disableDotRule: true, disableDotRule: true,
}, },
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
}, },
}); });
+4
View File
@@ -40,3 +40,7 @@
no-cache, no-cache,
no-store, no-store,
must-revalidate''' must-revalidate'''
# COMMENT: For sharedArrayBuffer, see https://developer.chrome.com/blog/enabling-shared-array-buffer/
Cross-Origin-Embedder-Policy = "require-corp"
Cross-Origin-Opener-Policy = "same-origin"
+4 -2
View File
@@ -62,17 +62,19 @@
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.7.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"history": "5.0.0",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"oidc-client": "1.11.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.12.1", "query-string": "^6.12.1",
"react-dropzone": "^10.1.7", "react-dropzone": "^10.1.7",
"react-i18next": "^10.11.0", "react-i18next": "^10.11.0",
"react-resize-detector": "^4.2.0", "react-resize-detector": "^4.2.0",
"react-router": "^5.2.0", "react-router": "next",
"react-router-dom": "^5.2.0" "react-router-dom": "next"
}, },
"devDependencies": { "devDependencies": {
"@percy/cypress": "^2.3.0", "@percy/cypress": "^2.3.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+7
View File
@@ -39,6 +39,13 @@ window.config = {
configuration: {}, configuration: {},
}, },
], ],
httpErrorHandler: error => {
// This is 429 when rejected from the public idc sandbox too often.
console.warn(error.status);
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
// whiteLabeling: { // whiteLabeling: {
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
// createLogoComponentFn: function (React) { // createLogoComponentFn: function (React) {
+42 -31
View File
@@ -3,7 +3,9 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Router } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import Compose from './routes/Mode/Compose.js';
import { import {
DialogProvider, DialogProvider,
Modal, Modal,
@@ -13,22 +15,18 @@ import {
ViewportDialogProvider, ViewportDialogProvider,
ViewportGridProvider, ViewportGridProvider,
CineProvider, CineProvider,
UserAuthenticationProvider,
} from '@ohif/ui'; } from '@ohif/ui';
// Viewer Project // Viewer Project
// TODO: Should this influence study list? // TODO: Should this influence study list?
import { AppConfigProvider } from '@state'; import { AppConfigProvider } from '@state';
import createRoutes from './routes'; import createRoutes from './routes';
import appInit from './appInit.js'; import appInit from './appInit.js';
import history from './history' import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes.jsx';
// TODO: Temporarily for testing // TODO: Temporarily for testing
import '@ohif/mode-longitudinal'; import '@ohif/mode-longitudinal';
/**
* ENV Variable to determine routing behavior
*/
const OHIFRouter = Router
let commandsManager, extensionManager, servicesManager, hotkeysManager; let commandsManager, extensionManager, servicesManager, hotkeysManager;
function App({ config, defaultExtensions }) { function App({ config, defaultExtensions }) {
@@ -42,7 +40,8 @@ function App({ config, defaultExtensions }) {
// Set appConfig // Set appConfig
const appConfigState = init.appConfig; const appConfigState = init.appConfig;
const { routerBasename, modes, dataSources } = appConfigState; const { routerBasename, modes, dataSources, oidc } = appConfigState;
// Use config to create routes // Use config to create routes
const appRoutes = createRoutes({ const appRoutes = createRoutes({
modes, modes,
@@ -50,38 +49,50 @@ function App({ config, defaultExtensions }) {
extensionManager, extensionManager,
servicesManager, servicesManager,
hotkeysManager, hotkeysManager,
routerBasename,
}); });
const { const {
UIDialogService, UIDialogService,
UIModalService, UIModalService,
UINotificationService, UINotificationService,
UIViewportDialogService, UIViewportDialogService,
ViewportGridService, // TODO: Should this be a "UI" Service? ViewportGridService,
CineService CineService,
UserAuthenticationService,
} = servicesManager.services; } = servicesManager.services;
const providers = [
[AppConfigProvider, { value: appConfigState }],
[UserAuthenticationProvider, { service: UserAuthenticationService}],
[I18nextProvider, { i18n }],
[ThemeWrapper],
[ViewportGridProvider, {service: ViewportGridService}],
[ViewportDialogProvider, {service: UIViewportDialogService}],
[CineProvider, {service: CineService}],
[SnackbarProvider, {service: UINotificationService}],
[DialogProvider, {service: UIDialogService}],
[ModalProvider, {service: UIModalService, modal: Modal}],
]
const CombinedProviders = ({ children }) =>
Compose({ components: providers, children });
let authRoutes = null;
if (oidc) {
authRoutes = (<OpenIdConnectRoutes
oidc={oidc}
routerBasename={routerBasename}
UserAuthenticationService={UserAuthenticationService}
/>)
}
return ( return (
<AppConfigProvider value={appConfigState}> <CombinedProviders>
<I18nextProvider i18n={i18n}> <BrowserRouter>
<OHIFRouter basename={routerBasename} history={history}> {authRoutes}
<ThemeWrapper> {appRoutes}
<ViewportGridProvider service={ViewportGridService}> </BrowserRouter>
<ViewportDialogProvider service={UIViewportDialogService}> </CombinedProviders>
<CineProvider service={CineService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</CineProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
</ThemeWrapper>
</OHIFRouter>
</I18nextProvider>
</AppConfigProvider>
); );
} }
+13 -3
View File
@@ -13,11 +13,11 @@ import {
ViewportGridService, ViewportGridService,
HangingProtocolService, HangingProtocolService,
CineService, CineService,
UserAuthenticationService,
errorHandler
// utils, // utils,
} from '@ohif/core'; } from '@ohif/core';
// TODO -> this feels bad.
/** /**
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration * @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
* @param {object[]} defaultExtensions - array of extension objects * @param {object[]} defaultExtensions - array of extension objects
@@ -60,8 +60,15 @@ function appInit(appConfigOrFunc, defaultExtensions) {
ViewportGridService, ViewportGridService,
HangingProtocolService, HangingProtocolService,
CineService, CineService,
UserAuthenticationService,
]); ]);
errorHandler.getHTTPErrorHandler = () => {
if (typeof appConfig.httpErrorHandler === 'function') {
return appConfig.httpErrorHandler;
}
};
/** /**
* Example: [ext1, ext2, ext3] * Example: [ext1, ext2, ext3]
* Example2: [[ext1, config], ext2, [ext3, config]] * Example2: [[ext1, config], ext2, [ext3, config]]
@@ -75,13 +82,16 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// TODO: We no longer init webWorkers at app level // TODO: We no longer init webWorkers at app level
// TODO: We no longer init the user Manager // TODO: We no longer init the user Manager
if (!appConfig.modes) {
throw new Error('No modes are defined! Check your app-config.js');
}
// TODO: Remove this // TODO: Remove this
if (!appConfig.modes.length) { if (!appConfig.modes.length) {
appConfig.modes.push(window.longitudinalMode); appConfig.modes.push(window.longitudinalMode);
// appConfig.modes.push(window.segmentationMode); // appConfig.modes.push(window.segmentationMode);
} }
return { return {
appConfig, appConfig,
commandsManager, commandsManager,
+1
View File
@@ -1,3 +1,4 @@
// TODO: replace with HistoryRouter https://github.com/ReactTraining/react-router/pull/7586
import { createBrowserHistory, createHashHistory } from 'history'; import { createBrowserHistory, createHashHistory } from 'history';
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER); const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
const router = useHashRouting ? createHashHistory() : createBrowserHistory(); const router = useHashRouting ? createHashHistory() : createBrowserHistory();
@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
function CallbackPage({ userManager, onRedirectSuccess }) {
const onRedirectError = error => {
throw new Error(error);
};
userManager
.signinRedirectCallback()
.then(user => onRedirectSuccess(user))
.catch(error => onRedirectError(error));
// todo: add i18n (or return null?)
return <div>Redirecting...</div>;
}
CallbackPage.propTypes = {
userManager: PropTypes.object.isRequired,
};
export default CallbackPage;
@@ -3,8 +3,8 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MODULE_TYPES } from '@ohif/core'; import { MODULE_TYPES } from '@ohif/core';
// //
import { useAppConfig } from '@state';
import { extensionManager } from '../App.jsx'; import { extensionManager } from '../App.jsx';
import { useParams, useLocation } from 'react-router';
/** /**
* Uses route properties to determine the data source that should be passed * Uses route properties to determine the data source that should be passed
@@ -15,8 +15,10 @@ import { extensionManager } from '../App.jsx';
* @param {function} props.children - Layout Template React Component * @param {function} props.children - Layout Template React Component
*/ */
function DataSourceWrapper(props) { function DataSourceWrapper(props) {
const [appConfig] = useAppConfig(); const { children: LayoutTemplate, ...rest } = props;
const { children: LayoutTemplate, history, ...rest } = props; const params = useParams();
const location = useLocation();
// TODO: Fetch by type, name, etc? // TODO: Fetch by type, name, etc?
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE]; const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE];
// TODO: Good usecase for flatmap? // TODO: Good usecase for flatmap?
@@ -30,13 +32,10 @@ function DataSourceWrapper(props) {
return acc.concat(mods); return acc.concat(mods);
}, []); }, []);
// Grabbing first for now. This isn't hydrated yet, but we should // Grabbing first for now - should get active?
// hydrate it somewhere based on config... const name = webApiDataSources[0].name;
// ~ default.js // TODO: Why does this return an array?
const firstAppConfigDataSource = appConfig.dataSources[0]; const dataSource = extensionManager.getDataSources(name)[0]
const dataSourceConfig = firstAppConfigDataSource.configuration;
const firstWebApiDataSource = webApiDataSources[0];
const dataSource = firstWebApiDataSource.createDataSource(dataSourceConfig);
// Route props --> studies.mapParams // Route props --> studies.mapParams
// mapParams --> studies.search // mapParams --> studies.search
@@ -55,19 +54,18 @@ function DataSourceWrapper(props) {
useEffect(() => { useEffect(() => {
const queryFilterValues = _getQueryFilterValues( const queryFilterValues = _getQueryFilterValues(
history.location.search, location.search,
STUDIES_LIMIT STUDIES_LIMIT
); );
// 204: no content // 204: no content
async function getData() { async function getData() {
setIsLoading(true); setIsLoading(true);
const studies = await dataSource.query.studies.search(queryFilterValues); const studies = await dataSource.query.studies.search(queryFilterValues);
setIsLoading(false); setIsLoading(false);
setData({ setData({
studies, studies: studies || [],
total: studies.length, total: studies.length,
resultsPerPage: queryFilterValues.resultsPerPage, resultsPerPage: queryFilterValues.resultsPerPage,
pageNumber: queryFilterValues.pageNumber, pageNumber: queryFilterValues.pageNumber,
@@ -99,14 +97,13 @@ function DataSourceWrapper(props) {
console.warn(ex); console.warn(ex);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [history.location.search]); }, [location, params]);
// queryFilterValues // queryFilterValues
// TODO: Better way to pass DataSource? // TODO: Better way to pass DataSource?
return ( return (
<LayoutTemplate <LayoutTemplate
{...rest} {...rest}
history={history}
data={data.studies} data={data.studies}
dataTotal={data.total} dataTotal={data.total}
dataSource={dataSource} dataSource={dataSource}
+5 -11
View File
@@ -1,8 +1,8 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { useNavigate } from 'react-router-dom';
import { MODULE_TYPES } from '@ohif/core' import { MODULE_TYPES } from '@ohif/core'
import PropTypes from 'prop-types'
import Dropzone from 'react-dropzone' import Dropzone from 'react-dropzone'
import filesToStudies from './filesToStudies' import filesToStudies from './filesToStudies'
@@ -39,8 +39,8 @@ const getLoadButton = (onDrop, text, isDir) => {
</Dropzone>) </Dropzone>)
} }
function Local(props) { function Local() {
const { history } = props const navigate = useNavigate();
const dropzoneRef = useRef() const dropzoneRef = useRef()
// Initializing the dicom local dataSource // Initializing the dicom local dataSource
@@ -61,7 +61,7 @@ function Local(props) {
const onDrop = async (acceptedFiles) => { const onDrop = async (acceptedFiles) => {
const studies = await filesToStudies(acceptedFiles, dataSource) const studies = await filesToStudies(acceptedFiles, dataSource)
// Todo: navigate to work list and let user select a mode // Todo: navigate to work list and let user select a mode
history.push(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`) navigate(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`)
} }
// Set body style // Set body style
@@ -74,7 +74,7 @@ function Local(props) {
return ( return (
<Dropzone ref={dropzoneRef} onDrop={onDrop} noClick> <Dropzone ref={dropzoneRef} onDrop={onDrop} noClick>
{({ getRootProps, getInputProps }) => ( {({ getRootProps }) => (
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}> <div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
<div className="h-screen w-screen flex justify-center items-center "> <div className="h-screen w-screen flex justify-center items-center ">
<div className="py-8 px-8 mx-auto bg-secondary-dark shadow-md space-y-2 rounded-lg"> <div className="py-8 px-8 mx-auto bg-secondary-dark shadow-md space-y-2 rounded-lg">
@@ -107,10 +107,4 @@ function Local(props) {
) )
} }
Local.propTypes = {
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
};
export default Local export default Local
+8 -4
View File
@@ -10,14 +10,18 @@ export default function Compose(props) {
const { components = [], children } = props; const { components = [], children } = props;
return ( return (
<> <React.Fragment>
{components.reduceRight((acc, Comp) => { {components.reduceRight((acc, curr) => {
return <Comp>{acc}</Comp>; const [Comp, props] = Array.isArray(curr)
? [curr[0], curr[1]]
: [curr, {}];
return <Comp {...props}>{acc}</Comp>;
}, children)} }, children)}
</> </React.Fragment>
); );
} }
// https://juliuskoronci.medium.com/avoid-a-long-list-of-react-providers-c45a269d80c1
Compose.propTypes = { Compose.propTypes = {
components: PropTypes.array, components: PropTypes.array,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
+10 -10
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router'; import { useParams, useLocation } from 'react-router';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected? // TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore } from '@ohif/core'; import { DicomMetadataStore } from '@ohif/core';
@@ -54,7 +54,6 @@ async function defaultRouteInit({
} }
export default function ModeRoute({ export default function ModeRoute({
location,
mode, mode,
dataSourceName, dataSourceName,
extensionManager, extensionManager,
@@ -62,6 +61,7 @@ export default function ModeRoute({
hotkeysManager, hotkeysManager,
}) { }) {
// Parse route params/querystring // Parse route params/querystring
const location = useLocation();
const query = useQuery(); const query = useQuery();
const params = useParams(); const params = useParams();
@@ -80,7 +80,9 @@ export default function ModeRoute({
const { const {
DisplaySetService, DisplaySetService,
HangingProtocolService, HangingProtocolService,
UserAuthenticationService,
} = servicesManager.services; } = servicesManager.services;
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode; const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
if (dataSourceName === undefined) { if (dataSourceName === undefined) {
@@ -96,6 +98,12 @@ export default function ModeRoute({
// Only handling one route per mode for now // Only handling one route per mode for now
const route = mode.routes[0]; const route = mode.routes[0];
const layoutTemplateRouteData = route.layoutTemplate({ location });
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateRouteData.id
);
const LayoutComponent = layoutTemplateModuleEntry.component;
// For each extension, look up their context modules // For each extension, look up their context modules
// TODO: move to extension manager. // TODO: move to extension manager.
let contextModules = []; let contextModules = [];
@@ -279,14 +287,6 @@ export default function ModeRoute({
} }
ModeRoute.propTypes = { ModeRoute.propTypes = {
// Ref: https://reacttraining.com/react-router/web/api/location
location: PropTypes.shape({
key: PropTypes.string,
pathname: PropTypes.string.isRequired,
search: PropTypes.string.isRequired,
hash: PropTypes.string.isRequired,
//state: PropTypes.object.isRequired,
}),
mode: PropTypes.object.isRequired, mode: PropTypes.object.isRequired,
dataSourceName: PropTypes.string, dataSourceName: PropTypes.string,
extensionManager: PropTypes.object, extensionManager: PropTypes.object,
@@ -0,0 +1,15 @@
import React from "react";
import { Route } from "react-router-dom";
import { useUserAuthentication } from "@ohif/ui";
export const PrivateRoute = ({ ...rest }) => {
const [{ user, enabled }, userAuthenticationService] = useUserAuthentication();
if (enabled && !user) {
return userAuthenticationService.handleUnauthenticated();
}
return <Route {...rest}/>;
}
export default PrivateRoute;
@@ -0,0 +1,33 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
function SignoutCallbackComponent({ userManager }) {
const navigate = useNavigate();
const onRedirectSuccess = (/* user */) => {
const { pathname, search = '' } = JSON.parse(
sessionStorage.getItem('ohif-redirect-to')
);
navigate(`${pathname}?${search}`);
};
const onRedirectError = error => {
throw new Error(error);
};
userManager
.signoutRedirectCallback()
.then(user => onRedirectSuccess(user))
.catch(error => onRedirectError(error));
// todo: add i18n
return <div>Redirecting...</div>;
}
SignoutCallbackComponent.propTypes = {
userManager: PropTypes.object.isRequired,
};
export default SignoutCallbackComponent;
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import moment from 'moment'; import moment from 'moment';
import qs from 'query-string'; import qs from 'query-string';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
@@ -38,7 +38,6 @@ const seriesInStudiesMap = new Map();
* - debounce `setFilterValues` (150ms?) * - debounce `setFilterValues` (150ms?)
*/ */
function WorkList({ function WorkList({
history,
data: studies, data: studies,
dataTotal: studiesTotal, dataTotal: studiesTotal,
isLoadingData, isLoadingData,
@@ -52,6 +51,7 @@ function WorkList({
const [appConfig] = useAppConfig(); const [appConfig] = useAppConfig();
// ~ Filters // ~ Filters
const query = useQuery(); const query = useQuery();
const navigate = useNavigate();
const STUDIES_LIMIT = 101; const STUDIES_LIMIT = 101;
const queryFilterValues = _getQueryFilterValues(query); const queryFilterValues = _getQueryFilterValues(query);
const [filterValues, _setFilterValues] = useState({ const [filterValues, _setFilterValues] = useState({
@@ -175,12 +175,14 @@ function WorkList({
} }
}); });
history.push({ const search = qs.stringify(queryString, {
skipNull: true,
skipEmptyString: true,
});
navigate({
pathname: '/', pathname: '/',
search: `?${qs.stringify(queryString, { search: search ? `?${search}` : undefined,
skipNull: true,
skipEmptyString: true,
})}`,
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedFilterValues]); }, [debouncedFilterValues]);
@@ -441,9 +443,6 @@ function WorkList({
} }
WorkList.propTypes = { WorkList.propTypes = {
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
data: PropTypes.array.isRequired, data: PropTypes.array.isRequired,
dataSource: PropTypes.shape({ dataSource: PropTypes.shape({
query: PropTypes.object.isRequired, query: PropTypes.object.isRequired,
+20 -23
View File
@@ -26,7 +26,7 @@ export default function buildModeRoutes({
dataSources, dataSources,
extensionManager, extensionManager,
servicesManager, servicesManager,
hotkeysManager hotkeysManager,
}) { }) {
const routes = []; const routes = [];
@@ -50,21 +50,19 @@ export default function buildModeRoutes({
const path = `/${mode.id}/${dataSourceName}`; const path = `/${mode.id}/${dataSourceName}`;
// TODO move up. // TODO move up.
const component = ({ location }) => ( const children = () => (
<ModeRoute <ModeRoute
location={location} mode={mode}
mode={mode} dataSourceName={dataSourceName}
dataSourceName={dataSourceName} extensionManager={extensionManager}
extensionManager={extensionManager} servicesManager={servicesManager}
servicesManager={servicesManager} hotkeysManager={hotkeysManager}
hotkeysManager={hotkeysManager} />
/>
); );
routes.push({ routes.push({
path, path,
component, children,
exact: true,
}); });
}); });
@@ -74,21 +72,20 @@ export default function buildModeRoutes({
const path = `/${mode.id}`; const path = `/${mode.id}`;
// TODO move up. // TODO move up.
const component = ({ location }) => ( const children = () => (
<ModeRoute <ModeRoute
location={location} mode={mode}
mode={mode} dataSourceName={defaultDataSourceName}
dataSourceName={defaultDataSourceName} extensionManager={extensionManager}
extensionManager={extensionManager} servicesManager={servicesManager}
servicesManager={servicesManager} hotkeysManager={hotkeysManager}
hotkeysManager={hotkeysManager} />
/>
); );
routes.push({ routes.push({
path, path,
component, children,
exact: true, private: true, // todo: all mode routes are private for now
}); });
}); });
+38 -23
View File
@@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { Switch, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from '@ohif/ui';
// Route Components // Route Components
import DataSourceWrapper from './DataSourceWrapper'; import DataSourceWrapper from './DataSourceWrapper';
import WorkList from './WorkList'; import WorkList from './WorkList';
import Local from './Local'; import Local from './Local';
import NotFound from './NotFound'; import NotFound from './NotFound';
import buildModeRoutes from './buildModeRoutes'; import buildModeRoutes from './buildModeRoutes';
import { ErrorBoundary } from '@ohif/ui'; import PrivateRoute from './PrivateRoute';
// TODO: Make these configurable // TODO: Make these configurable
// TODO: Include "routes" debug route if dev build // TODO: Include "routes" debug route if dev build
@@ -14,14 +16,13 @@ const bakedInRoutes = [
// WORK LIST // WORK LIST
{ {
path: '/', path: '/',
exact: true, children: DataSourceWrapper,
component: DataSourceWrapper, private: true,
props: { children: WorkList }, props: { children: WorkList },
}, },
{ {
path: '/local', path: '/local',
exact: true, children: Local,
component: Local,
}, },
// NOT FOUND (404) // NOT FOUND (404)
{ component: NotFound }, { component: NotFound },
@@ -33,6 +34,7 @@ const createRoutes = ({
extensionManager, extensionManager,
servicesManager, servicesManager,
hotkeysManager, hotkeysManager,
routerBasename,
}) => { }) => {
const routes = const routes =
buildModeRoutes({ buildModeRoutes({
@@ -45,31 +47,44 @@ const createRoutes = ({
const allRoutes = [...routes, ...bakedInRoutes]; const allRoutes = [...routes, ...bakedInRoutes];
function RouteWithErrorBoundary({ route, ...rest }) {
// eslint-disable-next-line react/jsx-props-no-spreading
return (
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
<route.children
{...rest}
{...route.props}
route={route}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
</ErrorBoundary>
);
}
const { UserAuthenticationService } = servicesManager.services;
return ( return (
<Switch> <Routes basename={routerBasename}>
{allRoutes.map((route, i) => { {allRoutes.map((route, i) => {
return ( return route.private === true ? (
<PrivateRoute
key={i}
path={route.path}
handleUnauthenticated={
UserAuthenticationService.handleUnauthenticated
}
element={<RouteWithErrorBoundary route={route} />}
/>
) : (
<Route <Route
key={i} key={i}
path={route.path} path={route.path}
exact={route.exact} element={<RouteWithErrorBoundary route={route} />}
strict={route.strict}
render={props => (
// eslint-disable-next-line react/jsx-props-no-spreading
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
<route.component
{...props}
{...route.props}
route={route}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
</ErrorBoundary>
)}
/> />
); );
})} })}
</Switch> </Routes>
); );
}; };
@@ -0,0 +1,191 @@
import React from 'react';
import { useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router';
import CallbackPage from '../routes/CallbackPage';
import SignoutCallbackComponent from '../routes/SignoutCallbackComponent';
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
function _isAbsoluteUrl(url) {
return url.includes('http://') || url.includes('https://');
}
function _makeAbsoluteIfNecessary(url, base_url) {
if (_isAbsoluteUrl(url)) {
return url;
}
/*
* Make sure base_url and url are not duplicating slashes.
*/
if (base_url[base_url.length - 1] === '/') {
base_url = base_url.slice(0, base_url.length - 1);
}
return base_url + url;
}
const initUserManager = (oidc, routerBasename) => {
if (!oidc || !oidc.length) {
return;
}
const firstOpenIdClient = oidc[0];
const { protocol, host } = window.location;
const baseUri = `${protocol}//${host}${routerBasename}`;
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
const silent_redirect_uri =
firstOpenIdClient.silent_redirect_uri || '/silent-refresh.html';
const post_logout_redirect_uri =
firstOpenIdClient.post_logout_redirect_uri || '/';
const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, {
redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri),
silent_redirect_uri: _makeAbsoluteIfNecessary(
silent_redirect_uri,
baseUri
),
post_logout_redirect_uri: _makeAbsoluteIfNecessary(
post_logout_redirect_uri,
baseUri
),
});
return getUserManagerForOpenIdConnectClient(openIdConnectConfiguration);
}
function LoginComponent(userManager) {
const queryParams = new URLSearchParams(location.search);
const iss = queryParams.get('iss');
const loginHint = queryParams.get('login_hint');
const targetLinkUri = queryParams.get('target_link_uri');
if (iss !== oidcAuthority) {
console.error(
'iss of /login does not match the oidc authority'
);
return null;
}
userManager.removeUser().then(() => {
if (targetLinkUri !== null) {
const ohifRedirectTo = {
pathname: new URL(targetLinkUri).pathname,
};
sessionStorage.setItem(
'ohif-redirect-to',
JSON.stringify(ohifRedirectTo)
);
} else {
const ohifRedirectTo = {
pathname: '/',
};
sessionStorage.setItem(
'ohif-redirect-to',
JSON.stringify(ohifRedirectTo)
);
}
if (loginHint !== null) {
userManager.signinRedirect({ login_hint: loginHint });
} else {
userManager.signinRedirect();
}
});
return null;
}
function OpenIdConnectRoutes({
oidc,
routerBasename,
UserAuthenticationService
}) {
const userManager = initUserManager(oidc, routerBasename);
const getAuthorizationHeader = () => {
const user = UserAuthenticationService.getUser();
return {
Authorization: `Bearer ${user.access_token}`
};
}
const handleUnauthenticated = () => {
userManager.signinRedirect()
// return null because this is used in a react component
return null;
};
const navigate = useNavigate();
useEffect(() => {
UserAuthenticationService.set({ enabled: true });
UserAuthenticationService.setServiceImplementation({
getAuthorizationHeader,
handleUnauthenticated
});
}, [])
const oidcAuthority = oidc[0].authority;
const location = useLocation();
const { pathname, search } = location;
const redirect_uri = new URL(userManager.settings._redirect_uri).pathname//.replace(routerBasename,'')
const silent_refresh_uri = new URL(userManager.settings._silent_redirect_uri).pathname//.replace(routerBasename,'')
const post_logout_redirect_uri = new URL(userManager.settings._post_logout_redirect_uri).pathname//.replace(routerBasename,'');
// const pathnameRelative = pathname.replace(routerBasename,'');
if (pathname !== redirect_uri) {
sessionStorage.setItem(
'ohif-redirect-to',
JSON.stringify({ pathname, search })
);
}
return (
<Routes basename={routerBasename}>
<Route
path={silent_refresh_uri}
onEnter={window.location.reload}
/>
<Route
path={post_logout_redirect_uri}
element={
<SignoutCallbackComponent
userManager={userManager}
successCallback={() => console.log('Signout successful')}
errorCallback={error => {
console.warn(error);
console.warn('Signout failed');
}}
/>
}
/>
<Route
path={redirect_uri}
element={<CallbackPage userManager={userManager} onRedirectSuccess={(user) => {
const { pathname, search = '' } = JSON.parse(
sessionStorage.getItem('ohif-redirect-to')
);
UserAuthenticationService.setUser(user);
navigate({
pathname,
search
})
}}/>}
/>
<Route
path="/login"
element={<LoginComponent userManager={userManager} oidcAuthority={oidcAuthority}/>}
/>
</Routes>
);
}
export default OpenIdConnectRoutes;
@@ -0,0 +1,31 @@
import { UserManager } from 'oidc-client';
/**
* Creates a userManager from oidcSettings
* LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration
*
* @param {Object} oidcSettings
* @param {string} oidcSettings.authServerUrl,
* @param {string} oidcSettings.clientId,
* @param {string} oidcSettings.authRedirectUri,
* @param {string} oidcSettings.postLogoutRedirectUri,
* @param {string} oidcSettings.responseType,
* @param {string} oidcSettings.extraQueryParams,
*/
export default function getUserManagerForOpenIdConnectClient(oidcSettings) {
if (!oidcSettings) {
return;
}
const settings = {
...oidcSettings,
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
filterProtocolClaims: true,
loadUserInfo: true,
};
const userManager = new UserManager(settings);
return userManager;
}
+1024 -214
View File
File diff suppressed because it is too large. Load diff