1 Commits

Author SHA1 Message Date
mario
505462b231 generate ext tapi masih error 2025-04-14 08:30:27 +07:00
36 changed files with 601 additions and 735 deletions

3
.gitignore vendored
View File

@@ -12,7 +12,6 @@ coverage/
.yarn/
.nx/
addOns/yarn.lock
**.zip
# YALC (for Erik)
.yalc
@@ -60,5 +59,3 @@ tests/playwright-report/
# Dummy
/dump
jwt-auth-inject.json
platform/app/dist.zip

View File

@@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@@ -0,0 +1,96 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the extension in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 expertise-panel ()
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,7 @@
# expertise-panel
## Description
Expertisi Panel extension
## Author
## License
MIT

View File

@@ -0,0 +1,50 @@
module.exports = {
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-refresh/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@@ -0,0 +1,75 @@
{
"name": "expertise-panel",
"version": "0.0.1",
"description": "Expertisi Panel extension",
"author": "",
"license": "MIT",
"main": "dist/umd/expertise-panel/index.umd.js",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:my-extension": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^3.9.1",
"@ohif/extension-default": "^3.9.1",
"@ohif/extension-cornerstone": "^3.9.1",
"@ohif/i18n": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"@svgr/webpack": "^8.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
function ExpertiseSidePanelComponent() {
return <div className="w-full text-center text-white">Hello Worlds</div>;
}
export default ExpertiseSidePanelComponent;

View File

@@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@@ -0,0 +1,97 @@
import ExpertiseSidePanelComponent from './ExpertiseSidePanelComponent';
import { id } from './id';
/**
* You can remove any of the following modules if you don't need them.
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {},
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => {
return [
{
name: 'ExpertiseSidePanel',
iconName: 'logo-ohif-small',
iconLabel: 'Expertise Side Panel',
label: 'Expertise Side Panel Label',
component: ExpertiseSidePanelComponent,
},
];
},
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getViewportModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ToolbarModule should provide a list of tool buttons that will be available in OHIF
* for Modes to consume and use in the toolbar. Each tool button is defined by
* {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and
* splitButton toolButton that the default extension is providing.
*/
getToolbarModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* LayoutTemplateMOdule should provide a list of layout templates that will be
* available in OHIF for Modes to consume and use to layout the viewer.
* Each layout template is defined by a { name, id, component}. Examples include
* the default layout template provided by the default extension which renders
* a Header, left and right sidebars, and a viewport section in the middle
* of the viewer.
*/
getLayoutTemplateModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
* available in OHIF for Modes to consume and use to create displaySets from Series.
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* HangingProtocolModule should provide a list of hanging protocols that will be
* available in OHIF for Modes to use to decide on the structure of the viewports
* and also the series that hung in the viewports. Each hanging protocol is defined by
* { name, protocols}. Examples include the default hanging protocol provided by
* the default extension that shows 2x2 viewports.
*/
getHangingProtocolModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* CommandsModule should provide a list of commands that will be available in OHIF
* for Modes to consume and use in the viewports. Each command is defined by
* an object of { actions, definitions, defaultContext } where actions is an
* object of functions, definitions is an object of available commands, their
* options, and defaultContext is the default context for the command to run against.
*/
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ContextModule should provide a list of context that will be available in OHIF
* and will be provided to the Modes. A context is a state that is shared OHIF.
* Context is defined by an object of { name, context, provider }. Examples include
* the measurementTracking context provided by the measurementTracking extension.
*/
getContextModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* DataSourceModule should provide a list of data sources to be used in OHIF.
* DataSources can be used to map the external data formats to the OHIF's
* native format. DataSources are defined by an object of { name, type, createDataSource }.
*/
getDataSourcesModule: ({ servicesManager, commandsManager, extensionManager }) => {},
};

View File

@@ -102,20 +102,8 @@ export const studyDataForOverlayItem = (studyInstanceUID: string) => {
try {
const qidoRootUrl = getQidoRootUrl();
// Get the authentication token from session storage
const authToken = window.sessionStorage.getItem('ohif-auth-token');
// Create request headers with Authorization if token exists
const headers: HeadersInit = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(
`${qidoRootUrl}/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=${studyInstanceUID}`,
{
headers,
}
`${qidoRootUrl}/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=${studyInstanceUID}`
);
if (!response.ok) {

View File

@@ -39,16 +39,6 @@ export default function initWADOImageLoader(
Accept: acceptHeader,
};
// // Patch Mario:
const authToken = sessionStorage.getItem('ohif-auth-token');
if (!authToken) {
// window.location.href = '/login'; // Kalau mau pakae login tinggal uncomment
window.location.href = '/';
return;
}
xhrRequestHeaders.Authorization = `Bearer ${authToken}`;
if (headers) {
Object.assign(xhrRequestHeaders, headers);
}

View File

@@ -26,15 +26,6 @@ const CornerstoneViewportDownloadForm = ({
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
// console.log('cornerstoneViewportService', cornerstoneViewportService);
const viewportInfo = cornerstoneViewportService.getViewportInfo("default");
// console.log('viewportInfo', viewportInfo);
// Retrieve StudyInstanceUID from viewportInfo
const StudyInstanceUID = viewportInfo.getViewportData().data[0].StudyInstanceUID;
const SetInstanceUID = viewportInfo.getViewportData().data[0].displaySetInstanceUID;
// console.log('StudyInstanceUID', StudyInstanceUID);
// console.log('SetInstanceUID', SetInstanceUID);
const {
viewportId: activeViewportId,
renderingEngineId,
@@ -117,10 +108,6 @@ const CornerstoneViewportDownloadForm = ({
const downloadCanvas = getOrCreateCanvas(element);
// Log the canvas content before conversion
const context = downloadCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, downloadCanvas.width, downloadCanvas.height);
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
@@ -227,43 +214,6 @@ const CornerstoneViewportDownloadForm = ({
});
};
// New function to send annotation data
const sendAnnotationData = async (base64Image, activeViewportElement) => {
try {
// Get the SOPInstanceUID from the active viewport
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
const imageId = activeViewportEnabledElement?.viewport?.getCurrentImageId();
if (!base64Image || !StudyInstanceUID) {
throw new Error('Missing required data');
}
const payload = {
image: base64Image,
StudyInstanceUID: StudyInstanceUID
};
const response = await fetch('http://host:port/one-api/tools/annotation/store', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log('Annotation data sent successfully:', data);
return data;
} catch (error) {
console.error('Error sending annotation data:', error);
throw error;
}
};
const downloadBlob = (filename, fileType) => {
const file = `${filename}.${fileType}`;
const divForDownloadViewport = document.querySelector(
@@ -271,16 +221,9 @@ const CornerstoneViewportDownloadForm = ({
);
html2canvas(divForDownloadViewport).then(canvas => {
const dataUrl = canvas.toDataURL(fileType, 1.0);
const base64Image = dataUrl.split(',')[1]; // Remove prefix 'data:image/png;base64,'
// Send annotation data
sendAnnotationData(base64Image, activeViewportElement)
.catch(error => console.error('Annotation data sending failed:', error));
const link = document.createElement('a');
link.download = file;
link.href = dataUrl;
link.href = canvas.toDataURL(fileType, 1.0);
link.click();
});
};

View File

@@ -26,8 +26,6 @@ const SidePanelWithServices = ({
const [sidePanelOpen, setSidePanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
const [studyInstanceUID, setStudyInstanceUID] = useState('');
const [lastActivatedStudyUID, setLastActivatedStudyUID] = useState('');
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
setActiveTabIndex(activeTabIndex);
@@ -73,33 +71,23 @@ const SidePanelWithServices = ({
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
const isExpertisePanel = activatePanelEvent.panelId.includes('-exp-');
// Handle the `-exp` suffix logic
const isExpertisePanel = activatePanelEvent.panelId.endsWith('-exp');
const realPanelID = isExpertisePanel
? activatePanelEvent.panelId.split('-exp-')[0]
? activatePanelEvent.panelId.replace(/-exp$/, '')
: activatePanelEvent.panelId;
// studyInstanceUID = take from activatePanelEvent.panelId after '-exp-
setStudyInstanceUID(isExpertisePanel ? activatePanelEvent.panelId.split('-exp-')[1] : null);
const tabIndex = tabs.findIndex(tab => tab.id === realPanelID);
if (isExpertisePanel && side === 'right') {
// Extract study UID from the panel ID
const currentStudyUID = activatePanelEvent.panelId.split('-exp-')[1];
const shouldOpen = !sidePanelOpen; // Use sidePanelOpen to determine toggle state
setSidePanelOpen(shouldOpen);
// Toggle logic - close if same study is clicked again, open if different study
if (currentStudyUID === lastActivatedStudyUID && sidePanelOpen) {
// Same study - close panel
setSidePanelOpen(false);
setActiveTabIndex(null);
setLastActivatedStudyUID('');
} else {
// Different study or panel was closed - open panel with new study
setSidePanelOpen(true);
setActiveTabIndex(tabIndex !== -1 ? tabIndex : null);
setStudyInstanceUID(currentStudyUID);
setLastActivatedStudyUID(currentStudyUID);
}
if (shouldOpen) {
setActiveTabIndex(tabIndex !== -1 ? tabIndex : null);
} else {
setActiveTabIndex(null);
}
} else if (tabIndex !== -1) {
setActiveTabIndex(tabIndex);
}
@@ -109,7 +97,7 @@ const SidePanelWithServices = ({
return () => {
activatePanelSubscription.unsubscribe();
};
}, [tabs, sidePanelOpen, panelService, lastActivatedStudyUID]);
}, [tabs, sidePanelOpen, panelService]);
return (
<SidePanel
@@ -122,7 +110,6 @@ const SidePanelWithServices = ({
onActiveTabIndexChange={handleActiveTabIndexChange}
expandedWidth={expandedWidth}
servicesManager={servicesManager} // Pass servicesManager ke SidePanel
studyInstanceUID={studyInstanceUID}
/>
);
};

View File

@@ -85,15 +85,6 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
if (authHeaders && authHeaders.Authorization) {
xhrRequestHeaders.Authorization = authHeaders.Authorization;
}
const authToken = sessionStorage.getItem('ohif-auth-token');
if (!authToken) {
// window.location.href = '/login'; // Kalau mau pakae login tinggal uncomment
window.location.href = '/';
return;
}
xhrRequestHeaders.Authorization = `Bearer ${authToken}`;
return xhrRequestHeaders;
};

View File

@@ -6,8 +6,7 @@ function OHIFCornerstonePdfViewport({ displaySets }) {
var [url, setUrl] = useState(null);
const sopInstanceUid = displaySets[0].SOPInstanceUID;
url = `${window.config.pacs_document_host}:${window.config.pacs_document_port}/rid/IHERetrieveDocument?requestType=DOCUMENT&documentUID=${sopInstanceUid}&preferredContentType=application%2Fpdf`;
console.log('URL PDF', url);
url = `http://128.199.154.150:8080/rid/IHERetrieveDocument?requestType=DOCUMENT&documentUID=${sopInstanceUid}&preferredContentType=application%2Fpdf`;
useEffect(() => {
document.body.addEventListener('drag', makePdfDropTarget);

View File

@@ -42,7 +42,8 @@
"@ohif/extension-default": "3.9.1",
"@ohif/extension-dicom-pdf": "3.9.1",
"@ohif/extension-dicom-video": "3.9.1",
"@ohif/extension-measurement-tracking": "3.9.1"
"@ohif/extension-measurement-tracking": "3.9.1",
"expertise-panel": "^0.0.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",

View File

@@ -72,6 +72,7 @@ const extensionDependencies = {
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
'expertise-panel': '^0.0.1',
};
function modeFactory({ modeConfiguration }) {
@@ -181,7 +182,7 @@ function modeFactory({ modeConfiguration }) {
return {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList],
leftPanels: [tracked.thumbnailList, 'expertise-panel.panelModule.ExpertiseSidePanel'],
rightPanels: [cornerstone.segmentation, tracked.measurements],
rightPanelClosed: true,
viewports: [

View File

@@ -25,7 +25,7 @@ const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
const PROXY_PATH_REWRITE_FROM = process.env.PROXY_PATH_REWRITE_FROM;
const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
const OHIF_PORT = Number(process.env.OHIF_PORT || 3030);
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
const Dotenv = require('dotenv-webpack');
const writePluginImportFile = require('./writePluginImportsFile.js');
@@ -78,6 +78,10 @@ module.exports = (env, argv) => {
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
SRC_DIR,
path.resolve(
__dirname,
'/home/mario/Works/ohif-viewer/expertise-panel-ext/expertise-panel/node_modules'
),
],
},
plugins: [

View File

@@ -61,6 +61,10 @@
"packageName": "@ohif/extension-cornerstone-dicom-rt",
"default": false,
"version": "3.0.0"
},
{
"packageName": "expertise-panel",
"version": "0.0.1"
}
],
"modes": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,16 +1,11 @@
/** @type {AppTypes.Config} */
function sas_get_token() {
//implement token here
return '';
}
window.config = {
sasGetToken: sas_get_token,
routerBasename: '/',
// whiteLabeling: {},
extensions: [],
modes: [],
customizationService: {},
showStudyList: false,
showStudyList: true,
// some windows systems have issues with more than 3 web workers
maxNumberOfWebWorkers: 3,
// below flag is for performance reasons, but it might not work for all servers
@@ -28,9 +23,8 @@ window.config = {
prefetch: 25,
},
expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay)
expertise_host: `https://devone.aplikasi.web.id/one-api/mockup/pacsmwl/Workorder/get_dummy_expertise`, //* Tambahan untuk fetch data Expertise)
pacs_document_host: `${window.location.hostname}`,
pacs_document_port: 8080,
expertise_host: 'http://128.199.154.150', //* Tambahan untuk fetch data Expertise)
// filterQueryParam: false,
// defaultDataSourceName: 'dicomweb',
defaultDataSourceName: 'local-proxy',
@@ -51,8 +45,8 @@ window.config = {
configuration: {
friendlyName: 'Static WADO Local Data',
name: 'DCM4CHEE',
qidoRoot: `http://${window.location.hostname}:5000/rs`,
wadoRoot: `http://${window.location.hostname}:5000/rs`,
qidoRoot: 'http://128.199.154.150:5000/rs',
wadoRoot: 'http://128.199.154.150:5000/rs',
qidoSupportsIncludeField: false,
supportsReject: true,
supportsStow: true,
@@ -69,7 +63,6 @@ window.config = {
},
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
sourceName: 'dicomwebproxy',

View File

@@ -1,57 +0,0 @@
/** @type {AppTypes.Config} */
window.config = {
routerBasename: '/',
pacs_document_host: `http://152.42.173.210`,
pacs_document_port: 8585,
goProxyHost: `http://152.42.173.210:5555`,
expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay)
expertise_host: `https://devone.aplikasi.web.id/one-api/mockup/pacsmwl/Workorder/get_dummy_expertise`, //* Tambahan untuk fetch data Expertise)
enableGoogleCloudAdapter: false,
// below flag is for performance reasons, but it might not work for all servers
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
extensions: [],
modes: [],
showStudyList: false,
// filterQueryParam: false,
defaultDataSourceName: 'dicomweb',
dataSources: [
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
configuration: {
friendlyName: 'dcmjs DICOMWeb Server',
name: 'GCP',
wadoUriRoot: `http://152.42.173.210:5555/dicomWeb`,
qidoRoot: `http://152.42.173.210:5555/dicomWeb`,
wadoRoot: `http://152.42.173.210:5555/dicomWeb`,
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
dicomUploadEnabled: false,
omitQuotationForMultipartRequest: true,
configurationAPI: 'ohif.dataSourceConfigurationAPI.google',
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
friendlyName: 'dicom json',
name: 'json',
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
sourceName: 'dicomlocal',
configuration: {
friendlyName: 'dicom local',
},
},
],
};

View File

@@ -1,20 +1,47 @@
/** @type {AppTypes.Config} */
window.config = {
routerBasename: '/',
pacs_document_host: `https://${window.location.hostname}`,
pacs_document_port: 8585,
goProxyHost: `https://${window.location.hostname}:5555`,
expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay)
expertise_host: `https://devone.aplikasi.web.id/one-api/mockup/pacsmwl/Workorder/get_dummy_expertise`, //* Tambahan untuk fetch data Expertise)
enableGoogleCloudAdapter: false,
// below flag is for performance reasons, but it might not work for all servers
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// This is an array, but we'll only use the first entry for now
oidc: [
{
// ~ REQUIRED
// Authorization Server URL
authority: 'https://accounts.google.com',
client_id: '382212153306-7q39hdie4ecj0uhemkitvedo93bnvfhn.apps.googleusercontent.com',
redirect_uri: '/callback',
response_type: 'id_token token',
scope:
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
// Tambahan dari Google CLoud Secret
project_id: "westone-433204",
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://oauth2.googleapis.com/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_secret: "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",
redirect_uris: [
"http://devkedungdoro.aplikasi.web.id:3000/callback"
],
javascript_origins: [
"https://devone.aplikasi.web.id",
"http://devkedungdoro.aplikasi.web.id:3000"
]
},
],
extensions: [],
modes: [],
showStudyList: false,
showStudyList: true,
// filterQueryParam: false,
defaultDataSourceName: 'dicomweb',
dataSources: [
@@ -24,18 +51,27 @@ window.config = {
configuration: {
friendlyName: 'dcmjs DICOMWeb Server',
name: 'GCP',
wadoUriRoot: `https://${window.location.hostname}:5555/dicomWeb`,
qidoRoot: `https://${window.location.hostname}:5555/dicomWeb`,
wadoRoot: `https://${window.location.hostname}:5555/dicomWeb`,
wadoUriRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
qidoRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
wadoRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsFuzzyMatching: true,
supportsWildcard: true,
dicomUploadEnabled: false,
dicomUploadEnabled: true,
omitQuotationForMultipartRequest: true,
configurationAPI: 'ohif.dataSourceConfigurationAPI.google',
defaultDicomStoreConfiguredItems: {
id: 'projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage',
itemType: '3',
name: 'ohif',
url: 'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif'
},
},
},
{

View File

@@ -14,21 +14,9 @@
name="mobile-web-app-capable"
content="yes"
/>
<meta
property="og:title"
content="SAS Dicom Viewer"
/>
<meta
property="og:description"
content="Web-based Cloud DICOM Viewer by SAS for viewing and analyzing DICOM images. Powered by Open Health Imaging Foundation and Google Cloud Healthcare."
/>
<meta
property="og:image"
content="<%= PUBLIC_URL %>assets/logo-sismedika.png"
/>
<meta
name="application-name"
content="SAS Dicom Viewer"
content="OHIF Viewer"
/>
<meta
name="apple-mobile-web-app-capable"
@@ -208,7 +196,7 @@
src="<%= PUBLIC_URL %>init-service-worker.js"
></script>
<title>SAS Dicom Viewer</title>
<title>OHIF Viewer</title>
<!-- WEB FONTS -->
<link
@@ -228,6 +216,7 @@
}
</script>
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
@@ -247,6 +236,7 @@
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="react-portal"></div>
<div id="root"></div>
<div id="root">
</div>
</body>
</html>

View File

@@ -14,21 +14,9 @@
name="mobile-web-app-capable"
content="yes"
/>
<meta
property="og:title"
content="SAS Dicom Viewer"
/>
<meta
property="og:description"
content="Web-based Cloud DICOM Viewer by SAS for viewing and analyzing DICOM images. Powered by Open Health Imaging Foundation and Google Cloud Healthcare."
/>
<meta
property="og:image"
content="<%= PUBLIC_URL %>assets/logo-sismedika.png"
/>
<meta
name="application-name"
content="SAS Dicom Viewer"
content="OHIF Viewer"
/>
<meta
name="apple-mobile-web-app-capable"
@@ -203,7 +191,7 @@
src="<%= PUBLIC_URL %>init-service-worker.js"
></script>
<title>SAS Dicom Viewer</title>
<title>OHIF Viewer</title>
<!-- WEB FONTS -->
<link

View File

@@ -36,49 +36,6 @@ import createRoutes from './routes';
import appInit from './appInit.js';
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
import { ShepherdJourneyProvider } from 'react-shepherd';
import { initializeCustomAuth } from './utils/initUserAuthenticationService';
function injectAuth() {
console.log('---> Inject Auth');
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
// Kalau ingin disable study list (Role Patient)
// window.config.showStudyList = false;
const authToken = sessionStorage.getItem('ohif-auth-token');
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
this._url = url; // Save URL if you want conditional logic
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this.setRequestHeader('Authorization', `Bearer ${authToken}`);
this.addEventListener('readystatechange', function () {
if (this.readyState === 4) {
// readyState 4 = DONE
try {
// Check for auth errors (401/403) and redirect to login if needed
if (this.status === 401 || this.status === 403) {
window.sessionStorage.removeItem('ohif-auth-token');
// window.location.href = '/login'; // Kalau mau pakae login tinggal uncomment
}
} catch (e) {
console.error('Error handling auth response:', e);
}
}
});
return originalXHRSend.apply(this, arguments);
};
}
// Setup token access function
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
// Enable auth token injection
// injectAuth();
let commandsManager: CommandsManager,
extensionManager: ExtensionManager,
@@ -151,9 +108,6 @@ function App({
customizationService,
} = servicesManager.services;
// Initialize our custom authentication service
initializeCustomAuth(userAuthenticationService);
const providers = [
[AppConfigProvider, { value: appConfigState }],
[UserAuthenticationProvider, { service: userAuthenticationService }],

View File

@@ -1,138 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserAuthentication } from '@ohif/ui';
import { Icons } from '@ohif/ui-next';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [, authContext] = useUserAuthentication();
// Get the intended destination from URL query params or default to home
const searchParams = new URLSearchParams(location.search);
const redirectPath = searchParams.get('redirect') || '/';
// Check if already authenticated
useEffect(() => {
const token = window.sessionStorage.getItem('ohif-auth-token');
if (token) {
// Already logged in, redirect to destination
navigate(redirectPath, { replace: true });
}
}, [redirectPath, navigate]);
const handleLogin = async e => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Use window.config.goProxyHost for authentication endpoint
const proxyHost = window.config?.goProxyHost || `https://${window.location.hostname}:5555`;
const authEndpoint = `${proxyHost}/auth/login`;
// Call go-ohif-proxy login endpoint
const response = await fetch(authEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed. Please check your credentials.');
}
const data = await response.json();
// Store token in sessionStorage
window.sessionStorage.setItem('ohif-auth-token', data.access_token);
// Decode token to extract role and user information
let userInfo = data.user;
// Update the auth context
authContext.setUser({
...userInfo,
token: data.access_token,
});
// Set window.config.sasGetToken for the injectAuth function
if (window.config) {
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
}
// Handle role-specific redirects if specified in response
if (data.redirect_url) {
navigate(data.redirect_url, { replace: true });
} else {
// Redirect to the original destination
navigate(redirectPath, { replace: true });
}
} catch (error) {
console.error('Login error:', error);
setError(error.message || 'Failed to log in. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<div className="flex h-screen w-screen items-center justify-center bg-black">
<div className="bg-popover w-88 rounded p-8 shadow-lg">
<div className="mb-4 flex justify-center">
<Icons.OHIFLogo className="h-12 text-white" />
</div>
<h2 className="text-md text-center font-bold text-white">Login to</h2>
<h1 className="mb-8 text-center text-2xl font-bold text-white">Cloud DICOM Viewer</h1>
{error && <div className="mb-4 rounded bg-red-800 px-4 py-2 text-white">{error}</div>}
<form onSubmit={handleLogin}>
<div className="mb-4">
<label className="mb-2 block text-sm font-bold text-white">Email</label>
<input
type="text"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={email}
onChange={e => setEmail(e.target.value)}
required
/>
</div>
<div className="mb-6">
<label className="mb-2 block text-sm font-bold text-white">Password</label>
<input
type="password"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
className="focus:shadow-outline w-full rounded bg-blue-500 py-2 px-4 font-bold text-white hover:bg-blue-700 focus:outline-none"
disabled={isLoading}
>
{isLoading ? 'Logging in...' : 'Log In'}
</button>
</div>
<p className="text-muted-foreground mt-8 text-center text-sm">
Powered by OHIF & Google Cloud DICOM Storage
</p>
</form>
</div>
</div>
);
};
export default Login;

View File

@@ -1,139 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserAuthentication } from '@ohif/ui';
import { Icons } from '@ohif/ui-next';
const ShortlinkLogin = () => {
const [dob, setDob] = useState('');
const [shortToken, setShortToken] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [, authContext] = useUserAuthentication();
// Parse the short token from URL query params
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const token = searchParams.get('short');
if (token) {
setShortToken(token);
} else {
// No short token found, redirect to regular login
setError('No shortlink token found in URL');
setTimeout(() => {
navigate('/', { replace: true });
}, 3000);
}
}, [location.search, navigate]);
// Handle form submission
const handleSubmit = async e => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Use window.config.goProxyHost for authentication endpoint
const proxyHost = window.config?.goProxyHost || `https://${window.location.hostname}:5555`;
const authEndpoint = `${proxyHost}/auth/shortlink`;
// Call the shortlink authentication endpoint
const response = await fetch(authEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ short_token: shortToken, dob }),
});
if (!response.ok) {
throw new Error('Authentication failed. Please check your date of birth and try again.');
}
const data = await response.json();
// Store token in sessionStorage
window.sessionStorage.setItem('ohif-auth-token', data.access_token);
// Decode token to extract user information (if available in token)
let userInfo = data.user;
// Update the auth context
authContext.setUser({
...userInfo,
token: data.access_token,
});
// Set window.config.sasGetToken for the injectAuth function
if (window.config) {
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
}
// Navigate to the viewer page with the authenticated patient's study
// The actual URL would depend on how studies are loaded in your OHIF instance
if (data.redirect_url) {
navigate(data.redirect_url, { replace: true });
} else {
// Default navigation if no specific redirect is provided
navigate('/', { replace: true });
}
} catch (error) {
console.error('Authentication error:', error);
setError(error.message || 'Failed to authenticate. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleDateChange = e => {
// Format date input as YYYY-MM-DD
setDob(e.target.value);
};
return (
<div className="flex h-screen w-screen items-center justify-center bg-black">
<div className="bg-popover w-88 rounded p-8 shadow-lg">
<div className="mb-4 flex justify-center">
<Icons.OHIFLogo className="h-12 text-white" />
</div>
<h1 className="mb-8 text-center text-2xl font-bold text-white">Cloud DICOM Viewer</h1>
{error && <div className="mb-4 rounded bg-red-800 px-4 py-2 text-white">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label className="mb-2 block text-sm font-bold text-white">
Masukkan tanggal lahir Anda:
</label>
<input
type="date"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={dob}
onChange={handleDateChange}
required
/>
<p className="mt-1 text-xs text-gray-400">Format: Bulan - Tanggal - Tahun</p>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
className="focus:shadow-outline w-full rounded bg-blue-500 py-2 px-4 font-bold text-white hover:bg-blue-700 focus:outline-none"
disabled={isLoading || !shortToken}
>
{isLoading ? 'Verifying...' : 'View'}
</button>
</div>
<p className="text-muted-foreground mt-8 text-center text-sm">
Powered by OHIF & Google Cloud DICOM Storage
</p>
</form>
</div>
</div>
);
};
export default ShortlinkLogin;

View File

@@ -85,38 +85,6 @@ function WorkList({
const debouncedFilterValues = useDebounce(filterValues, 200);
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
/*
* Patch untuk Role checking patient gabisa akses ke study list
*/
const token = window.sessionStorage.getItem('ohif-auth-token');
if (!token) {
return;
}
const decodedToken = decodeToken(token);
// Check jika 'role' = 'patient' tapi akses '/' return ke viewer
if (decodedToken && decodedToken.role === 'patient') {
const currentPath = window.location.pathname + window.location.search;
if (currentPath === '/') {
console.log(
'User is a patient and trying to access the root path. Redirecting to his/her home URL.'
);
window.location.href = `${decodedToken.home_url}`;
}
}
function decodeToken(token) {
try {
const payload = token.split('.')[1];
if (payload) {
return JSON.parse(atob(payload));
}
} catch (e) {
console.error('Error parsing JWT token', e);
}
return null;
}
/*
* The default sort value keep the filters synchronized with runtime conditional sorting
* Only applied if no other sorting is specified and there are less than 101 studies
@@ -574,7 +542,7 @@ function WorkList({
/>
<Onboarding />
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
<div className="flex h-full flex-col overflow-y-auto">
<div className="flex flex-col h-full overflow-y-auto">
<ScrollArea>
<div className="flex grow flex-col">
<StudyListFilter
@@ -590,7 +558,9 @@ function WorkList({
// ? () => dataSourceConfigurationComponent()
// : undefined
// }
getDataSourceConfigurationComponent={undefined}
getDataSourceConfigurationComponent={
undefined
}
/>
</div>
{hasStudies ? (

View File

@@ -12,8 +12,6 @@ import buildModeRoutes from './buildModeRoutes';
import PrivateRoute from './PrivateRoute';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Login from './Login';
import ShortlinkLogin from './ShortlinkLogin';
const NotFoundServer = ({
message = 'Unable to query for studies at this time. Check your data source configuration or network connection',
@@ -76,15 +74,6 @@ const bakedInRoutes = [
path: '/localbasic',
children: Local.bind(null, { modePath: 'viewer/dicomlocal' }),
},
// * Custom Patch untuk Login go-ohif-proxy
{
path: '/login',
children: Login,
},
{
path: '/short-auth',
children: ShortlinkLogin,
},
];
// NOT FOUND (404)

View File

@@ -1,73 +0,0 @@
/**
* Initializes the custom authentication service for OHIF Viewer
* to work with go-ohif-proxy authentication
*/
export function initializeCustomAuth(userAuthenticationService) {
// Set up the authentication service with custom implementation
userAuthenticationService.setServiceImplementation({
// Custom implementation to handle unauthenticated users
handleUnauthenticated: () => {
// Check if there's a shortlink token in the URL
const urlParams = new URLSearchParams(window.location.search);
const shortToken = urlParams.get('short');
// If there's a shortlink token, redirect to the shortlink login page
if (shortToken) {
window.location.href = `/short-auth?short=${shortToken}`;
return null;
}
// Otherwise, handle as normal login flow
// Get the current path for redirect after login
const currentPath = window.location.pathname + window.location.search;
// Clear any existing tokens
window.sessionStorage.removeItem('ohif-auth-token');
// Redirect to login page with the redirect URL in query params
// window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
window.location.href = '/';
// Return null to prevent rendering while redirecting
return null;
},
// Custom implementation to get the authorization header
// di ohif3.9.1 ini sepertinya masih development
// getAuthorizationHeader: () => {
// const token = window.sessionStorage.getItem('ohif-auth-token');
// return token ? `Bearer ${token}` : undefined;
// },
});
// Set authentication as enabled
userAuthenticationService.set({ enabled: true });
// Check if we already have a token and set the user if we do
const token = window.sessionStorage.getItem('ohif-auth-token');
if (!token) {
return;
}
const decodedToken = decodeToken(token);
// Check jika 'role' = 'patient' tapi akses '/' return ke viewer
if (decodedToken && decodedToken.role === 'patient') {
const currentPath = window.location.pathname + window.location.search;
if (currentPath === '/') {
console.log('User is a patient and trying to access the root path. Redirecting to /patient.');
window.location.href = `${decodedToken.home_url}`;
}
}
function decodeToken(token) {
try {
const payload = token.split('.')[1];
if (payload) {
return JSON.parse(atob(payload));
}
} catch (e) {
console.error('Error parsing JWT token', e);
}
return null;
}
}

View File

@@ -154,7 +154,6 @@ const SidePanel = ({
expandedWidth = 280,
onActiveTabIndexChange,
servicesManager, // Tambah servicesManager as a prop
studyInstanceUID,
}) => {
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(0);
@@ -167,8 +166,8 @@ const SidePanel = ({
const [viewportData, setViewportData] = useState(null);
// Harusnya (viewportId), tapi karena gabutuh perubahan viewport maka dihardcode 'default'
// const viewportInfo = cornerstoneViewportService.getViewportInfo('default');
// const studyInstanceUID = viewportInfo?.viewportData?.data?.[0]?.StudyInstanceUID || '';
const viewportInfo = cornerstoneViewportService.getViewportInfo('default');
const studyInstanceUID = viewportInfo?.viewportData?.data?.[0]?.StudyInstanceUID || '';
const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
const baseStyle = createBaseStyle(expandedWidth);
@@ -294,20 +293,9 @@ const SidePanel = ({
return;
}
// Create request headers with Authorization if token exists
const authToken = window.sessionStorage.getItem('ohif-auth-token');
const headers: HeadersInit = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
// Fetch data with specific fields including Accession Number
const response = await fetch(
`${qidoRootUrl}/studies?includefield=00080050&StudyInstanceUID=${studyInstanceUID}`,
{
headers,
}
`${qidoRootUrl}/studies?includefield=00080050&StudyInstanceUID=${studyInstanceUID}`
);
if (!response.ok) {
@@ -367,21 +355,53 @@ const SidePanel = ({
}
setIsExpertiseLoading(true);
const url = `${window.config.expertise_host}?accessionNo=${accessionNumber}`;
const url = `${window.config.expertise_host}/nv/query.php?method=view&AccessionNumber=${encodeURIComponent(accessionNumber)}`;
const response = await fetch(url);
const data = await response.json();
if (data?.status === 'OK' && data?.data) {
// Create expertise data in the format expected by your component
const formattedExpertiseData = {
ordering_physician: data.data.senderDoctorName,
radiologist: data.data.pjDoctorName,
expertise_dttm: data.data.expTime,
// Convert the expertise object to a string format that parseExpertise can handle
expertise: formatExpertiseToString(data.data.expertise),
};
// console.log('Study data:', data);
setExpertiseData(formattedExpertiseData);
const data = {
study: {
accession_no: 'CR.250411.001',
study_iuid: '1.2.826.0.1.3680043.9.7307.1.20250411001',
study_description: '',
study_datetime: '20250411093937',
number_of_series: '1',
number_of_instances: '1',
modality: 'CR',
patient_mrn: '00000941',
patient_name: 'NEFANNY RIDWAN',
patient_sex: 'F',
patient_date_of_birth: '19881127',
patient_age: '36Y 4M 14D',
expertise: [
{
expertise:
'Keterangan : MCU\r\n\r\nRadiografi Thorax PA (inspirasi kurang)\r\n\r\nCor : besar dan bentuk normal\r\nPulmo : tak tampak infiltrat\r\nTrachea tampak di tengah\r\nSinus phrenicocostalis kanan kiri tajam\r\nHemidiafragma kanan kiri tampak baik\r\nTulang-tulang tampak baik\r\nSoft tissue tak tampak kelainan\r\n\r\nKesan :\r\nTidak tampak kelainan signifikan pada pemeriksaan saat ini\r\n\r\nBTK,',
radiologist: 'dr. Hendra Boy Situmorang, Sp.Rad ',
expertise_dttm: '2025-04-11 09:43',
radiologist_edit: null,
expertise_edit_dttm: '0000-00-00 00:00',
ordering_physician: 'dr. Laksmitasari Dewi',
},
],
series: [
{
series_number: '1',
series_iuid: '1.2.156.112536.2.560.28134011043131122.1519098341436.1',
series_description: 'V04_0014',
number_of_instances: 1,
thumbnail:
'http://192.168.22.3/nv/wado_proxy_thumb.php?requestType=WADO&studyUID=1.2.826.0.1.3680043.9.7307.1.20250411001&seriesUID=1.2.156.112536.2.560.28134011043131122.1519098341436.1&objectUID=1.2.156.112536.2.560.28134011043131122.1519098341436.4&rows=123',
sop_iuids: ['1.2.156.112536.2.560.28134011043131122.1519098341436.4'],
},
],
},
};
if (data?.study?.expertise && data.study.expertise.length > 0) {
setExpertiseData(data.study.expertise[0]);
}
} catch (error) {
console.error('Error fetching expertise data:', error);
@@ -390,32 +410,6 @@ const SidePanel = ({
}
};
// Helper function to convert expertise object to string format
const formatExpertiseToString = expertiseObj => {
if (!expertiseObj) return '';
let result = '';
// Add each section with proper formatting
if (expertiseObj.Indikasi) {
result += `KLINIS: ${expertiseObj.Indikasi}\r\n`;
}
if (expertiseObj.Teknik) {
result += `TEKNIK: ${expertiseObj.Teknik}\r\n\r\n`;
}
if (expertiseObj.Deskripsi) {
result += `KETERANGAN:\r\n${expertiseObj.Deskripsi.replace(/\n/g, '\r\n')}\r\n\r\n`;
}
if (expertiseObj.Kesan) {
result += `KESAN: ${expertiseObj.Kesan}`;
}
return result;
};
const getExpertisePanel = () => {
if (side !== 'right') return null; // Only show in the right side panel
@@ -668,7 +662,6 @@ SidePanel.propTypes = {
onActiveTabIndexChange: PropTypes.func,
expandedWidth: PropTypes.number,
servicesManager: PropTypes.object.isRequired, // Tambah servicesManager prop
studyInstanceUID: PropTypes.string, // Tambahkan prop studyInstanceUID
};
export { SidePanel };

View File

@@ -63,7 +63,6 @@ const StudyBrowser = ({
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
servicesManager={servicesManager} // Pass servicesManager ke Study Item
studyInstanceUid={studyInstanceUid}
/>
</React.Fragment>
);

View File

@@ -21,10 +21,7 @@ const StudyItem = ({
viewPreset = 'thumbnails',
onThumbnailContextMenu,
servicesManager, // Tambah servicesManager as a prop
studyInstanceUid = '',
}: withAppTypes) => {
// FETCHING ACCESSION NUMBER DAN EXPERTISE
return (
<Accordion
type="single"
@@ -62,17 +59,16 @@ const StudyItem = ({
<>
{/* Expertise Button */}
<div
className="bg-primary-dark hover:bg-primary-active mx-8 my-4 cursor-pointer rounded-lg border border-white py-3 text-center text-white"
className="bg-primary-dark hover:bg-primary-active my-4 w-full cursor-pointer border border-white py-2 text-center text-white"
onClick={() => {
// Trigger the expertise panel in the right side panel (segmentation Panel)
servicesManager.services.panelService.activatePanel(
// '@ohif/extension-cornerstone.panelModule.panelSegmentation-exp',
`@ohif/extension-cornerstone.panelModule.panelSegmentation-exp-${studyInstanceUid}`,
'@ohif/extension-cornerstone.panelModule.panelSegmentation-exp',
true
);
}}
>
Expertise
View Expertise
</div>
{/* Thumbnails */}
@@ -109,7 +105,6 @@ StudyItem.propTypes = {
onClickUntrack: PropTypes.func,
viewPreset: PropTypes.string,
servicesManager: PropTypes.object.isRequired, // Tambah servicesManager prop
studyInstanceUid: PropTypes.string.string,
};
export { StudyItem };