Skip to content

Commit 2a142a8

Browse files
committed
Merge remote-tracking branch 'origin/Preview' into dev
2 parents d4f7253 + d57e83f commit 2a142a8

13 files changed

+20284
-30
lines changed

package-lock.json

+19,457
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { previewInstance } from "../../services/instances"
2+
import { useCustomQuery } from "../../utils"
3+
4+
type PreviewInstanceProps = {
5+
instanceUID: string
6+
}
7+
const PreviewInstance = ({ instanceUID }: PreviewInstanceProps) => {
8+
9+
const { data } = useCustomQuery<any, string>(
10+
['instances', instanceUID, 'preview'],
11+
() => previewInstance(instanceUID),
12+
{
13+
select: (data: any) => {
14+
return URL.createObjectURL(data);
15+
},
16+
staleTime : 100000,
17+
gcTime : 100000
18+
}
19+
20+
)
21+
22+
return (
23+
<img className="w-full h-full" src={data}/>
24+
)
25+
26+
}
27+
28+
export default PreviewInstance

src/content/series/PreviewSeries.tsx

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Component for a modal to preview a series
3+
* @name PreviewSeries
4+
*/
5+
6+
import React, { ChangeEvent, useMemo, useState } from "react";
7+
import { getInstancesOfSeries } from "../../services/orthanc";
8+
import { useCustomQuery } from "../../utils";
9+
10+
import { Input, Modal, Spinner } from "../../ui";
11+
import PreviewInstance from "./PreviewInstance";
12+
import { Instances } from "../../utils/types";
13+
14+
type PreviewSeriesProps = {
15+
seriesId: string;
16+
onClose: () => void;
17+
show: boolean;
18+
}
19+
20+
const PreviewSeries: React.FC<PreviewSeriesProps> = ({ seriesId, onClose, show }) => {
21+
22+
const [rows, setRows] = useState(1)
23+
const [columns, setColumns] = useState(3)
24+
25+
const [imageIndex, setImageIndex] = useState(0)
26+
27+
const pageSize = useMemo(() => {
28+
return rows * columns
29+
}, [rows, columns])
30+
31+
const { data: instanceUIDs, isLoading } = useCustomQuery(
32+
['series', seriesId, 'instances'],
33+
() => getInstancesOfSeries(seriesId),
34+
{
35+
select: (instances: Instances[]) => {
36+
return instances.sort((a, b) => a.indexInSeries - b.indexInSeries)
37+
}
38+
}
39+
)
40+
41+
const selectedInstanceUIDs = useMemo(() => {
42+
if (!instanceUIDs) return null
43+
const start = Math.max(imageIndex, 0)
44+
const end = Math.min(start + (pageSize - 1), instanceUIDs.length - 1)
45+
console.log(start, end)
46+
const selectedUIDs = []
47+
for (let i = start; i <= end; i++) {
48+
selectedUIDs.push(instanceUIDs[i])
49+
}
50+
51+
return selectedUIDs
52+
53+
}, [pageSize, imageIndex, instanceUIDs])
54+
55+
if (isLoading) return <Spinner />
56+
57+
const handleColumnChange = (event: ChangeEvent<HTMLInputElement>) => {
58+
setColumns(Number(event.target.value));
59+
};
60+
61+
const handleRowChange = (event: ChangeEvent<HTMLInputElement>) => {
62+
setRows(Number(event.target.value));
63+
};
64+
65+
return (
66+
<Modal show={show} size='xl'>
67+
<Modal.Header onClose={onClose} >
68+
<Modal.Title>Preview Series</Modal.Title>
69+
</Modal.Header>
70+
<Modal.Body>
71+
<div className={"flex w-full h-full"} style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gridTemplateRows: `repeat(${rows}, 1fr)` }}>
72+
{
73+
selectedInstanceUIDs?.map((instance: Instances) => {
74+
return <PreviewInstance key={instance.id} instanceUID={instance.id} />
75+
})
76+
}
77+
</div>
78+
<input className="w-full" type="range" value={imageIndex} min={0} max={(instanceUIDs?.length ?? 1) - 1} onChange={(event: ChangeEvent<HTMLInputElement>) => setImageIndex(Number(event.target.value))} />
79+
<div className={"flex w-full justify-center"}>
80+
<div className="flex gap-3">
81+
<Input
82+
label="Columns :"
83+
type="number"
84+
id="number"
85+
name="columns"
86+
min={0}
87+
max={10}
88+
value={columns}
89+
onChange={handleColumnChange}
90+
/>
91+
<Input
92+
label="Rows :"
93+
type="number"
94+
id="number"
95+
name="rows"
96+
min={1}
97+
max={10}
98+
value={rows}
99+
onChange={handleRowChange}
100+
/>
101+
</div>
102+
</div>
103+
</Modal.Body>
104+
</Modal>
105+
);
106+
};
107+
108+
export default PreviewSeries;

