fix(DicomTagBrowser): Fix parsing problems that was limiting nested tags rendering (#3306)
* fix(DicomTagBrowser): Fix parsing problems that was limiting nested tags rendering * Added a progress indicator when switching to a list of more than 1000 rows. * Switched to using react-window to virtualize the dicom tag browser window. * Added filtering to the DICOM tag browser with a new search UI widget. Lots of tweaks and updates to the look-and-feel. * Removed unused import. * PR feedback including some UI tweaks. * More PR feedback. --------- Co-authored-by: Igor Octaviano <igor.octaviano@radicalimaging.com> Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
This commit is contained in:
parent
217b330d7b
commit
b684d80426
@ -38,6 +38,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^12.2.2",
|
"react-i18next": "^12.2.2",
|
||||||
|
"react-window": "^1.8.9",
|
||||||
"webpack": "^5.50.0",
|
"webpack": "^5.50.0",
|
||||||
"webpack-merge": "^5.7.3"
|
"webpack-merge": "^5.7.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,27 +1,41 @@
|
|||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import { classes } from '@ohif/core';
|
import { classes } from '@ohif/core';
|
||||||
|
import { Icon, InputRange, Select, Typography } from '@ohif/ui';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import DicomTagTable from './DicomTagTable';
|
import DicomTagTable from './DicomTagTable';
|
||||||
import './DicomTagBrowser.css';
|
import './DicomTagBrowser.css';
|
||||||
import { InputRange, Select, Typography } from '@ohif/ui';
|
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
const { ImageSet } = classes;
|
||||||
const { DicomMetaDictionary } = dcmjs.data;
|
const { DicomMetaDictionary } = dcmjs.data;
|
||||||
const { nameMap } = DicomMetaDictionary;
|
const { nameMap } = DicomMetaDictionary;
|
||||||
|
|
||||||
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||||
|
// The column indices that are to be excluded during a filter of the table.
|
||||||
|
// At present the column indices are:
|
||||||
|
// 0: DICOM tag
|
||||||
|
// 1: VR
|
||||||
|
// 2: Keyword
|
||||||
|
// 3: Value
|
||||||
|
const excludedColumnIndicesForFilter: Set<number> = new Set([1]);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
selectedDisplaySetInstanceUID,
|
selectedDisplaySetInstanceUID,
|
||||||
setSelectedDisplaySetInstanceUID,
|
setSelectedDisplaySetInstanceUID,
|
||||||
] = useState(displaySetInstanceUID);
|
] = useState(displaySetInstanceUID);
|
||||||
const [instanceNumber, setInstanceNumber] = useState(1);
|
const [instanceNumber, setInstanceNumber] = useState(1);
|
||||||
|
const [filterValue, setFilterValue] = useState('');
|
||||||
|
|
||||||
const onSelectChange = value => {
|
const onSelectChange = value => {
|
||||||
setSelectedDisplaySetInstanceUID(value.value);
|
setSelectedDisplaySetInstanceUID(value.value);
|
||||||
setInstanceNumber(1);
|
setInstanceNumber(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputRef = useRef(null);
|
||||||
|
|
||||||
const activeDisplaySet = displaySets.find(
|
const activeDisplaySet = displaySets.find(
|
||||||
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
|
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
|
||||||
);
|
);
|
||||||
@ -54,64 +68,130 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
});
|
});
|
||||||
}, [displaySets]);
|
}, [displaySets]);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
let metadata;
|
||||||
|
if (isImageStack) {
|
||||||
|
metadata = activeDisplaySet.images[instanceNumber - 1];
|
||||||
|
} else {
|
||||||
|
metadata = activeDisplaySet.instance || activeDisplaySet;
|
||||||
|
}
|
||||||
|
const tags = getSortedTags(metadata);
|
||||||
|
return getFormattedRowsFromTags(tags, metadata);
|
||||||
|
}, [instanceNumber, selectedDisplaySetInstanceUID]);
|
||||||
|
|
||||||
|
const filteredRows = useMemo(() => {
|
||||||
|
if (!filterValue) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValueLowerCase = filterValue.toLowerCase();
|
||||||
|
return rows.filter(row => {
|
||||||
|
return row.reduce((keepRow, col, colIndex) => {
|
||||||
|
if (keepRow) {
|
||||||
|
// We are already keeping the row, why do more work so return now.
|
||||||
|
return keepRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludedColumnIndicesForFilter.has(colIndex)) {
|
||||||
|
return keepRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keepRow || col.toLowerCase().includes(filterValueLowerCase);
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
}, [rows, filterValue]);
|
||||||
|
|
||||||
|
const debouncedSetFilterValue = useMemo(() => {
|
||||||
|
return debounce(setFilterValue, 200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedSetFilterValue?.cancel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dicom-tag-browser-content">
|
<div className="dicom-tag-browser-content">
|
||||||
<div className="flex flex-row items-center mb-2">
|
<div className="flex flex-row mb-6 items-center pl-1">
|
||||||
<Typography variant="subtitle" className="w-1/2 mr-8">
|
<div className="flex flex-row items-center w-1/2">
|
||||||
Series
|
<Typography variant="subtitle" className="mr-4">
|
||||||
</Typography>
|
Series
|
||||||
{showInstanceList && (
|
|
||||||
<Typography variant="subtitle" className="w-1/2">
|
|
||||||
Instance Number
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
<div className="grow mr-8">
|
||||||
</div>
|
<Select
|
||||||
<div className="flex flex-row items-center mb-6">
|
id="display-set-selector"
|
||||||
<div className="w-1/2 mr-8">
|
isClearable={false}
|
||||||
<Select
|
onChange={onSelectChange}
|
||||||
isClearable={false}
|
options={displaySetList}
|
||||||
onChange={onSelectChange}
|
value={displaySetList.find(
|
||||||
options={displaySetList}
|
ds => ds.value === selectedDisplaySetInstanceUID
|
||||||
value={displaySetList.find(
|
)}
|
||||||
ds => ds.value === selectedDisplaySetInstanceUID
|
className="text-white"
|
||||||
)}
|
|
||||||
className="text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{showInstanceList ? (
|
|
||||||
<div className="w-1/2">
|
|
||||||
<InputRange
|
|
||||||
value={instanceNumber}
|
|
||||||
key={selectedDisplaySetInstanceUID}
|
|
||||||
onChange={value => {
|
|
||||||
setInstanceNumber(parseInt(value));
|
|
||||||
}}
|
|
||||||
minValue={1}
|
|
||||||
maxValue={activeDisplaySet.images.length}
|
|
||||||
step={1}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
|
<div className="flex flex-row items-center w-1/2">
|
||||||
|
{showInstanceList && (
|
||||||
|
<Typography variant="subtitle" className="mr-4">
|
||||||
|
Instance Number
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{showInstanceList && (
|
||||||
|
<div className="grow">
|
||||||
|
<InputRange
|
||||||
|
value={instanceNumber}
|
||||||
|
key={selectedDisplaySetInstanceUID}
|
||||||
|
onChange={value => {
|
||||||
|
setInstanceNumber(parseInt(value));
|
||||||
|
}}
|
||||||
|
minValue={1}
|
||||||
|
maxValue={activeDisplaySet.images.length}
|
||||||
|
step={1}
|
||||||
|
inputClassName="w-full"
|
||||||
|
labelPosition="left"
|
||||||
|
trackColor={'#3a3f99'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DicomTagTable
|
<div className="w-full h-1 bg-black"></div>
|
||||||
rows={getFormattedRowsFromTags(activeDisplaySet, instanceNumber)}
|
<div className="flex flex-row my-3 w-1/2">
|
||||||
></DicomTagTable>
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
<DicomTagTable rows={filteredRows} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFormattedRowsFromTags(displaySet, instanceNumber) {
|
function getFormattedRowsFromTags(tags, metadata) {
|
||||||
const isImageStack = _isImageStack(displaySet);
|
|
||||||
|
|
||||||
let metadata;
|
|
||||||
|
|
||||||
if (isImageStack) {
|
|
||||||
metadata = displaySet.images[instanceNumber - 1];
|
|
||||||
} else {
|
|
||||||
metadata = displaySet;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = getSortedTags(metadata);
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
|
||||||
tags.forEach(tagInfo => {
|
tags.forEach(tagInfo => {
|
||||||
@ -126,7 +206,7 @@ function getFormattedRowsFromTags(displaySet, instanceNumber) {
|
|||||||
const { values } = tagInfo;
|
const { values } = tagInfo;
|
||||||
|
|
||||||
values.forEach((item, index) => {
|
values.forEach((item, index) => {
|
||||||
const formatedRowsFromTags = getFormattedRowsFromTags(item);
|
const formatedRowsFromTags = getFormattedRowsFromTags(item, metadata);
|
||||||
|
|
||||||
rows.push([
|
rows.push([
|
||||||
`${item[0].tagIndent}(FFFE,E000)`,
|
`${item[0].tagIndent}(FFFE,E000)`,
|
||||||
@ -140,34 +220,21 @@ function getFormattedRowsFromTags(displaySet, instanceNumber) {
|
|||||||
} else {
|
} else {
|
||||||
if (tagInfo.vr === 'xs') {
|
if (tagInfo.vr === 'xs') {
|
||||||
try {
|
try {
|
||||||
/* const dataset = metadataProvider.getStudyDataset(
|
const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
|
||||||
meta.StudyInstanceUID
|
const originalTagInfo = metadata[tag];
|
||||||
);*/
|
tagInfo.vr = originalTagInfo.vr;
|
||||||
// console.log(dataset);
|
|
||||||
// const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
|
|
||||||
// const originalTagInfo = dataset[tag];
|
|
||||||
// tagInfo.vr = originalTagInfo.vr;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to parse value representation for tag '${tagInfo.keyword}'`
|
`Failed to parse value representation for tag '${tagInfo.keyword}'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tagInfo.vr === 'PN') {
|
rows.push([
|
||||||
rows.push([
|
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
||||||
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
tagInfo.vr,
|
||||||
tagInfo.vr,
|
tagInfo.keyword,
|
||||||
tagInfo.keyword,
|
tagInfo.value,
|
||||||
tagInfo.value,
|
]);
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
rows.push([
|
|
||||||
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
|
||||||
tagInfo.vr,
|
|
||||||
tagInfo.keyword,
|
|
||||||
tagInfo.value,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,97 +1,224 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { VariableSizeList as List } from 'react-window';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
function ColumnHeaders() {
|
const lineHeightPx = 20;
|
||||||
|
const lineHeightClassName = `leading-[${lineHeightPx}px]`;
|
||||||
|
const rowVerticalPaddingPx = 10;
|
||||||
|
const rowBottomBorderPx = 1;
|
||||||
|
const rowVerticalPaddingStyle = { padding: `${rowVerticalPaddingPx}px 0` };
|
||||||
|
const rowStyle = {
|
||||||
|
borderBottomWidth: `${rowBottomBorderPx}px`,
|
||||||
|
...rowVerticalPaddingStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) {
|
||||||
return (
|
return (
|
||||||
<div className="m-auto flex flex-col py-2 bg-secondary-light">
|
<div
|
||||||
<div className="flex flex-row w-full">
|
className={classNames(
|
||||||
<div className="px-3 w-5/24">
|
'flex flex-row w-full bg-secondary-dark ohif-scrollbar overflow-y-scroll'
|
||||||
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
)}
|
||||||
<span className="flex flex-row items-center focus:outline-none">
|
style={rowVerticalPaddingStyle}
|
||||||
Tag
|
>
|
||||||
</span>
|
<div className="px-3 w-4/24">
|
||||||
</label>
|
<label
|
||||||
</div>
|
ref={tagRef}
|
||||||
<div className="px-3 w-4/24">
|
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
|
||||||
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
>
|
||||||
<span className="flex flex-row items-center focus:outline-none">
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
VR
|
Tag
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 w-6/24">
|
<div className="px-3 w-2/24">
|
||||||
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
<label
|
||||||
<span className="flex flex-row items-center focus:outline-none">
|
ref={vrRef}
|
||||||
Keyword
|
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
|
||||||
</span>
|
>
|
||||||
</label>
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
</div>
|
VR
|
||||||
<div className="px-3 w-5/24">
|
</span>
|
||||||
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
</label>
|
||||||
<span className="flex flex-row items-center focus:outline-none">
|
</div>
|
||||||
Value
|
<div className="px-3 w-6/24">
|
||||||
</span>
|
<label
|
||||||
</label>
|
ref={keywordRef}
|
||||||
</div>
|
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
|
||||||
|
>
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Keyword
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 w-5/24 grow">
|
||||||
|
<label
|
||||||
|
ref={valueRef}
|
||||||
|
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
|
||||||
|
>
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Value
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DicomTagTable({ rows }) {
|
function DicomTagTable({ rows }) {
|
||||||
|
const listRef = useRef();
|
||||||
|
const canvasRef = useRef();
|
||||||
|
|
||||||
|
const [tagHeaderElem, setTagHeaderElem] = useState(null);
|
||||||
|
const [vrHeaderElem, setVrHeaderElem] = useState(null);
|
||||||
|
const [keywordHeaderElem, setKeywordHeaderElem] = useState(null);
|
||||||
|
const [valueHeaderElem, setValueHeaderElem] = useState(null);
|
||||||
|
|
||||||
|
// Here the refs are inturn stored in state to trigger a render of the table.
|
||||||
|
// This virtualized table does NOT render until the header is rendered because the header column widths are used to determine the row heights in the table.
|
||||||
|
// Therefore whenever the refs change (in particular the first time the refs are set), we want to trigger a render of the table.
|
||||||
|
const tagRef = elem => {
|
||||||
|
if (elem) {
|
||||||
|
setTagHeaderElem(elem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const vrRef = elem => {
|
||||||
|
if (elem) {
|
||||||
|
setVrHeaderElem(elem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const keywordRef = elem => {
|
||||||
|
if (elem) {
|
||||||
|
setKeywordHeaderElem(elem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const valueRef = elem => {
|
||||||
|
if (elem) {
|
||||||
|
setValueHeaderElem(elem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When new rows are set, scroll to the top and reset the virtualization.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!listRef?.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listRef.current.scrollTo(0);
|
||||||
|
listRef.current.resetAfterIndex(0);
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the browser window resizes, update the row virtualization (i.e. row heights)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const debouncedResize = debounce(
|
||||||
|
() => listRef.current.resetAfterIndex(0),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener('resize', debouncedResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedResize.cancel();
|
||||||
|
window.removeEventListener('resize', debouncedResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const Row = useCallback(
|
||||||
|
({ index, style }) => {
|
||||||
|
const row = rows[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ ...style, ...rowStyle }}
|
||||||
|
className={classNames(
|
||||||
|
'hover:bg-secondary-main transition duration-300 bg-black flex flex-row w-full border-secondary-light items-center text-base break-all',
|
||||||
|
lineHeightClassName
|
||||||
|
)}
|
||||||
|
key={`DICOMTagRow-${index}`}
|
||||||
|
>
|
||||||
|
<div className="px-3 w-4/24">{row[0]}</div>
|
||||||
|
<div className="px-3 w-2/24">{row[1]}</div>
|
||||||
|
<div className="px-3 w-6/24">{row[2]}</div>
|
||||||
|
<div className="px-3 w-5/24 grow">{row[3]}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever any one of the column headers is set, then the header is rendered.
|
||||||
|
* Here we chose the tag header.
|
||||||
|
*/
|
||||||
|
const isHeaderRendered = useCallback(() => tagHeaderElem !== null, [
|
||||||
|
tagHeaderElem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item/row size. We use the header column widths to calculate the various row heights.
|
||||||
|
* @param index the row index
|
||||||
|
* @returns the row height
|
||||||
|
*/
|
||||||
|
const getItemSize = useCallback(
|
||||||
|
index => {
|
||||||
|
const headerWidths = [
|
||||||
|
tagHeaderElem.offsetWidth,
|
||||||
|
vrHeaderElem.offsetWidth,
|
||||||
|
keywordHeaderElem.offsetWidth,
|
||||||
|
valueHeaderElem.offsetWidth,
|
||||||
|
];
|
||||||
|
|
||||||
|
const context = canvasRef.current.getContext('2d');
|
||||||
|
context.font = getComputedStyle(canvasRef.current).font;
|
||||||
|
|
||||||
|
return rows[index]
|
||||||
|
.map((colText, index) => {
|
||||||
|
const colOneLineWidth = context.measureText(colText).width;
|
||||||
|
const numLines = Math.ceil(colOneLineWidth / headerWidths[index]);
|
||||||
|
return (
|
||||||
|
numLines * lineHeightPx +
|
||||||
|
2 * rowVerticalPaddingPx +
|
||||||
|
rowBottomBorderPx
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce((maxHeight, colHeight) => Math.max(maxHeight, colHeight));
|
||||||
|
},
|
||||||
|
[rows, keywordHeaderElem, tagHeaderElem, valueHeaderElem, vrHeaderElem]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ColumnHeaders()}
|
<canvas
|
||||||
|
style={{ visibility: 'hidden', position: 'absolute' }}
|
||||||
|
className="text-base"
|
||||||
|
ref={canvasRef}
|
||||||
|
/>
|
||||||
|
<ColumnHeaders
|
||||||
|
tagRef={tagRef}
|
||||||
|
vrRef={vrRef}
|
||||||
|
keywordRef={keywordRef}
|
||||||
|
valueRef={valueRef}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="m-auto relative border-2 border-secondary-light overflow-hidden ohif-scrollbar"
|
className="m-auto relative border-2 border-black bg-black"
|
||||||
style={{ height: '32rem' }}
|
style={{ height: '32rem' }}
|
||||||
>
|
>
|
||||||
<table className="w-full text-white">
|
{isHeaderRendered() && (
|
||||||
<tbody>
|
<List
|
||||||
{rows.map((row, index) => {
|
ref={listRef}
|
||||||
const className = row.className ? row.className : null;
|
height={500}
|
||||||
|
itemCount={rows.length}
|
||||||
return (
|
itemSize={getItemSize}
|
||||||
<tr
|
width={'100%'}
|
||||||
className="hover:bg-secondary-main transition duration-300 bg-primary-dark"
|
className="ohif-scrollbar"
|
||||||
key={`DICOMTagRow-${index}`}
|
>
|
||||||
>
|
{Row}
|
||||||
<td
|
</List>
|
||||||
style={{ maxWidth: '0px' }}
|
)}
|
||||||
className="px-4 py-2 text-base break-all border-b border-secondary-light w-5/24"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="inline-flex max-w-full">{row[0]}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{ maxWidth: '0px' }}
|
|
||||||
className="px-4 py-2 text-base break-all border-b border-secondary-light w-4/24"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="inline-flex max-w-full">{row[1]}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{ maxWidth: '0px' }}
|
|
||||||
className="px-4 py-2 text-base break-all border-b border-secondary-light w-6/24"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="inline-flex max-w-full">{row[2]}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{ maxWidth: '0px' }}
|
|
||||||
className="px-4 py-2 text-base break-all border-b border-secondary-light w-14/24"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="inline-flex max-w-full">{row[3]}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
"react-outside-click-handler": "^1.3.0",
|
"react-outside-click-handler": "^1.3.0",
|
||||||
"react-select": "3.0.8",
|
"react-select": "3.0.8",
|
||||||
"react-with-direction": "^1.3.1",
|
"react-with-direction": "^1.3.1",
|
||||||
|
"react-window": "^1.8.9",
|
||||||
"swiper": "^8.4.2",
|
"swiper": "^8.4.2",
|
||||||
"webpack": "^5.81.0"
|
"webpack": "^5.81.0"
|
||||||
},
|
},
|
||||||
|
|||||||
8
platform/ui/src/assets/icons/icon-clear-field.svg
Normal file
8
platform/ui/src/assets/icons/icon-clear-field.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="19" height="19" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<circle fill="#0944B3" cx="9.5" cy="9.5" r="9.5"/>
|
||||||
|
<g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<path d="m5.188 5.187 8.625 8.625M13.813 5.187l-8.625 8.625"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 377 B |
9
platform/ui/src/assets/icons/icon-search.svg
Normal file
9
platform/ui/src/assets/icons/icon-search.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M0 0h18v18H0z"/>
|
||||||
|
<g transform="translate(1 1)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<circle cx="5.565" cy="5.565" r="5.565"/>
|
||||||
|
<path d="M9.5 9.5 16 16"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
@ -53,12 +53,14 @@ import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
|
|||||||
import iconAlertOutline from './../../assets/icons/icons-alert-outline.svg';
|
import iconAlertOutline from './../../assets/icons/icons-alert-outline.svg';
|
||||||
import iconAlertSmall from './../../assets/icons/icon-alert-small.svg';
|
import iconAlertSmall from './../../assets/icons/icon-alert-small.svg';
|
||||||
import iconClose from './../../assets/icons/icon-close.svg';
|
import iconClose from './../../assets/icons/icon-close.svg';
|
||||||
|
import iconClearField from './../../assets/icons/icon-clear-field.svg';
|
||||||
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
|
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
|
||||||
import iconNext from './../../assets/icons/icon-next.svg';
|
import iconNext from './../../assets/icons/icon-next.svg';
|
||||||
import iconPlay from './../../assets/icons/icon-play.svg';
|
import iconPlay from './../../assets/icons/icon-play.svg';
|
||||||
import iconPause from './../../assets/icons/icon-pause.svg';
|
import iconPause from './../../assets/icons/icon-pause.svg';
|
||||||
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
|
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
|
||||||
import iconPrev from './../../assets/icons/icon-prev.svg';
|
import iconPrev from './../../assets/icons/icon-prev.svg';
|
||||||
|
import iconSearch from './../../assets/icons/icon-search.svg';
|
||||||
import iconStatusAlert from './../../assets/icons/icon-status-alert.svg';
|
import iconStatusAlert from './../../assets/icons/icon-status-alert.svg';
|
||||||
import iconTransferring from './../../assets/icons/icon-transferring.svg';
|
import iconTransferring from './../../assets/icons/icon-transferring.svg';
|
||||||
import iconUpload from './../../assets/icons/icon-upload.svg';
|
import iconUpload from './../../assets/icons/icon-upload.svg';
|
||||||
@ -151,9 +153,11 @@ const ICONS = {
|
|||||||
info: info,
|
info: info,
|
||||||
'icon-alert-outline': iconAlertOutline,
|
'icon-alert-outline': iconAlertOutline,
|
||||||
'icon-alert-small': iconAlertSmall,
|
'icon-alert-small': iconAlertSmall,
|
||||||
|
'icon-clear-field': iconClearField,
|
||||||
'icon-close': iconClose,
|
'icon-close': iconClose,
|
||||||
'icon-play': iconPlay,
|
'icon-play': iconPlay,
|
||||||
'icon-pause': iconPause,
|
'icon-pause': iconPause,
|
||||||
|
'icon-search': iconSearch,
|
||||||
'icon-status-alert': iconStatusAlert,
|
'icon-status-alert': iconStatusAlert,
|
||||||
'icon-transferring': iconTransferring,
|
'icon-transferring': iconTransferring,
|
||||||
'info-action': infoAction,
|
'info-action': infoAction,
|
||||||
|
|||||||
@ -5,8 +5,8 @@ input[type='range'] {
|
|||||||
input[type='range']::-webkit-slider-thumb {
|
input[type='range']::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
height: 10px;
|
height: 13px;
|
||||||
width: 10px;
|
width: 13px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #5acce6;
|
background: #5acce6;
|
||||||
}
|
}
|
||||||
@ -14,8 +14,8 @@ input[type='range']::-webkit-slider-thumb {
|
|||||||
input[type='range']::-moz-range-thumb {
|
input[type='range']::-moz-range-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
height: 10px;
|
height: 13px;
|
||||||
width: 10px;
|
width: 13px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #5acce6;
|
background: #5acce6;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,9 @@ const InputRange: React.FC<{
|
|||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
labelVariant?: string;
|
labelVariant?: string;
|
||||||
showLabel: boolean;
|
showLabel?: boolean;
|
||||||
|
labelPosition?: string;
|
||||||
|
trackColor?: string;
|
||||||
}> = ({
|
}> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -36,6 +38,8 @@ const InputRange: React.FC<{
|
|||||||
labelClassName,
|
labelClassName,
|
||||||
labelVariant,
|
labelVariant,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
|
labelPosition = '',
|
||||||
|
trackColor,
|
||||||
}) => {
|
}) => {
|
||||||
const [rangeValue, setRangeValue] = useState(value);
|
const [rangeValue, setRangeValue] = useState(value);
|
||||||
|
|
||||||
@ -63,6 +67,16 @@ const InputRange: React.FC<{
|
|||||||
containerClassName ? containerClassName : ''
|
containerClassName ? containerClassName : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{showLabel && labelPosition === 'left' && (
|
||||||
|
<Typography
|
||||||
|
variant={labelVariant ?? 'subtitle'}
|
||||||
|
component="p"
|
||||||
|
className={classNames('w-8', labelClassName ?? 'text-white')}
|
||||||
|
>
|
||||||
|
{rangeValueForStr}
|
||||||
|
{unit}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={minValue}
|
min={minValue}
|
||||||
@ -72,14 +86,16 @@ const InputRange: React.FC<{
|
|||||||
inputClassName ? inputClassName : ''
|
inputClassName ? inputClassName : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
|
background:
|
||||||
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
|
trackColor ||
|
||||||
|
`linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
|
||||||
|
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
|
||||||
}}
|
}}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
id="myRange"
|
id="myRange"
|
||||||
step={step}
|
step={step}
|
||||||
/>
|
/>
|
||||||
{showLabel && (
|
{showLabel && (!labelPosition || labelPosition === 'right') && (
|
||||||
<Typography
|
<Typography
|
||||||
variant={labelVariant ?? 'subtitle'}
|
variant={labelVariant ?? 'subtitle'}
|
||||||
component="p"
|
component="p"
|
||||||
|
|||||||
@ -39,7 +39,7 @@ module.exports = {
|
|||||||
main: '#3a3f99',
|
main: '#3a3f99',
|
||||||
disabled: '#2b166b',
|
disabled: '#2b166b',
|
||||||
focus: '#5acce6',
|
focus: '#5acce6',
|
||||||
placeholder: '#39383f'
|
placeholder: '#39383f',
|
||||||
},
|
},
|
||||||
|
|
||||||
secondary: {
|
secondary: {
|
||||||
|
|||||||
@ -50,7 +50,7 @@ module.exports = {
|
|||||||
main: '#3a3f99',
|
main: '#3a3f99',
|
||||||
disabled: '#2b166b',
|
disabled: '#2b166b',
|
||||||
focus: '#5acce6',
|
focus: '#5acce6',
|
||||||
placeholder: '#39383f'
|
placeholder: '#39383f',
|
||||||
},
|
},
|
||||||
|
|
||||||
secondary: {
|
secondary: {
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@ -1302,7 +1302,7 @@
|
|||||||
core-js-pure "^3.25.1"
|
core-js-pure "^3.25.1"
|
||||||
regenerator-runtime "^0.13.11"
|
regenerator-runtime "^0.13.11"
|
||||||
|
|
||||||
"@babel/runtime@7.17.9", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
"@babel/runtime@7.17.9", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.21.0"
|
version "7.21.0"
|
||||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||||
@ -13975,7 +13975,7 @@ memfs@^3.1.2, memfs@^3.4.1, memfs@^3.4.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey "^1.0.3"
|
fs-monkey "^1.0.3"
|
||||||
|
|
||||||
memoize-one@^5.0.0:
|
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
@ -17518,6 +17518,14 @@ react-waypoint@^10.3.0:
|
|||||||
prop-types "^15.0.0"
|
prop-types "^15.0.0"
|
||||||
react-is "^17.0.1 || ^18.0.0"
|
react-is "^17.0.1 || ^18.0.0"
|
||||||
|
|
||||||
|
react-window@^1.8.9:
|
||||||
|
version "1.8.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8"
|
||||||
|
integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.0.0"
|
||||||
|
memoize-one ">=3.1.1 <6"
|
||||||
|
|
||||||
react-with-direction@^1.3.1:
|
react-with-direction@^1.3.1:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.4.0.tgz#ebdf64d685d0650ce966e872e6431ad5a2485444"
|
resolved "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.4.0.tgz#ebdf64d685d0650ce966e872e6431ad5a2485444"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user