feat: query string dynamic app config (#3391)

* feat: query string dynamic app config

* fix: bug fix and couple of very small improvements
This commit is contained in:
Ashish Narnoli 2023-05-16 00:22:48 +05:30 committed by GitHub
parent f4be73b5cf
commit af8bb27575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 101 additions and 21 deletions

View File

@ -46,13 +46,27 @@ function ViewerLayout({
const onClickReturnButton = () => { const onClickReturnButton = () => {
const { pathname } = location; const { pathname } = location;
const dataSourceIdx = pathname.indexOf('/', 1); const dataSourceIdx = pathname.indexOf('/', 1);
const search = // const search =
dataSourceIdx === -1 // dataSourceIdx === -1
? undefined // ? undefined
: `datasources=${pathname.substring(dataSourceIdx + 1)}`; // : `datasources=${pathname.substring(dataSourceIdx + 1)}`;
// Todo: Handle parameters in a better way.
const query = new URLSearchParams(window.location.search);
const configUrl = query.get('configUrl');
const searchQuery = new URLSearchParams();
if (dataSourceIdx !== -1) {
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
}
if (configUrl) {
searchQuery.append('configUrl', configUrl);
}
navigate({ navigate({
pathname: '/', pathname: '/',
search, search: decodeURIComponent(searchQuery.toString()),
}); });
}; };

View File

@ -116,7 +116,24 @@ Here are a list of some options available:
if auth headers are used, a preflight request is required. if auth headers are used, a preflight request is required.
- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type. - `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type.
- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies. - `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies.
- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.<br/>
Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.
- Regex helps to avoid easy exploit. Dafault is `/.*/`. Setup your own regex to choose a specific source of configuration only.
- System administrators can return `cross-origin: same-origin` with OHIF files to disallow any loading from other origin. It will block read access to resources loaded from a different origin to avoid potential attack vector.
- Example config:
```js
dangerouslyUseDynamicConfig: {
enabled: false,
regex: /.*/
}
```
> Example 1, to allow numbers and letters in an absolute or sub-path only.<br/>
`regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/`<br/>
Example 2, to restricts to either hosptial.com or othersite.com.<br/>
`regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/` <br/>
Example usage:<br/>
`http://localhost:3000/?configUrl=http://localhost:3000/config/example.json`<br/>

View File

@ -25,6 +25,16 @@ window.config = {
}, },
// filterQueryParam: false, // filterQueryParam: false,
defaultDataSourceName: 'dicomweb', defaultDataSourceName: 'dicomweb',
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
// dangerouslyUseDynamicConfig: {
// enabled: true,
// // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only.
// // Example 1, to allow numbers and letters in an absolute or sub-path only.
// // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/
// // Example 2, to restricts to either hosptial.com or othersite.com.
// // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/
// regex: /.*/,
// },
dataSources: [ dataSources: [
{ {
friendlyName: 'dcmjs DICOMWeb Server', friendlyName: 'dcmjs DICOMWeb Server',

View File

@ -20,20 +20,28 @@ import {
modes as defaultModes, modes as defaultModes,
extensions as defaultExtensions, extensions as defaultExtensions,
} from './pluginImports'; } from './pluginImports';
import loadDynamicConfig from './loadDynamicConfig';
/** loadDynamicConfig(window.config).then(config_json => {
* Combine our appConfiguration with installed extensions and modes. // Reset Dynamic config if defined
* In the future appConfiguration may contain modes added at runtime. if (config_json !== null) {
* */ window.config = config_json;
const appProps = { }
config: window ? window.config : {},
defaultExtensions,
defaultModes,
};
/** Create App */ /**
const app = React.createElement(App, appProps, null); * Combine our appConfiguration with installed extensions and modes.
/** Render */ * In the future appConfiguration may contain modes added at runtime.
ReactDOM.render(app, document.getElementById('root')); * */
const appProps = {
config: window ? window.config : {},
defaultExtensions,
defaultModes,
};
/** Create App */
const app = React.createElement(App, appProps, null);
/** Render */
ReactDOM.render(app, document.getElementById('root'));
});
export { history }; export { history };

View File

@ -0,0 +1,23 @@
export default async config => {
const useDynamicConfig = config.dangerouslyUseDynamicConfig;
// Check if dangerouslyUseDynamicConfig enabled
if (useDynamicConfig?.enabled) {
// If enabled then get configUrl query-string
let query = new URLSearchParams(window.location.search);
let configUrl = query.get('configUrl');
if (configUrl) {
// validate regex
const regex = useDynamicConfig.regex;
if (configUrl.match(regex)) {
const response = await fetch(configUrl);
return response.json();
} else {
return null;
}
}
}
return null;
};

View File

@ -170,6 +170,7 @@ function _getQueryFilterValues(query, queryLimit) {
// Offset... // Offset...
offset: offset:
Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1), Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1),
config: query.get('configUrl'),
}; };
// patientName: good // patientName: good

View File

@ -345,12 +345,17 @@ function WorkList({
// mode.routeName // mode.routeName
// mode.routes[x].path // mode.routes[x].path
// Don't specify default data source, and it should just be picked up... (this may not currently be the case) // Don't specify default data source, and it should just be picked up... (this may not currently be the case)
// How do we know which params to pass? Today, it's just StudyInstanceUIDs // How do we know which params to pass? Today, it's just StudyInstanceUIDs and configUrl if exists
const query = new URLSearchParams();
if (filterValues.configUrl) {
query.append('configUrl', filterValues.configUrl);
}
query.append('StudyInstanceUIDs', studyInstanceUid);
return ( return (
<Link <Link
key={i} key={i}
to={`${dataPath ? '../../' : ''}${mode.routeName}${dataPath || to={`${dataPath ? '../../' : ''}${mode.routeName}${dataPath ||
''}?StudyInstanceUIDs=${studyInstanceUid}`} ''}?${query.toString()}`}
// to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`} // to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
> >
<Button <Button
@ -531,6 +536,7 @@ const defaultFilterValues = {
pageNumber: 1, pageNumber: 1,
resultsPerPage: 25, resultsPerPage: 25,
datasources: '', datasources: '',
configUrl: null,
}; };
function _tryParseInt(str, defaultValue) { function _tryParseInt(str, defaultValue) {
@ -561,6 +567,7 @@ function _getQueryFilterValues(params) {
pageNumber: _tryParseInt(params.get('pagenumber'), undefined), pageNumber: _tryParseInt(params.get('pagenumber'), undefined),
resultsPerPage: _tryParseInt(params.get('resultsperpage'), undefined), resultsPerPage: _tryParseInt(params.get('resultsperpage'), undefined),
datasources: params.get('datasources'), datasources: params.get('datasources'),
configUrl: params.get('configurl'),
}; };
// Delete null/undefined keys // Delete null/undefined keys