src/content/series/SeriesActions.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SeriesActions.tsx
22
import React from 'react';
3-
import { FaEdit, FaTrash } from "react-icons/fa";
3+
import { FaEdit, FaEye, FaTrash } from "react-icons/fa";
44
import { Series } from "../../utils/types";
55
import DropdownButton from '../../ui/menu/DropDownButton';
66

@@ -23,6 +23,12 @@ const SeriesActions: React.FC<SeriesActionsProps> = ({ series, onActionClick })
2323
color: 'red',
2424
action: () => onActionClick('delete', series)
2525
},
26+
{
27+
label: 'Preview Series',
28+
icon: <FaEye />,
29+
color: 'green',
30+
action: () => onActionClick('preview', series),
31+
},
2632
];
2733

2834
const handleClick = (e: React.MouseEvent) => {

src/content/series/SeriesRoot.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getSeriesOfStudy, deleteSeries } from '../../services/orthanc';
88
import { Series } from '../../utils/types';
99
import SeriesTable from './SeriesTable';
1010
import EditSeries from './EditSeries';
11+
import PreviewSeries from './PreviewSeries';
1112
import { useConfirm } from '../../services/ConfirmContextProvider';
1213
import { useCustomToast } from '../../utils/toastify';
1314
import { Spinner } from '../../ui';
@@ -18,6 +19,7 @@ interface SeriesRootProps {
1819

1920
const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
2021
const [editingSeries, setEditingSeries] = useState<Series | null>(null);
22+
const [previewSeries, setPreviewSeries] = useState<Series | null>(null);
2123

2224
const { confirm } = useConfirm();
2325
const { toastSuccess, toastError } = useCustomToast();
@@ -49,6 +51,10 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
4951
setEditingSeries(series);
5052
};
5153

54+
const handlePreviewSeries = (series: Series) => {
55+
setPreviewSeries(series);
56+
}
57+
5258
const handleDeleteSeries = async (seriesId: string) => {
5359
const confirmContent = (
5460
<div className="italic">
@@ -70,6 +76,9 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
7076
case 'delete':
7177
handleDeleteSeries(series.id);
7278
break;
79+
case 'preview':
80+
handlePreviewSeries(series);
81+
break;
7382
default:
7483
console.log(`Unhandled action: ${action}`);
7584
}
@@ -100,6 +109,13 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
100109
show={!!editingSeries}
101110
/>
102111
)}
112+
{previewSeries && (
113+
<PreviewSeries
114+
seriesId={previewSeries.id}
115+
onClose={() => setPreviewSeries(null)}
116+
show={!!previewSeries}
117+
/>
118+
)}
103119
</div>
104120
);
105121
};

src/content/studies/PreviewStudy.tsx

Whitespace-only changes.

src/content/studies/StudyActions.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// StudyActions.tsx
22
import React from 'react';
3-
import { FaEdit, FaTrash } from "react-icons/fa";
3+
import { FaEdit, FaEye, FaTrash } from "react-icons/fa";
44
import { StudyMainDicomTags } from "../../utils/types";
55
import DropdownButton from '../../ui/menu/DropDownButton';
66

@@ -23,6 +23,12 @@ const StudyActions: React.FC<StudyActionsProps> = ({ study, onActionClick }) =>
2323
color: 'red',
2424
action: () => onActionClick('delete', study.id)
2525
},
26+
{
27+
label: 'Preview Study',
28+
icon: <FaEye />,
29+
color: 'green',
30+
action: () => onActionClick('preview', study.id)
31+
},
2632
];
2733

2834
const handleClick = (e: React.MouseEvent) => {

src/content/studies/StudyRoot.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useCustomMutation } from '../../utils/reactQuery';
33
import { deleteStudy } from '../../services/orthanc';
44
import StudyTable from './StudyTable';
55
import EditStudy from './EditStudy';
6+
// import PreviewStudy from './PreviewStudy';
67
import { useConfirm } from '../../services/ConfirmContextProvider';
78
import { useCustomToast } from '../../utils/toastify';
89
import Patient from '../../model/Patient';
@@ -13,9 +14,9 @@ type StudyRootProps = {
1314
onStudySelected?: (studyId: string) => void;
1415
}
1516

16-
const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudySelected}) => {
17+
const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudySelected }) => {
1718
const [editingStudy, setEditingStudy] = useState<string | null>(null);
18-
19+
1920
const { confirm } = useConfirm();
2021
const { toastSuccess, toastError } = useCustomToast();
2122

@@ -33,11 +34,11 @@ const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudyS
3334
onStudyUpdated();
3435
},
3536
onError: (error: any) => {
36-
toastError('Failed to delete study: ' + error);
37+
toastError('Failed to delete study: ' + error);
3738
},
3839
}
3940
);
40-
41+
4142
const handleRowClick = (studyId: string) => {
4243
onStudySelected && onStudySelected(studyId);
4344
}
@@ -49,7 +50,7 @@ const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudyS
4950
<span className="text-xl not-italic font-bold text-primary">{studyId} ?</span>
5051
</div>
5152
);
52-
if (await confirm({content: confirmContent})) {
53+
if (await confirm({ content: confirmContent })) {
5354
mutateDeleteStudy({ id: studyId });
5455
}
5556
};
@@ -62,9 +63,12 @@ const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudyS
6263
case 'delete':
6364
handleDeleteStudy(studyId);
6465
break;
66+
case 'preview':
67+
// handlePreviewStudy(studyId);
68+
break;
6569
default:
6670
break;
67-
}
71+
}
6872
};
6973

