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 { pathname } = location;
const dataSourceIdx = pathname.indexOf('/', 1);
const search =
dataSourceIdx === -1
? undefined
: `datasources=${pathname.substring(dataSourceIdx + 1)}`;
// const search =
// dataSourceIdx === -1
// ? undefined
// : `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({
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.
- `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.
- `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,
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: [
{
friendlyName: 'dcmjs DICOMWeb Server',

View File

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

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:
Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1),
config: query.get('configUrl'),
};
// patientName: good

View File

@ -345,12 +345,17 @@ function WorkList({
// mode.routeName
// mode.routes[x].path
// 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 (
<Link
key={i}
to={`${dataPath ? '../../' : ''}${mode.routeName}${dataPath ||
''}?StudyInstanceUIDs=${studyInstanceUid}`}
''}?${query.toString()}`}
// to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
>
<Button
@ -531,6 +536,7 @@ const defaultFilterValues = {
pageNumber: 1,
resultsPerPage: 25,
datasources: '',
configUrl: null,
};
function _tryParseInt(str, defaultValue) {
@ -561,6 +567,7 @@ function _getQueryFilterValues(params) {
pageNumber: _tryParseInt(params.get('pagenumber'), undefined),
resultsPerPage: _tryParseInt(params.get('resultsperpage'), undefined),
datasources: params.get('datasources'),
configUrl: params.get('configurl'),
};
// Delete null/undefined keys