From a336992971c07552c9dbb6e1de43169d37762ef1 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:19:17 -0400 Subject: [PATCH] feat(cloud data source config): GUI and API for configuring a cloud data source with Google cloud healthcare implementation (#3589) --- .../src/viewports/_getStatusComponent.tsx | 4 +- .../src/viewports/_getStatusComponent.tsx | 4 +- .../viewports/OHIFCornerstoneSRViewport.tsx | 4 +- .../DataSourceConfigurationComponent.tsx | 109 ++++++++ .../DataSourceConfigurationModalComponent.tsx | 209 ++++++++++++++++ .../src/Components/ItemListComponent.tsx | 93 +++++++ .../GoogleCloudDataSourceConfigurationAPI.ts | 234 ++++++++++++++++++ .../src/DicomTagBrowser/DicomTagBrowser.tsx | 39 +-- .../default/src/getCustomizationModule.tsx | 27 +- .../viewports/TrackedCornerstoneViewport.tsx | 2 +- platform/app/public/config/google.js | 1 + platform/app/src/routes/WorkList/WorkList.tsx | 8 + .../core/src/extensions/ExtensionManager.ts | 9 +- .../src/types/DataSourceConfigurationAPI.ts | 68 +++++ platform/core/src/types/index.ts | 14 +- .../img/data-source-configuration-ui.png | Bin 0 -> 27337 bytes .../dataSources/configuration-ui.md | 172 +++++++++++++ .../configuration/dataSources/static-files.md | 2 +- .../en-US/DataSourceConfiguration.json | 24 ++ platform/i18n/src/locales/en-US/index.js | 2 + .../ui/src/assets/icons/status-untracked.svg | 2 +- .../Button/__stories__/button.stories.mdx | 12 +- .../InputFilterText/InputFilterText.tsx | 91 +++++++ .../_stories_/inputFilterText.stories.mdx | 54 ++++ .../src/components/InputFilterText/index.js | 2 + .../StudyListFilter/StudyListFilter.tsx | 22 +- platform/ui/src/components/index.js | 2 + platform/ui/src/index.js | 1 + 28 files changed, 1155 insertions(+), 56 deletions(-) create mode 100644 extensions/default/src/Components/DataSourceConfigurationComponent.tsx create mode 100644 extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx create mode 100644 extensions/default/src/Components/ItemListComponent.tsx create mode 100644 extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts create mode 100644 platform/core/src/types/DataSourceConfigurationAPI.ts create mode 100644 platform/docs/docs/assets/img/data-source-configuration-ui.png create mode 100644 platform/docs/docs/configuration/dataSources/configuration-ui.md create mode 100644 platform/i18n/src/locales/en-US/DataSourceConfiguration.json create mode 100644 platform/ui/src/components/InputFilterText/InputFilterText.tsx create mode 100644 platform/ui/src/components/InputFilterText/_stories_/inputFilterText.stories.mdx create mode 100644 platform/ui/src/components/InputFilterText/index.js diff --git a/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx index fee3d619a..a78a6db1b 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx @@ -18,7 +18,9 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) { ); break; case false: - StatusIcon = () => ; + StatusIcon = () => ( + + ); ToolTipMessage = () =>
Click LOAD to load RTSTRUCT.
; } diff --git a/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx index d491faa6e..a814a3922 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx @@ -18,7 +18,9 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) { ); break; case false: - StatusIcon = () => ; + StatusIcon = () => ( + + ); ToolTipMessage = () =>
Click LOAD to load segmentation.
; } diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index da60f2d83..b95af260f 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -511,7 +511,9 @@ function _getStatusComponent({ ); break; case 3: - StatusIcon = () => ; + StatusIcon = () => ( + + ); ToolTipMessage = () => (
{`Click ${loadStr} to restore measurements.`}
diff --git a/extensions/default/src/Components/DataSourceConfigurationComponent.tsx b/extensions/default/src/Components/DataSourceConfigurationComponent.tsx new file mode 100644 index 000000000..768f55696 --- /dev/null +++ b/extensions/default/src/Components/DataSourceConfigurationComponent.tsx @@ -0,0 +1,109 @@ +import React, { ReactElement, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Icon, useModal } from '@ohif/ui'; +import { ExtensionManager, ServicesManager, Types } from '@ohif/core'; +import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent'; + +type DataSourceConfigurationComponentProps = { + servicesManager: ServicesManager; + extensionManager: ExtensionManager; +}; + +function DataSourceConfigurationComponent({ + servicesManager, + extensionManager, +}: DataSourceConfigurationComponentProps): ReactElement { + const { t } = useTranslation('DataSourceConfiguration'); + const { show, hide } = useModal(); + + const { customizationService } = servicesManager.services; + + const [configurationAPI, setConfigurationAPI] = useState< + Types.BaseDataSourceConfigurationAPI + >(); + + const [configuredItems, setConfiguredItems] = useState< + Array + >(); + + useEffect(() => { + let shouldUpdate = true; + + const dataSourceChangedCallback = async () => { + const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition(); + + if (!activeDataSourceDef.configuration.configurationAPI) { + return; + } + + const { factory: configurationAPIFactory } = + customizationService.get( + activeDataSourceDef.configuration.configurationAPI + ) ?? {}; + + if (!configurationAPIFactory) { + return; + } + + const configAPI = configurationAPIFactory(activeDataSourceDef.sourceName); + setConfigurationAPI(configAPI); + + configAPI.getConfiguredItems().then(list => { + if (shouldUpdate) { + setConfiguredItems(list); + } + }); + }; + + const sub = extensionManager.subscribe( + extensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, + dataSourceChangedCallback + ); + + dataSourceChangedCallback(); + + return () => { + shouldUpdate = false; + sub.unsubscribe(); + }; + }, []); + + return configuredItems ? ( +
+ + show({ + content: DataSourceConfigurationModalComponent, + title: t('Configure Data Source'), + contentProps: { + configurationAPI, + configuredItems, + onHide: hide, + }, + }) + } + > + {configuredItems.map((item, itemIndex) => { + return ( +
+
+ {item.name} +
+ {itemIndex !== configuredItems.length - 1 && ( +
|
+ )} +
+ ); + })} +
+ ) : ( + <> + ); +} + +export default DataSourceConfigurationComponent; diff --git a/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx b/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx new file mode 100644 index 000000000..74145d3b0 --- /dev/null +++ b/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx @@ -0,0 +1,209 @@ +import classNames from 'classnames'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Icon } from '@ohif/ui'; +import { Types } from '@ohif/core'; +import ItemListComponent from './ItemListComponent'; + +const NO_WRAP_ELLIPSIS_CLASS_NAMES = + 'text-ellipsis whitespace-nowrap overflow-hidden'; + +type DataSourceConfigurationModalComponentProps = { + configurationAPI: Types.BaseDataSourceConfigurationAPI; + configuredItems: Array; + onHide: () => void; +}; + +function DataSourceConfigurationModalComponent({ + configurationAPI, + configuredItems, + onHide, +}: DataSourceConfigurationModalComponentProps) { + const { t } = useTranslation('DataSourceConfiguration'); + + const [itemList, setItemList] = useState< + Array + >(); + + const [selectedItems, setSelectedItems] = useState(configuredItems); + + // Determines whether to show the full configuration for the data source. + // This typically occurs when the configuration component is first displayed. + const [showFullConfig, setShowFullConfig] = useState(true); + + const [errorMessage, setErrorMessage] = useState(); + + const [itemLabels] = useState(configurationAPI.getItemLabels()); + + /** + * The index of the selected item that is considered current and for which + * its sub-items should be displayed in the items list component. When the + * full/existing configuration for a data source is to be shown, the current + * selected item is the second to last in the `selectedItems` list. + */ + const currentSelectedItemIndex = showFullConfig + ? selectedItems.length - 2 + : selectedItems.length - 1; + + useEffect(() => { + let shouldUpdate = true; + + setErrorMessage(null); + + // Clear out the former/old list while we fetch the next sub item list. + setItemList(null); + + if (selectedItems.length === 0) { + configurationAPI + .initialize() + .then(items => { + if (shouldUpdate) { + setItemList(items); + } + }) + .catch(error => setErrorMessage(error.message)); + } else if (!showFullConfig && selectedItems.length === itemLabels.length) { + // The last item to configure the data source (path) has been selected. + configurationAPI.setCurrentItem(selectedItems[selectedItems.length - 1]); + // We can hide the modal dialog now. + onHide(); + } else { + configurationAPI + .setCurrentItem(selectedItems[currentSelectedItemIndex]) + .then(items => { + if (shouldUpdate) { + setItemList(items); + } + }) + .catch(error => setErrorMessage(error.message)); + } + + return () => { + shouldUpdate = false; + }; + }, [ + selectedItems, + configurationAPI, + onHide, + itemLabels, + showFullConfig, + currentSelectedItemIndex, + ]); + + const getSelectedItemCursorClasses = itemIndex => + itemIndex !== itemLabels.length - 1 && itemIndex < selectedItems.length + ? 'cursor-pointer' + : 'cursor-auto'; + + const getSelectedItemBackgroundClasses = itemIndex => + itemIndex < selectedItems.length + ? classNames( + 'bg-black/[.4]', + itemIndex !== itemLabels.length - 1 + ? 'hover:bg-transparent active:bg-secondary-dark' + : '' + ) + : 'bg-transparent'; + + const getSelectedItemBorderClasses = itemIndex => + itemIndex === currentSelectedItemIndex + 1 + ? classNames('border-2', 'border-solid', 'border-primary-light') + : itemIndex < selectedItems.length + ? 'border border-solid border-primary-active hover:border-primary-light active:border-white' + : 'border border-dashed border-secondary-light'; + + const getSelectedItemTextClasses = itemIndex => + itemIndex <= selectedItems.length + ? 'text-primary-light' + : 'text-primary-active'; + + const getErrorComponent = (): ReactElement => { + return ( +
+
+ {t(`Error fetching ${itemLabels[selectedItems.length]} list`)} +
+
{errorMessage}
+
+ ); + }; + + const getSelectedItemsComponent = (): ReactElement => { + return ( +
+ {itemLabels.map((itemLabel, itemLabelIndex) => { + return ( +
{ + setShowFullConfig(false); + setSelectedItems(theList => + theList.slice(0, itemLabelIndex) + ); + } + : undefined + } + > +
+ {itemLabelIndex < selectedItems.length ? ( + + ) : ( + + )} +
+ {t(itemLabel)} +
+
+ {itemLabelIndex < selectedItems.length ? ( +
+ {selectedItems[itemLabelIndex].name} +
+ ) : ( +

+ )} +
+ ); + })} +
+ ); + }; + + return ( +
+ {getSelectedItemsComponent()} +
+ {errorMessage ? ( + getErrorComponent() + ) : ( + { + setShowFullConfig(false); + setSelectedItems(theList => [ + ...theList.slice(0, currentSelectedItemIndex + 1), + item, + ]); + }} + > + )} +
+ ); +} + +export default DataSourceConfigurationModalComponent; diff --git a/extensions/default/src/Components/ItemListComponent.tsx b/extensions/default/src/Components/ItemListComponent.tsx new file mode 100644 index 000000000..b2d8525a3 --- /dev/null +++ b/extensions/default/src/Components/ItemListComponent.tsx @@ -0,0 +1,93 @@ +import classNames from 'classnames'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + Icon, + InputFilterText, + LoadingIndicatorProgress, +} from '@ohif/ui'; +import { Types } from '@ohif/core'; + +type ItemListComponentProps = { + itemLabel: string; + itemList: Array; + onItemClicked: (item: Types.BaseDataSourceConfigurationAPIItem) => void; +}; + +function ItemListComponent({ + itemLabel, + itemList, + onItemClicked, +}: ItemListComponentProps): ReactElement { + const { t } = useTranslation('DataSourceConfiguration'); + const [filterValue, setFilterValue] = useState(''); + + useEffect(() => { + setFilterValue(''); + }, [itemList]); + + return ( +
+
+
+ {t(`Select ${itemLabel}`)} +
+ +
+
+ {itemList == null ? ( + + ) : itemList.length === 0 ? ( +
+ + {t(`No ${itemLabel} available`)} +
+ ) : ( + <> +
+ {t(itemLabel)} +
+
+ {itemList + .filter( + item => + !filterValue || + item.name.toLowerCase().includes(filterValue.toLowerCase()) + ) + .map(item => { + const border = + 'rounded border-transparent border-b-secondary-light border-[1px] hover:border-primary-light'; + return ( +
+
{item.name}
+ +
+ ); + })} +
+ + )} +
+
+ ); +} + +export default ItemListComponent; diff --git a/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts b/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts new file mode 100644 index 000000000..d95cbd8ea --- /dev/null +++ b/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts @@ -0,0 +1,234 @@ +import { ExtensionManager, Types } from '@ohif/core'; + +/** + * This file contains the implementations of BaseDataSourceConfigurationAPIItem + * and BaseDataSourceConfigurationAPI for the Google cloud healthcare API. To + * better understand this implementation and/or to implement custom implementations, + * see the platform\core\src\types\DataSourceConfigurationAPI.ts and its JS doc + * comments as a guide. + */ + +/** + * The various Google Cloud Healthcare path item types. + */ +enum ItemType { + projects = 0, + locations = 1, + datasets = 2, + dicomStores = 3, +} + +interface NamedItem { + name: string; +} +interface Project extends NamedItem { + projectId: string; +} + +const initialUrl = 'https://cloudresourcemanager.googleapis.com/v1'; +const baseHealthcareUrl = 'https://healthcare.googleapis.com/v1'; + +class GoogleCloudDataSourceConfigurationAPIItem + implements Types.BaseDataSourceConfigurationAPIItem { + id: string; + name: string; + url: string; + itemType: ItemType; +} + +class GoogleCloudDataSourceConfigurationAPI + implements Types.BaseDataSourceConfigurationAPI { + private _extensionManager: ExtensionManager; + private _fetchOptions: { method: string; headers: unknown }; + private _dataSourceName: string; + + constructor(dataSourceName, servicesManager, extensionManager) { + this._dataSourceName = dataSourceName; + this._extensionManager = extensionManager; + const userAuthenticationService = + servicesManager.services.userAuthenticationService; + this._fetchOptions = { + method: 'GET', + headers: userAuthenticationService.getAuthorizationHeader(), + }; + } + + getItemLabels = () => ['Project', 'Location', 'Data set', 'DICOM store']; + + async initialize(): Promise { + const url = `${initialUrl}/projects`; + + const projects = (await GoogleCloudDataSourceConfigurationAPI._doFetch( + url, + ItemType.projects, + this._fetchOptions + )) as Array; + + if (!projects?.length) { + return []; + } + + const projectItems = projects.map(project => { + return { + id: project.projectId, + name: project.name, + itemType: ItemType.projects, + url: `${baseHealthcareUrl}/projects/${project.projectId}`, + }; + }); + + return projectItems; + } + + async setCurrentItem( + anItem: Types.BaseDataSourceConfigurationAPIItem + ): Promise { + const googleCloudItem = anItem as GoogleCloudDataSourceConfigurationAPIItem; + + if (googleCloudItem.itemType === ItemType.dicomStores) { + // Last configurable item, so update the data source configuration. + const url = `${googleCloudItem.url}/dicomWeb`; + const dataSourceDefCopy = JSON.parse( + JSON.stringify( + this._extensionManager.getDataSourceDefinition(this._dataSourceName) + ) + ); + dataSourceDefCopy.configuration = { + ...dataSourceDefCopy.configuration, + wadoUriRoot: url, + qidoRoot: url, + wadoRoot: url, + }; + + this._extensionManager.updateDataSourceConfiguration( + dataSourceDefCopy.sourceName, + dataSourceDefCopy.configuration + ); + + return []; + } + + const subItemType = googleCloudItem.itemType + 1; + const subItemField = `${ItemType[subItemType]}`; + + const url = `${googleCloudItem.url}/${subItemField}`; + + const fetchedSubItems = await GoogleCloudDataSourceConfigurationAPI._doFetch( + url, + subItemType, + this._fetchOptions + ); + + if (!fetchedSubItems?.length) { + return []; + } + + const subItems = fetchedSubItems.map(subItem => { + const nameSplit = subItem.name.split('/'); + return { + id: subItem.name, + name: nameSplit[nameSplit.length - 1], + itemType: subItemType, + url: `${baseHealthcareUrl}/${subItem.name}`, + }; + }); + + return subItems; + } + + async getConfiguredItems(): Promise< + Array + > { + const dataSourceDefinition = this._extensionManager.getDataSourceDefinition( + this._dataSourceName + ); + + const url = dataSourceDefinition.configuration.wadoUriRoot; + const projectsIndex = url.indexOf('projects'); + const urlSplit = url.substring(projectsIndex).split('/'); + + const configuredItems = []; + for (let itemType = 0; itemType < 4; itemType += 1) { + if (itemType === ItemType.projects) { + const projectId = urlSplit[1]; + const projectUrl = `${initialUrl}/projects/${projectId}`; + const data = await GoogleCloudDataSourceConfigurationAPI._doFetch( + projectUrl, + ItemType.projects, + this._fetchOptions + ); + const project = data[0] as Project; + configuredItems.push({ + id: project.projectId, + name: project.name, + itemType: itemType, + url: `${baseHealthcareUrl}/projects/${project.projectId}`, + }); + } else { + const relativePath = urlSplit.slice(0, itemType * 2 + 2).join('/'); + configuredItems.push({ + id: relativePath, + name: urlSplit[itemType * 2 + 1], + itemType: itemType, + url: `${baseHealthcareUrl}/${relativePath}`, + }); + } + } + + return configuredItems; + } + + /** + * Fetches an array of items the specified item type. + * @param urlStr the fetch url + * @param fetchItemType the type to fetch + * @param fetchOptions the header options for the fetch (e.g. authorization header) + * @param fetchSearchParams any search query params; currently only used for paging results + * @returns an array of items of the specified type + */ + private static async _doFetch( + urlStr: string, + fetchItemType: ItemType, + fetchOptions = {}, + fetchSearchParams: Record = {} + ): Promise | Array> { + try { + const url = new URL(urlStr); + url.search = new URLSearchParams(fetchSearchParams).toString(); + + const response = await fetch(url, fetchOptions); + const data = await response.json(); + if (response.status >= 200 && response.status < 300 && data != null) { + if (data.nextPageToken != null) { + fetchSearchParams.pageToken = data.nextPageToken; + const subPageData = await this._doFetch( + urlStr, + fetchItemType, + fetchOptions, + fetchSearchParams + ); + data[ItemType[fetchItemType]] = data[ItemType[fetchItemType]].concat( + subPageData + ); + } + if (data[ItemType[fetchItemType]]) { + return data[ItemType[fetchItemType]]; + } else if (data.name) { + return [data]; + } else { + return []; + } + } else { + const message = + data?.error?.message || + `Error returned from Google Cloud Healthcare: ${response.status} - ${response.statusText}`; + throw new Error(message); + } + } catch (err) { + const message = err?.message || 'Error occurred during fetch request.'; + throw new Error(message); + } + } +} + +export { GoogleCloudDataSourceConfigurationAPI }; diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx index 57cf04fa8..c616fe022 100644 --- a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx +++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx @@ -1,10 +1,9 @@ import dcmjs from 'dcmjs'; import moment from 'moment'; -import React, { useState, useMemo, useEffect, useRef } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { classes } from '@ohif/core'; -import { Icon, InputRange, Select, Typography } from '@ohif/ui'; +import { InputRange, Select, Typography, InputFilterText } from '@ohif/ui'; import debounce from 'lodash.debounce'; -import classNames from 'classnames'; import DicomTagTable from './DicomTagTable'; import './DicomTagBrowser.css'; @@ -34,8 +33,6 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => { setInstanceNumber(1); }; - const searchInputRef = useRef(null); - const activeDisplaySet = displaySets.find( ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID ); @@ -158,33 +155,11 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
- {/* TODO - refactor the following into its own reusable component */} - +
diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 6d03e2786..60c7d07f3 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -1,6 +1,8 @@ import { CustomizationService } from '@ohif/core'; import React from 'react'; import DataSourceSelector from './Panels/DataSourceSelector'; +import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent'; +import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI'; /** * @@ -11,7 +13,10 @@ import DataSourceSelector from './Panels/DataSourceSelector'; * custom page for the user to view their profile, or to add a custom * page for login etc. */ -export default function getCustomizationModule() { +export default function getCustomizationModule({ + servicesManager, + extensionManager, +}) { return [ { name: 'helloPage', @@ -136,6 +141,26 @@ export default function getCustomizationModule() { return clonedObject; }, }, + + { + // the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI + id: 'ohif.dataSourceConfigurationComponent', + component: DataSourceConfigurationComponent.bind(null, { + servicesManager, + extensionManager, + }), + }, + + { + // The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare + id: 'ohif.dataSourceConfigurationAPI.google', + factory: (dataSourceName: string) => + new GoogleCloudDataSourceConfigurationAPI( + dataSourceName, + servicesManager, + extensionManager + ), + }, ], }, ]; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 00e2c974b..66d9e2aa9 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -384,7 +384,7 @@ function _getStatusComponent(isTracked) { } > - + ); diff --git a/platform/app/public/config/google.js b/platform/app/public/config/google.js index a7c5c269c..bf3cfe7d9 100644 --- a/platform/app/public/config/google.js +++ b/platform/app/public/config/google.js @@ -55,6 +55,7 @@ window.config = { supportsWildcard: false, dicomUploadEnabled: true, omitQuotationForMultipartRequest: true, + configurationAPI: 'ohif.dataSourceConfigurationAPI.google', }, }, { diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 64616d9a8..d410a85b6 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -485,6 +485,9 @@ function WorkList({ } : undefined; + const { component: dataSourceConfigurationComponent } = + customizationService.get('ohif.dataSourceConfigurationComponent') ?? {}; + return (
setFilterValues(defaultFilterValues)} isFiltering={isFiltering(filterValues, defaultFilterValues)} onUploadClick={uploadProps ? () => show(uploadProps) : undefined} + getDataSourceConfigurationComponent={ + dataSourceConfigurationComponent + ? () => dataSourceConfigurationComponent() + : undefined + } /> {hasStudies ? (
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index b000bb07f..a41b0e8b9 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -354,7 +354,7 @@ export default class ExtensionManager extends PubSubService { * @param dataSourceName the data source name * @returns the data source definition */ - getDataSourceDef = dataSourceName => { + getDataSourceDefinition = dataSourceName => { if (dataSourceName === undefined) { // Default to the activeDataSource dataSourceName = this.activeDataSource; @@ -363,6 +363,13 @@ export default class ExtensionManager extends PubSubService { return this.dataSourceDefs[dataSourceName]; }; + /** + * Gets the data source definition for the active data source. + */ + getActiveDataSourceDefinition = () => { + return this.getDataSourceDefinition(this.activeDataSource); + }; + /** * @private * @param {string} moduleType diff --git a/platform/core/src/types/DataSourceConfigurationAPI.ts b/platform/core/src/types/DataSourceConfigurationAPI.ts new file mode 100644 index 000000000..146f4a285 --- /dev/null +++ b/platform/core/src/types/DataSourceConfigurationAPI.ts @@ -0,0 +1,68 @@ +export interface BaseDataSourceConfigurationAPIItem { + id: string; + name: string; +} + +/** + * The interface to use to configure an associated data source. Typically an + * instance of this interface is associated with a data source that the instance + * understands and can alter the data source's configuration. + */ +export interface BaseDataSourceConfigurationAPI { + /** + * Gets the i18n labels (i.e. the i18n lookup keys) for each of the configurable items + * of the data source configuration API. + * For example, for the Google Cloud Healthcare API, this would be + * ['Project', 'Location', 'Data set', 'DICOM store']. + * Besides the configurable item labels themselves, several other string look ups + * are used base on EACH of the labels returned by this method. + * For instance, for the label {itemLabel}, the following strings are fetched for + * translation... + * 1. `No {itemLabel} available` + * - used to indicate no such items are available + * - for example, for Google, `No Project available` would be 'No projects available' + * 2. `Select {itemLabel}` + * - used to direct selection of the item + * - for example, for Google, `Select Project` would be 'Select a project' + * 3. `Error fetching {itemLabel} list` + * - used to indicate an error occurred fetching the list of items + * - usually accompanied by the error itself + * - for example, for Google, `Error fetching Project list` would be 'Error fetching projects' + * 4. `Search {itemLabel} list` + * - used as the placeholder text for filtering a list of items + * - for example, for Google, `Search Project list` would be 'Search projects' + */ + getItemLabels(): Array; + + /** + * Initializes the data source configuration API and returns the top-level sub-items + * that can be chosen to begin the process of configuring the data source. + * For example, for the Google Cloud Healthcare API, this would perform the initial request + * to fetch the top level projects for the logged in user account. + */ + initialize(): Promise>; + + /** + * Sets the current path item and returns the sub-items of that item + * that can be further chosen to configure a data source. + * When setting the last configurable item of the data source (path), this method + * returns an empty list AND configures the active data source with the selected + * items path. + * For example, for the Google Cloud Healthcare API, this would take the current item + * (say a data set) and queries and returns its sub-items (i.e. all of the DICOM stores + * contained in that data set). Furthermore, whenever the item to set is a DICOM store, + * the Google Cloud Healthcare API implementation would update the OHIF data source + * associated with this instance to point to that DICOM store. + * @param item the item to set as current + */ + setCurrentItem( + item: BaseDataSourceConfigurationAPIItem + ): Promise>; + + /** + * Gets the list of items currently configured for the data source associated with + * this API instance. The resultant array must be the same length as the result of + * `getItemLabels`. + */ + getConfiguredItems(): Promise>; +} diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts index cdbce68e7..d2a39b83b 100644 --- a/platform/core/src/types/index.ts +++ b/platform/core/src/types/index.ts @@ -3,6 +3,10 @@ import * as HangingProtocol from './HangingProtocol'; import Services from './Services'; import Hotkey from '../classes/Hotkey'; import { DataSourceDefinition } from './DataSource'; +import { + BaseDataSourceConfigurationAPI, + BaseDataSourceConfigurationAPIItem, +} from './DataSourceConfigurationAPI'; export * from '../services/CustomizationService/types'; // Separate out some generic types @@ -19,4 +23,12 @@ export * from './Color'; * Export the types used within the various services and managers, but * not the services/managers themselves, which are exported at the top level. */ -export { Extensions, HangingProtocol, Services, Hotkey, DataSourceDefinition }; +export { + Extensions, + HangingProtocol, + Services, + Hotkey, + DataSourceDefinition, + BaseDataSourceConfigurationAPI, + BaseDataSourceConfigurationAPIItem, +}; diff --git a/platform/docs/docs/assets/img/data-source-configuration-ui.png b/platform/docs/docs/assets/img/data-source-configuration-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..f04956e0752f6d6056df0c5097e8369b545e6b10 GIT binary patch literal 27337 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

0!GSO#@QW(XLbZVeJy6(UFN~K0LDGQGn+uMg# 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 K7FA|m@&5q=nz_3G literal 0 HcmV?d00001 diff --git a/platform/docs/docs/configuration/dataSources/configuration-ui.md b/platform/docs/docs/configuration/dataSources/configuration-ui.md new file mode 100644 index 000000000..15f3b0bcb --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/configuration-ui.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 6 +sidebar_label: Configuration UI +--- + +# Configuration UI + +OHIF provides for a generic mechanism for configuring a data source. This is +most useful for those organizations with several data sources +that share common (path) hierarchies. For example, an organization may have several DICOM stores +in the Google Cloud Healthcare realm where each is organized into various projects, +location, data sets and DICOM stores. + +By implementing the `BaseDataSourceConfigurationAPI` and +`BaseDataSourceConfigurationAPIItem` in an [OHIF extension](../../platform/extensions/index.md), a data source can +be made configurable via the generic UI as is depicted below for a +Google Cloud Healthcare data source. + +![Data source configuration UI](../../assets/img/data-source-configuration-ui.png) + +## `BaseDataSourceConfigurationAPIItem` interface + +Each (path) item of a data source is represented by an instance of this interface. +At the very least each of these items must expose two properties: + +|Property |Description| +|---------|-----------| +|id|a string that uniquely identifies the item| +|name|a human readable name for the item| + +Note that information such as where in the path hierarchy the item exists +has been omitted, but can be added in any concrete class that might implement this +interface. For example, the the Google Cloud Healthcare implementation of this +interface (`GoogleCloudDataSourceConfigurationAPIItem`) adds an `itemType` +(i.e. projects, locations, datasets, or dicomStores) and `url`. + +## `BaseDataSourceConfigurationAPI` interface + +The implementation of this interface is at the heart of the configuration process. +It possesses several methods for building up a data source path based on various +`BaseDataSourceConfigurationAPIItem` objects that are set via calls to the `setCurrentItem` +method. + +The constructor for the concrete class implementation should accept whatever +parameters are necessary for configuring the data source. One argument +to the constructor must be the string identifying the name of the data source +to be configured. Furthermore, considering that the `ExtensionManager` possesses +API to configure and update data sources, it too will likely be an argument to +the constructor. See [Creation via Customization Module](#creation-via-customization-module) +for more information on how the constructor is invoked via a factory method. + +For an example implementation of this interface see `GoogleCloudDataSourceConfigurationAPI`. + +### Interface Methods + +Each of the following subsections lists a method of the interface with a description +detailing what the method should do. + +#### `getItemLabels` + +Gets the i18n labels (i.e. the i18n lookup keys) for each of the configurable items +of the data source configuration API. For example, for the Google Cloud Healthcare +API, this would be `['Project', 'Location', 'Data set', 'DICOM store']`. + +Besides the configurable item labels themselves, several other string look ups +are used base on EACH of the labels returned by this method. +For instance, for the label `{itemLabel}``, the following strings are fetched for +translation... +1. `No {itemLabel} available` + - used to indicate no such items are available + - for example, for Google, `No Project available` would be 'No projects available' +2. `Select {itemLabel}` + - used to direct selection of the item + - for example, for Google, `Select Project` would be 'Select a project' +3. `Error fetching {itemLabel} list` + - used to indicate an error occurred fetching the list of items + - usually accompanied by the error itself + - for example, for Google, `Error fetching Project list` would be 'Error fetching projects' +4. `Search {itemLabel} list` + - used as the placeholder text for filtering a list of items + - for example, for Google, `Search Project list` would be 'Search projects' + +#### `initialize` + +Initializes the cloud server API and returns the top-level sub-items +that can be chosen to begin the process of configuring a data source. +For example, for the Google Cloud Healthcare API, this would perform the initial request +to fetch the top level projects for the logged in user account. + +#### `setCurrentItem` + +Sets the current path item that is passed as an argument to the method and +returns the sub-items of that item +that can be further chosen to configure a data source. +When setting the last configurable item of the data source (path), this method +returns an empty list AND configures the active data source with the selected +items path. + +For example, for the Google Cloud Healthcare API, this would take the current item +(say a data set) and queries and returns its sub-items (i.e. all of the DICOM stores +contained in that data set). Furthermore, whenever the item to set is a DICOM store, +the Google Cloud Healthcare API implementation would update the OHIF data source +associated with this instance to point to that DICOM store. + +#### `getConfiguredItems` + +Gets the list of items currently configured for the data source associated with +this API instance. The resultant array must be the same length as the result of +`getItemLabels`. Furthermore the items returned should correspond (index-wise) +with the labels returned from `getItemLabels`. + +## Creation via Customization Module + +The generic UI (i.e. `DataSourceConfigurationComponent`) uses the +[OHIF UI customization service](../../platform/services/ui/customization-service.md) to +instantiate the `BaseDataSourceConfigurationAPI` instance to configure a data source. + +A UI configurable data source should have a `configurationAPI` field as part of +its `configuration` in the OHIF config file. The `configurationAPI` value is the +customization id of the customization module that provides the factory method +to instantiate the `BaseDataSourceConfigurationAPI` instance. + +For example, the following is a snippet of a Google Cloud Healthcare data source configuration. + +```js + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google-dicomweb', + configuration: { + name: 'GCP', + wadoUriRoot: 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/...', + ... + configurationAPI: 'ohif.dataSourceConfigurationAPI.google', + ... + }, + }, + ] +``` + +This suggests that the factory method is provided by the `'ohif.dataSourceConfigurationAPI.google'` +customization module. That customization module is provided by the `default` extension's +`getCustomizationModule` and looks something like the following snippet of code. Notice that +the factory method's name MUST be `factory` and accept one argument - the data source name. +Furthermore note how the constructor is invoked with anything required by the concrete configuration +API class. + +```js +export default function getCustomizationModule({ + servicesManager, + extensionManager, +}) { + return [ + { + name: 'default', + value: [ + { + // The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare + id: 'ohif.dataSourceConfigurationAPI.google', + factory: (dataSourceName: string) => + new GoogleCloudDataSourceConfigurationAPI( + dataSourceName, + servicesManager, + extensionManager + ), + }, + ], + }, + ]; +} + +``` diff --git a/platform/docs/docs/configuration/dataSources/static-files.md b/platform/docs/docs/configuration/dataSources/static-files.md index 296a87996..820e20a1a 100644 --- a/platform/docs/docs/configuration/dataSources/static-files.md +++ b/platform/docs/docs/configuration/dataSources/static-files.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 sidebar_label: Static Files --- diff --git a/platform/i18n/src/locales/en-US/DataSourceConfiguration.json b/platform/i18n/src/locales/en-US/DataSourceConfiguration.json new file mode 100644 index 000000000..07af42104 --- /dev/null +++ b/platform/i18n/src/locales/en-US/DataSourceConfiguration.json @@ -0,0 +1,24 @@ +{ + "Configure Data Source":"Configure Data Source", + "Data set": "Data set", + "DICOM store": "DICOM store", + "Location": "Location", + "Project": "Project", + "Error fetching Data set list": "Error fetching data sets", + "Error fetching DICOM store list": "Error fetching DICOM stores", + "Error fetching Location list": "Error fetching locations", + "Error fetching Project list": "Error fetching projects", + "No Project available": "No projects available", + "No Location available": "No locations available", + "No Data set available": "No data sets available", + "No DICOM store available": "No DICOM stores available", + "Select": "Select", + "Search Data set list": "Search data sets", + "Search DICOM store list": "Search DICOM stores", + "Search Location list": "Search locations", + "Search Project list": "Search projects", + "Select Data set": "Select a data Set", + "Select DICOM store": "Select a DICOM store", + "Select Location": "Select a location", + "Select Project": "Select a project" +} diff --git a/platform/i18n/src/locales/en-US/index.js b/platform/i18n/src/locales/en-US/index.js index 037f807cd..d8b19e6f8 100644 --- a/platform/i18n/src/locales/en-US/index.js +++ b/platform/i18n/src/locales/en-US/index.js @@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json'; import Buttons from './Buttons.json'; import CineDialog from './CineDialog.json'; import Common from './Common.json'; +import DataSourceConfiguration from './DataSourceConfiguration.json'; import DatePicker from './DatePicker.json'; import Header from './Header.json'; import MeasurementTable from './MeasurementTable.json'; @@ -18,6 +19,7 @@ export default { Buttons, CineDialog, Common, + DataSourceConfiguration, DatePicker, Header, MeasurementTable, diff --git a/platform/ui/src/assets/icons/status-untracked.svg b/platform/ui/src/assets/icons/status-untracked.svg index cf347223f..a4186231a 100644 --- a/platform/ui/src/assets/icons/status-untracked.svg +++ b/platform/ui/src/assets/icons/status-untracked.svg @@ -1,6 +1,6 @@ - + diff --git a/platform/ui/src/components/Button/__stories__/button.stories.mdx b/platform/ui/src/components/Button/__stories__/button.stories.mdx index 5e6fa6236..ca77c4903 100644 --- a/platform/ui/src/components/Button/__stories__/button.stories.mdx +++ b/platform/ui/src/components/Button/__stories__/button.stories.mdx @@ -1,4 +1,4 @@ -import Button, {ButtonType, ButtonSize} from '../Button'; +import {Button, ButtonEnums} from '../../../components'; import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; import { createComponentTemplate, @@ -45,8 +45,8 @@ There can be different types of buttons: `primary`, and `secondary`.

- - + +
@@ -59,8 +59,8 @@ to the button's height.
- - + +
@@ -71,7 +71,7 @@ You can mix different props together to create a button. - diff --git a/platform/ui/src/components/InputFilterText/InputFilterText.tsx b/platform/ui/src/components/InputFilterText/InputFilterText.tsx new file mode 100644 index 000000000..c7ceec7ad --- /dev/null +++ b/platform/ui/src/components/InputFilterText/InputFilterText.tsx @@ -0,0 +1,91 @@ +import classNames from 'classnames'; +import debounce from 'lodash.debounce'; +import React, { + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import Icon from '../Icon'; + +type InputFilterTextProps = { + className?: string; + value?: string; + placeholder: string; + onDebounceChange?: (val: string) => void; + onChange?: (val: string) => void; + debounceTime?: number; +}; + +/** + * A component to use as the input for text to filter by/on. A debounced callback is automatically provided + * so that the filtering in-turn will be debounced. There is also a straight onChange callback when the filter value is + * required immediately and NOT debounced. The debounce time is also configurable. + */ +const InputFilterText = ({ + className, + value = '', + placeholder, + onDebounceChange, + onChange, + debounceTime = 200, +}: InputFilterTextProps): ReactElement => { + const [filterValue, setFilterValue] = useState(value); + + const searchInputRef = useRef(null); + + const debouncedOnChange = useMemo(() => { + return debounce(onDebounceChange || (() => {}), debounceTime); + }, []); + + // This allows for the filter value to be updated via the props. + useEffect(() => setFilterValue(value), [value]); + + useEffect(() => { + return debouncedOnChange?.cancel(); + }, []); + + const handleFilterTextChanged = useCallback(value => { + setFilterValue(value); + + if (onChange) { + onChange(value); + } + + if (onDebounceChange) { + debouncedOnChange(value); + } + }, []); + + return ( + + ); +}; + +export default InputFilterText; diff --git a/platform/ui/src/components/InputFilterText/_stories_/inputFilterText.stories.mdx b/platform/ui/src/components/InputFilterText/_stories_/inputFilterText.stories.mdx new file mode 100644 index 000000000..aa030fe8a --- /dev/null +++ b/platform/ui/src/components/InputFilterText/_stories_/inputFilterText.stories.mdx @@ -0,0 +1,54 @@ +import InputFilterText from '../InputFilterText'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; + +export const argTypes = { + component: InputFilterText, + title: 'Components/InputFilterText', +}; + + + +export const InputFilterTextTemplate = args => ( +
+ +
+) + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +InputFilterText is a component that is styled such that it can be used as a text input to +filter a list of textual items. It allows you to enter any text. There are two (optional) +callbacks that can be invoked as the characters of the text are entered: one +callback is invoked as each character is typed and another is debounced so that +any filtering can occur once the user has entered a significant amount of info and +pausing by a configurable amount of time in milliseconds. The component also +provides a button on the far right of the component that when clicked will clear the input +text. The button only appears when there is text in the component. + + + console.log('input text changed'), + onDebounceChange: () => console.log('debounce text changed'), + }} + > + {InputFilterTextTemplate.bind({})} + + + +## Props + + + +## Contribute + +