feat(cloud data source config): GUI and API for configuring a cloud data source with Google cloud healthcare implementation (#3589)
This commit is contained in:
1 parent
a2ef2b0fcb
commit
a336992971
28 files changed
+1155
-56
No files matched your search
@@ -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<Types.BaseDataSourceConfigurationAPIItem>
|
||||
>();
|
||||
|
||||
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 ? (
|
||||
<div className="flex text-aqua-pale overflow-hidden items-center">
|
||||
<Icon
|
||||
name="settings"
|
||||
className="cursor-pointer shrink-0 w-3.5 h-3.5 mr-2.5"
|
||||
onClick={() =>
|
||||
show({
|
||||
content: DataSourceConfigurationModalComponent,
|
||||
title: t('Configure Data Source'),
|
||||
contentProps: {
|
||||
configurationAPI,
|
||||
configuredItems,
|
||||
onHide: hide,
|
||||
},
|
||||
})
|
||||
}
|
||||
></Icon>
|
||||
{configuredItems.map((item, itemIndex) => {
|
||||
return (
|
||||
<div key={itemIndex} className="flex overflow-hidden">
|
||||
<div
|
||||
key={itemIndex}
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{itemIndex !== configuredItems.length - 1 && (
|
||||
<div className="px-2.5">|</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceConfigurationComponent;
|
||||
@@ -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<Types.BaseDataSourceConfigurationAPIItem>;
|
||||
onHide: () => void;
|
||||
};
|
||||
|
||||
function DataSourceConfigurationModalComponent({
|
||||
configurationAPI,
|
||||
configuredItems,
|
||||
onHide,
|
||||
}: DataSourceConfigurationModalComponentProps) {
|
||||
const { t } = useTranslation('DataSourceConfiguration');
|
||||
|
||||
const [itemList, setItemList] = useState<
|
||||
Array<Types.BaseDataSourceConfigurationAPIItem>
|
||||
>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-4 min-h-[1px] grow">
|
||||
<div className="text-primary-light text-[20px]">
|
||||
{t(`Error fetching ${itemLabels[selectedItems.length]} list`)}
|
||||
</div>
|
||||
<div className="bg-black text-[14px] grow p-4">{errorMessage}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getSelectedItemsComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{itemLabels.map((itemLabel, itemLabelIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={itemLabel}
|
||||
className={classNames(
|
||||
'rounded-md p-3.5 flex flex-col gap-1 shrink min-w-[1px] basis-[200px]',
|
||||
getSelectedItemCursorClasses(itemLabelIndex),
|
||||
getSelectedItemBackgroundClasses(itemLabelIndex),
|
||||
getSelectedItemBorderClasses(itemLabelIndex),
|
||||
getSelectedItemTextClasses(itemLabelIndex)
|
||||
)}
|
||||
onClick={
|
||||
(showFullConfig && itemLabelIndex < currentSelectedItemIndex) ||
|
||||
itemLabelIndex <= currentSelectedItemIndex
|
||||
? () => {
|
||||
setShowFullConfig(false);
|
||||
setSelectedItems(theList =>
|
||||
theList.slice(0, itemLabelIndex)
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="flex gap-2 items-center text-">
|
||||
{itemLabelIndex < selectedItems.length ? (
|
||||
<Icon name="status-tracked" />
|
||||
) : (
|
||||
<Icon name="status-untracked" />
|
||||
)}
|
||||
<div className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES)}>
|
||||
{t(itemLabel)}
|
||||
</div>
|
||||
</div>
|
||||
{itemLabelIndex < selectedItems.length ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'text-white text-[14px]',
|
||||
NO_WRAP_ELLIPSIS_CLASS_NAMES
|
||||
)}
|
||||
>
|
||||
{selectedItems[itemLabelIndex].name}
|
||||
</div>
|
||||
) : (
|
||||
<br></br>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-300px)] flex flex-col pt-0.5 gap-4 select-none">
|
||||
{getSelectedItemsComponent()}
|
||||
<div className="w-full h-0.5 shrink-0 bg-black"></div>
|
||||
{errorMessage ? (
|
||||
getErrorComponent()
|
||||
) : (
|
||||
<ItemListComponent
|
||||
itemLabel={itemLabels[currentSelectedItemIndex + 1]}
|
||||
itemList={itemList}
|
||||
onItemClicked={item => {
|
||||
setShowFullConfig(false);
|
||||
setSelectedItems(theList => [
|
||||
...theList.slice(0, currentSelectedItemIndex + 1),
|
||||
item,
|
||||
]);
|
||||
}}
|
||||
></ItemListComponent>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceConfigurationModalComponent;
|
||||
@@ -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<Types.BaseDataSourceConfigurationAPIItem>;
|
||||
onItemClicked: (item: Types.BaseDataSourceConfigurationAPIItem) => void;
|
||||
};
|
||||
|
||||
function ItemListComponent({
|
||||
itemLabel,
|
||||
itemList,
|
||||
onItemClicked,
|
||||
}: ItemListComponentProps): ReactElement {
|
||||
const { t } = useTranslation('DataSourceConfiguration');
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFilterValue('');
|
||||
}, [itemList]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 min-h-[1px] grow">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-primary-light text-[20px]">
|
||||
{t(`Select ${itemLabel}`)}
|
||||
</div>
|
||||
<InputFilterText
|
||||
className="grow max-w-[40%]"
|
||||
value={filterValue}
|
||||
onDebounceChange={setFilterValue}
|
||||
placeholder={t(`Search ${itemLabel} list`)}
|
||||
></InputFilterText>
|
||||
</div>
|
||||
<div className="flex flex-col relative min-h-[1px] grow text-[14px] bg-black">
|
||||
{itemList == null ? (
|
||||
<LoadingIndicatorProgress className={'w-full h-full'} />
|
||||
) : itemList.length === 0 ? (
|
||||
<div className="flex flex-col h-full px-6 py-4 items-center justify-center text-primary-light">
|
||||
<Icon name="magnifier" className="mb-4" />
|
||||
<span>{t(`No ${itemLabel} available`)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-secondary-dark text-white px-3 py-1.5">
|
||||
{t(itemLabel)}
|
||||
</div>
|
||||
<div className="overflow-auto ohif-scrollbar">
|
||||
{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 (
|
||||
<div
|
||||
className={classNames(
|
||||
'group mx-2 px-6 py-2 flex justify-between items-center hover:text-primary-light hover:bg-primary-dark',
|
||||
border
|
||||
)}
|
||||
key={item.id}
|
||||
>
|
||||
<div>{item.name}</div>
|
||||
<Button
|
||||
onClick={() => onItemClicked(item)}
|
||||
className="invisible group-hover:visible"
|
||||
endIcon={<Icon name="arrow-left" />}
|
||||
>
|
||||
{t('Select')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemListComponent;
|
||||
+234
@@ -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<Types.BaseDataSourceConfigurationAPIItem[]> {
|
||||
const url = `${initialUrl}/projects`;
|
||||
|
||||
const projects = (await GoogleCloudDataSourceConfigurationAPI._doFetch(
|
||||
url,
|
||||
ItemType.projects,
|
||||
this._fetchOptions
|
||||
)) as Array<Project>;
|
||||
|
||||
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<Types.BaseDataSourceConfigurationAPIItem[]> {
|
||||
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<GoogleCloudDataSourceConfigurationAPIItem>
|
||||
> {
|
||||
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<string, string> = {}
|
||||
): Promise<Array<Project> | Array<NamedItem>> {
|
||||
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 };
|
||||
@@ -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 }) => {
|
||||
</div>
|
||||
<div className="w-full h-1 bg-black"></div>
|
||||
<div className="flex flex-row my-3 w-1/2">
|
||||
{/* TODO - refactor the following into its own reusable component */}
|
||||
<label className="relative block w-full mr-8">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<Icon name="icon-search"></Icon>
|
||||
</span>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className="block bg-black w-full shadow transition duration-300 appearance-none border border-inputfield-main focus:border-inputfield-focus focus:outline-none disabled:border-inputfield-disabled rounded w-full py-2 px-9 text-base leading-tight placeholder:text-inputfield-placeholder"
|
||||
placeholder="Search metadata..."
|
||||
onChange={event => debouncedSetFilterValue(event.target.value)}
|
||||
autoComplete="off"
|
||||
></input>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<Icon
|
||||
name="icon-clear-field"
|
||||
className={classNames(
|
||||
'cursor-pointer',
|
||||
filterValue ? '' : 'hidden'
|
||||
)}
|
||||
onClick={() => {
|
||||
searchInputRef.current.value = '';
|
||||
debouncedSetFilterValue('');
|
||||
}}
|
||||
></Icon>
|
||||
</span>
|
||||
</label>
|
||||
<InputFilterText
|
||||
className="block w-full mr-8"
|
||||
placeholder="Search metadata..."
|
||||
onDebounceChange={setFilterValue}
|
||||
></InputFilterText>
|
||||
</div>
|
||||
<DicomTagTable rows={filteredRows} />
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user