7074

@@ -87,6 +91,13 @@ const StudyRoot: React.FC<StudyRootProps> = ({ patient, onStudyUpdated, onStudyS
8791
show={!!editingStudy}
8892
/>
8993
)}
94+
{/* {previewStudy && (
95+
<PreviewStudy
96+
studyId={previewStudy.id}
97+
onClose={() => setPreviewStudy(null)}
98+
show={!!previewStudy}
99+
/>
100+
)} */}
90101
</div>
91102
);
92103
};

src/services/instances.ts

+11
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,15 @@ export const createDicom = (content: string[], tags: object = {}, parentOrthancI
3434
}).catch(error => {
3535
console.error(error)
3636
})
37+
}
38+
39+
40+
export const previewInstance = (instanceUID : string) : Promise<any>=> {
41+
42+
return axios.get('/api/instances/'+instanceUID+'/preview', {responseType : "blob"})
43+
.then((response) => {
44+
return response.data
45+
}).catch(error => {
46+
console.error(error)
47+
})
3748
}

src/services/orthanc.ts

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import axios from "axios";
2-
import { Patient, Study, Series, PatientPayload, OrthancResponse, StudyPayload, SeriesPayload } from '../utils/types';
2+
import { Patient, Study, Series, PatientPayload, OrthancResponse, StudyPayload, SeriesPayload, Instances } from '../utils/types';
33

44
export const getOrthancSystem = (): Promise<unknown> => {
55
return axios.get("/api/system").then(response => response.data)
@@ -178,6 +178,39 @@ export const getSeriesOfStudy = (studyId: string): Promise<Series[]> => {
178178
});
179179
};
180180

181+
export const getInstancesOfSeries = (seriesId: string) => {
182+
return axios.get(`/api/series/${seriesId}/instances`)
183+
.then((response: any): Instances[] => {
184+
const mappedData = response.data.map((data: any) : Instances => ({
185+
fileSize: data.FileSize,
186+
fileUuid: data.FileUuid,
187+
id: data.ID,
188+
indexInSeries: data.IndexInSeries,
189+
labels: data.Labels,
190+
mainDicomTags: {
191+
acquisitionNumber: data.MainDicomTags.AcquisitionNumber,
192+
imageComments: data.MainDicomTags.ImageComments,
193+
imageOrientationPatient: data.MainDicomTags.ImageOrientationPatient,
194+
imagePositionPatient: data.MainDicomTags.ImagePositionPatient,
195+
instanceCreationDate: data.MainDicomTags.InstanceCreationDate,
196+
instanceCreationTime: data.MainDicomTags.InstanceCreationTime,
197+
instanceNumber: data.MainDicomTags.InstanceNumber,
198+
sopInstanceUID: data.MainDicomTags.SopInstanceUID
199+
},
200+
parentSeries: data.ParentSeries,
201+
type: data.Type
202+
}));
203+
return mappedData;
204+
}).catch((error: any) => {
205+
if (error.response) {
206+
console.error("Error response:", error.response);
207+
throw error.response;
208+
}
209+
console.error("Error:", error);
210+
throw error;
211+
});
212+
}
213+
181214

182215
export const modifyPatient = (patientId: string, patient: PatientPayload): Promise<OrthancResponse> => {
183216
const patientPayloadUpdate = {
@@ -244,7 +277,7 @@ export const modifySeries = (seriesId: string, series: SeriesPayload): Promise<O
244277
const seriesPayloadUpdate = {
245278
Replace: {
246279
ImageOrientationPatient: series.replace.imageOrientationPatient,
247-
Manufacturer : series.replace.manufacturer,
280+
Manufacturer: series.replace.manufacturer,
248281
Modality: series.replace.modality,
249282
OperatorsName: series.replace.operatorsName,
250283
ProtocolName: series.replace.protocolName,
@@ -277,7 +310,7 @@ export const modifySeries = (seriesId: string, series: SeriesPayload): Promise<O
277310
throw error;
278311
});
279312
}
280-
313+
281314
export const getPatient = (patientId: string): Promise<Patient> => {
282315
return axios.get("/api/patients/" + patientId)
283316
.then((response): Patient => {

src/ui/Spinner.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11

22
//! This is a basic spinner component , maybe needs to change later
3-
const Spinner = () => {
3+
type SpinnerProps = {
4+
className? : string
5+
}
6+
const Spinner = ({className = ""} : SpinnerProps) => {
47
return (
5-
<div className="flex items-center justify-center">
8+
<div className={"flex items-center justify-center"+ className}>
69
<div className="size-16 animate-spin rounded-full border-b-2 border-primary bg-white"></div>
710
</div>
811
);

0 commit comments

Comments
 (0)