forked from OHIF/Viewers
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloud data source config): GUI and API for configuring a cloud d…
…ata source with Google cloud healthcare implementation (OHIF#3589)
- Loading branch information
1 parent
b4063f5
commit a7bc562
Showing
28 changed files
with
1,155 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
extensions/default/src/Components/DataSourceConfigurationComponent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
209 changes: 209 additions & 0 deletions
209
extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Oops, something went wrong.