Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d47cc43c1 | ||
|
|
e46a11a01e | ||
|
|
27a950d956 | ||
|
|
03d4c10adc | ||
|
|
7a0c369006 | ||
|
|
8c946a4742 | ||
|
|
30b9e44220 | ||
|
|
380abe2705 | ||
|
|
f4570c96c2 | ||
|
|
1fe951a71e | ||
|
|
ebe1af8d4f | ||
|
|
5cda5159da | ||
|
|
115a3ec0b0 | ||
|
|
ac82a111e7 | ||
|
|
a22fd67e13 | ||
|
|
91f0af058f | ||
|
|
5a1186388d | ||
|
|
2f0f92c095 | ||
|
|
5c686268ab | ||
|
|
974aabc9b2 | ||
|
|
17e3b49b06 | ||
|
|
e3c119489a | ||
|
|
17447baf44 | ||
|
|
daa3ddccdb | ||
|
|
c91b6dddd8 | ||
|
|
92548191e4 | ||
|
|
68fcaef19a | ||
|
|
ef611e4849 | ||
|
|
d128fd6bf0 | ||
|
|
3bdd434488 | ||
|
|
29bac8df7a | ||
|
|
319c67c389 | ||
|
|
b9836f6c71 | ||
|
|
6a6e2d7df3 | ||
|
|
2f8c408ed2 | ||
|
|
27910239e0 | ||
|
|
719bfd02eb | ||
|
|
994818faeb | ||
|
|
f06ef53e83 | ||
|
|
c7008621e4 | ||
|
|
55a580659e | ||
|
|
fd7620ced9 | ||
|
|
cd1cbf0f85 | ||
|
|
08055398de | ||
|
|
f6db7fade6 | ||
|
|
58d2bf1d12 | ||
|
|
87f5eca659 | ||
|
|
922ba8cc42 | ||
|
|
777ff88f3d | ||
|
|
68858db4b6 | ||
|
|
ab106afae3 | ||
|
|
ac69b0a656 | ||
|
|
8edba24eec | ||
|
|
aae232fc3e | ||
|
|
8fad8ceb80 | ||
|
|
f00f48da2b | ||
|
|
076b89ef83 | ||
|
|
4920b527d1 | ||
|
|
482041f053 | ||
|
|
674edf85f0 | ||
|
|
33a1a465ea | ||
|
|
6e04de30b6 | ||
|
|
19117167ac | ||
|
|
22e3c5d953 | ||
|
|
9a34877be4 | ||
|
|
b1b66429b6 | ||
|
|
78e522fbf6 | ||
|
|
830cb82965 | ||
|
|
9a71edda6a | ||
|
|
7a8ae23839 | ||
|
|
6873c4f065 | ||
|
|
6ea16eccd9 | ||
|
|
ff381c6521 | ||
|
|
26cd695b8b | ||
|
|
a4f1b2195c | ||
|
|
c8df17b032 | ||
|
|
f1a4c09b74 | ||
|
|
02572b4928 | ||
|
|
fd1d29820a | ||
|
|
6c63a917d3 | ||
|
|
caa6062c41 | ||
|
|
7f9743a020 | ||
|
|
a2c4161477 | ||
|
|
42cfce60f5 | ||
|
|
244949bab6 | ||
|
|
e87ec7b7f5 | ||
|
|
d98889f1eb | ||
|
|
817d43ee62 | ||
|
|
f396b30166 | ||
|
|
0bb3eeb068 | ||
|
|
1df21a9e07 | ||
|
|
fd1a9e2abe | ||
|
|
e34a057263 | ||
|
|
27e02825a4 | ||
|
|
8fd3af1e13 | ||
|
|
1334ba8eb0 | ||
|
|
a870b3cc60 | ||
|
|
52687a7113 | ||
|
|
421de23ba3 | ||
|
|
adc8d9ea0f | ||
|
|
f65639c2b0 | ||
|
|
cdd75bda2a | ||
|
|
d92fe44d3c | ||
|
|
d90c2ac024 | ||
|
|
7ce5e1d2d3 | ||
|
|
a3bf47f9bf | ||
|
|
81f79a26ad | ||
|
|
60b4b5d7cf | ||
|
|
69c54d4f49 | ||
|
|
0f8868a232 | ||
|
|
1bbfee4087 | ||
|
|
33c7b59611 | ||
|
|
b17f753e62 | ||
|
|
c77a2ef5c9 | ||
|
|
348edd4717 | ||
|
|
407060658a | ||
|
|
fc00f9d505 | ||
|
|
bc3d080672 | ||
|
|
09353354c3 | ||
|
|
0a12320083 | ||
|
|
813ee5ed4d | ||
|
|
6085b5f733 | ||
|
|
ffe12789dd | ||
|
|
0b68429dfb | ||
|
|
b747ea5144 | ||
|
|
caadf089df | ||
|
|
e0fc69c181 | ||
|
|
2b9765304f | ||
|
|
57730483e9 | ||
|
|
731b64e321 | ||
|
|
e97a09b0f4 | ||
|
|
f63d8a73d8 | ||
|
|
f10adf82f4 | ||
|
|
6174109d5a | ||
|
|
244e822afc | ||
|
|
cbdeeae597 | ||
|
|
7f6de72e8e | ||
|
|
1f514e5f1b | ||
|
|
f3fed511b9 | ||
|
|
31796d272c | ||
|
|
b41e6f93f3 | ||
|
|
882a45d398 | ||
|
|
fb0cd9355f | ||
|
|
2b12193020 | ||
|
|
9a626a2064 | ||
|
|
903777044a | ||
|
|
5e4725860d | ||
|
|
670470119f | ||
|
|
0d4f90f3cd | ||
|
|
43dd87c2c1 | ||
|
|
98f8583fba | ||
|
|
97a9a505ca | ||
|
|
bb74f3a761 | ||
|
|
aa33397fe6 | ||
|
|
8ae731fe4c | ||
|
|
add3416e11 | ||
|
|
136969bf18 | ||
|
|
e3e039ad47 | ||
|
|
0797eff0b7 | ||
|
|
55a5f04923 | ||
|
|
ed9e4d6751 | ||
|
|
b6119f8e29 | ||
|
|
2e38f4b2e4 | ||
|
|
e03abc45f5 | ||
|
|
f03d64ce39 | ||
|
|
e895bfb1aa | ||
|
|
17358c3f4b | ||
|
|
ec7337e801 | ||
|
|
538ea2516d | ||
|
|
fe687cd2f9 | ||
|
|
bdb9af5c4b | ||
|
|
db0f751a68 | ||
|
|
08fdb10f6d | ||
|
|
95c18b4986 | ||
|
|
e684558783 | ||
|
|
9104e7be50 | ||
|
|
81ad25b39f | ||
|
|
54d2cde6da | ||
|
|
9edc366ff7 | ||
|
|
79c10bb424 | ||
|
|
d8055e458f | ||
|
|
5c04e34c8f | ||
|
|
c59c5b3f14 | ||
|
|
c4adcfd39e | ||
|
|
0fbaf95971 | ||
|
|
a2a7e05e7d | ||
|
|
9da8647c84 | ||
|
|
e62f5f8dd2 | ||
|
|
dc6e9d6bb5 | ||
|
|
84a097212b | ||
|
|
2381810ba9 | ||
|
|
31b1adfa59 | ||
|
|
cd28877ed8 | ||
|
|
e81a39f5be | ||
|
|
9a2cf70504 | ||
|
|
d70eae3eb0 | ||
|
|
8481e94d4d | ||
|
|
19b22c39ae | ||
|
|
05802a3cf1 | ||
|
|
75d88bc454 | ||
|
|
9827d1bf84 | ||
|
|
e208f2e6a9 | ||
|
|
9c1a3c3183 | ||
|
|
ecaf578f92 | ||
|
|
d26adf92bd | ||
|
|
5ee832b195 | ||
|
|
8baba22a2e | ||
|
|
1d86b1a102 | ||
|
|
25e6817248 | ||
|
|
681384b742 | ||
|
|
54e07c281f | ||
|
|
9dedccc7f9 | ||
|
|
621e63b3e1 | ||
|
|
64cf3b324d | ||
|
|
b7f315ca47 | ||
|
|
caaa032c4b | ||
|
|
22127a8f59 | ||
|
|
50ca2bde97 | ||
|
|
c73cfbc110 | ||
|
|
5716b71d40 | ||
|
|
a3028ad1f6 | ||
|
|
352407c71a | ||
|
|
1b5a5a2963 | ||
|
|
cf5136899e | ||
|
|
8c4e96bb3d | ||
|
|
5b334175c3 | ||
|
|
20bfc829f7 | ||
|
|
0d498ba17d | ||
|
|
a0180a9f0c | ||
|
|
2cdef4b984 | ||
|
|
70532a6cc6 | ||
|
|
f0519920fa | ||
|
|
0d21cc6ad0 | ||
|
|
dc502190ed | ||
|
|
7d009ef1d9 | ||
|
|
777cca7471 | ||
|
|
e4bbbfbbe4 | ||
|
|
10059adc57 | ||
|
|
c522ff3dda | ||
|
|
980516fcfc | ||
|
|
2677170d67 | ||
|
|
3452f1b91e | ||
|
|
450e0981a5 | ||
|
|
8cf1e54490 | ||
|
|
85f836cd91 | ||
|
|
f5560c11b0 | ||
|
|
bef50f9298 | ||
|
|
986b7ae2bf | ||
|
|
86c479ac04 | ||
|
|
460e375f0a | ||
|
|
78344c2c66 | ||
|
|
b451ce4407 | ||
|
|
21b586b08f | ||
|
|
3bbeb1c521 | ||
|
|
7668b69f20 | ||
|
|
3a12138134 | ||
|
|
97786b808c | ||
|
|
cd1baa1b36 | ||
|
|
3ae3bee452 | ||
|
|
fd017c4512 | ||
|
|
c680720ce5 | ||
|
|
dfc4e89a91 | ||
|
|
13d337aaab | ||
|
|
4f26940005 | ||
|
|
9d08e81aa5 | ||
|
|
1a539d04c2 | ||
|
|
5cc78c867f | ||
|
|
cefa480979 | ||
|
|
66537bf1b9 | ||
|
|
862d426c71 | ||
|
|
e62f403fe9 | ||
|
|
dea0ceab57 | ||
|
|
a6c1e6c717 | ||
|
|
b03739428f | ||
|
|
2a52d9f05d | ||
|
|
d5e21bb5bb | ||
|
|
04ffe3ec76 | ||
|
|
9cdf8b690b | ||
|
|
8d49b31d03 | ||
|
|
e4cfae7ad9 | ||
|
|
e53041f630 | ||
|
|
35c54eac56 | ||
|
|
e7be573328 | ||
|
|
8a7706b286 | ||
|
|
c217b8bbde | ||
|
|
74f3cbc4b2 | ||
|
|
d091cd604c | ||
|
|
3a4280163d | ||
|
|
2d75e01ea0 | ||
|
|
5c5a49486d | ||
|
|
a8a57132a7 | ||
|
|
92c8996ed4 | ||
|
|
f07dca5fa4 | ||
|
|
c99e0d8a23 | ||
|
|
84144bc3ef | ||
|
|
5cbdd0c616 | ||
|
|
fc015326c7 | ||
|
|
01860b6094 | ||
|
|
cfdf827ff0 | ||
|
|
ceb6837153 | ||
|
|
b01b0108b0 | ||
|
|
d6862e7418 | ||
|
|
beb7083e55 | ||
|
|
412fe4e23f | ||
|
|
659649e10b | ||
|
|
f431b277dd | ||
|
|
2bc361cdca | ||
|
|
c6f306c0de | ||
|
|
c0ea27f6b3 | ||
|
|
df8d4029f1 | ||
|
|
a36cb63926 | ||
|
|
e51529e6c9 | ||
|
|
750a150644 | ||
|
|
a9d28d85da | ||
|
|
6ee5094cbe | ||
|
|
6bfb9bb140 | ||
|
|
db5b760432 | ||
|
|
e7de8be2d8 | ||
|
|
cb2088daf7 | ||
|
|
2a743554b6 | ||
|
|
a28984ef1f | ||
|
|
95e5e11d09 | ||
|
|
ff9f199850 | ||
|
|
1cc94f36a7 | ||
|
|
5c5223fcf1 | ||
|
|
8513ad9ef3 | ||
|
|
66d8bc6d6f | ||
|
|
0064a4b1cd | ||
|
|
74ce89b22b | ||
|
|
c92975f555 | ||
|
|
ce6c0533a0 | ||
|
|
4df624b8c4 | ||
|
|
e064fd353e | ||
|
|
458d310c78 | ||
|
|
73bc4d9094 | ||
|
|
7709ebb107 | ||
|
|
559b637290 | ||
|
|
36910f2b47 | ||
|
|
9e10c2b2de | ||
|
|
c5cbd77cb9 | ||
|
|
b3b4c10e4c | ||
|
|
855020d730 | ||
|
|
8503cbce4e | ||
|
|
c8667fb2d6 | ||
|
|
674ca9f96f | ||
|
|
7a4b97b4e8 | ||
|
|
c79c0c301d | ||
|
|
ff5060f276 | ||
|
|
dafb26b34b | ||
|
|
c4f9d5e750 | ||
|
|
cf40a8388c | ||
|
|
d596ab5c3d | ||
|
|
72198eee7d | ||
|
|
057f68a9b4 | ||
|
|
034ca64d6b | ||
|
|
4a0901aee9 | ||
|
|
d0ec9cf0e0 | ||
|
|
71bea0d4d8 | ||
|
|
e607ed2aa0 | ||
|
|
4939f43509 | ||
|
|
c80ea17cc2 | ||
|
|
aa7dc1e2c1 | ||
|
|
e7e1a8a6cd | ||
|
|
e66c27affb | ||
|
|
ae26904885 | ||
|
|
8bb730318a | ||
|
|
69b96478cb | ||
|
|
816666df7b | ||
|
|
e375a4ab11 | ||
|
|
7f4d71e412 | ||
|
|
a90605c82d | ||
|
|
5b523c2afa | ||
|
|
5551f817e3 | ||
|
|
a9ca3fa026 | ||
|
|
89380353d2 | ||
|
|
edb86fad55 | ||
|
|
395a1be3fb | ||
|
|
65d852b9e4 | ||
|
|
e797a3a141 | ||
|
|
246936f90b | ||
|
|
c57298475a | ||
|
|
6a5ebb3e25 | ||
|
|
291b23aee1 | ||
|
|
72427fe2ff | ||
|
|
882d04c9bf | ||
|
|
d1a24bc9d9 | ||
|
|
16a87fa75f | ||
|
|
6fdac4d605 | ||
|
|
6192502fe0 | ||
|
|
2341b32638 | ||
|
|
45aeaec916 | ||
|
|
66b84ae9c2 | ||
|
|
bc28fe26ac | ||
|
|
7f642a05da | ||
|
|
66a0aadcce | ||
|
|
808af7ee96 | ||
|
|
10b5e071f4 | ||
|
|
fe1969b071 | ||
|
|
16b2a8295f | ||
|
|
d33bb4210d | ||
|
|
ab681b9929 | ||
|
|
f52daca5e5 | ||
|
|
4afc88c0e5 | ||
|
|
f519f8609c |
No files matched your search
@@ -62,7 +62,7 @@ echo "Done generating documentation output"
|
|||||||
echo 'STARTING PUBLISH'
|
echo 'STARTING PUBLISH'
|
||||||
|
|
||||||
# WILL ALWAYS FAIL IF INITIATED FROM PR BRANCH
|
# WILL ALWAYS FAIL IF INITIATED FROM PR BRANCH
|
||||||
./node_modules/.bin/gh-pages \
|
npx gh-pages \
|
||||||
--silent \
|
--silent \
|
||||||
--repo https://$GITHUB_TOKEN@github.com/OHIF/Viewers.git \
|
--repo https://$GITHUB_TOKEN@github.com/OHIF/Viewers.git \
|
||||||
--message 'Autogenerated Message: [ci skip]' \
|
--message 'Autogenerated Message: [ci skip]' \
|
||||||
|
|||||||
@@ -13,21 +13,32 @@ version: 2.1
|
|||||||
##
|
##
|
||||||
orbs:
|
orbs:
|
||||||
codecov: codecov/codecov@1.0.5
|
codecov: codecov/codecov@1.0.5
|
||||||
cypress: cypress-io/cypress@1.10.0
|
cypress: cypress-io/cypress@1.11.0
|
||||||
|
executors:
|
||||||
|
# Custom executor to override Cypress config
|
||||||
|
deploy-to-prod-executor:
|
||||||
|
docker:
|
||||||
|
- image: 'cypress/browsers:node10.16.0-chrome77'
|
||||||
|
environment:
|
||||||
|
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
|
||||||
|
|
||||||
defaults: &defaults
|
defaults: &defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:10.16.0
|
- image: circleci/node:12.9.1
|
||||||
environment:
|
environment:
|
||||||
TERM: xterm # Enable colors in term
|
TERM: xterm # Enable colors in term
|
||||||
|
QUICK_BUILD: true
|
||||||
working_directory: ~/repo
|
working_directory: ~/repo
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CHECKOUT:
|
###
|
||||||
|
# Workflow: PR_CHECKS
|
||||||
|
###
|
||||||
|
UNIT_TESTS:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
# Enable yarn workspaces
|
# Update yarn
|
||||||
- run: yarn config set workspaces-experimental true
|
- run: yarn -v
|
||||||
# Checkout code and ALL Git Tags
|
# Checkout code and ALL Git Tags
|
||||||
- checkout:
|
- checkout:
|
||||||
post:
|
post:
|
||||||
@@ -36,9 +47,8 @@ jobs:
|
|||||||
name: Restore Yarn and Cypress Package Cache
|
name: Restore Yarn and Cypress Package Cache
|
||||||
keys:
|
keys:
|
||||||
# when lock file changes, use increasingly general patterns to restore cache
|
# when lock file changes, use increasingly general patterns to restore cache
|
||||||
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
- yarn-packages-v1-{{ .Branch }}-
|
- yarn-packages-
|
||||||
- yarn-packages-v1-
|
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: yarn install --frozen-lockfile
|
command: yarn install --frozen-lockfile
|
||||||
@@ -46,23 +56,11 @@ jobs:
|
|||||||
name: Save Yarn Package Cache
|
name: Save Yarn Package Cache
|
||||||
paths:
|
paths:
|
||||||
- ~/.cache ## Cache yarn and Cypress
|
- ~/.cache ## Cache yarn and Cypress
|
||||||
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
# Persist :+1:
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/repo
|
|
||||||
paths: .
|
|
||||||
|
|
||||||
UNIT_TESTS:
|
|
||||||
<<: *defaults
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: ~/repo
|
|
||||||
|
|
||||||
# RUN TESTS
|
# RUN TESTS
|
||||||
- run:
|
- run:
|
||||||
name: 'JavaScript Test Suite'
|
name: 'JavaScript Test Suite'
|
||||||
command: yarn run test:unit:ci
|
command: yarn run test:unit:ci
|
||||||
|
|
||||||
# PLATFORM/VIEWER
|
# PLATFORM/VIEWER
|
||||||
- run:
|
- run:
|
||||||
name: 'VIEWER: Combine report output'
|
name: 'VIEWER: Combine report output'
|
||||||
@@ -76,7 +74,6 @@ jobs:
|
|||||||
- codecov/upload:
|
- codecov/upload:
|
||||||
file: '/home/circleci/repo/platform/viewer/coverage/reports'
|
file: '/home/circleci/repo/platform/viewer/coverage/reports'
|
||||||
flags: 'viewer'
|
flags: 'viewer'
|
||||||
|
|
||||||
# PLATFORM/CORE
|
# PLATFORM/CORE
|
||||||
- run:
|
- run:
|
||||||
name: 'CORE: Combine report output'
|
name: 'CORE: Combine report output'
|
||||||
@@ -91,31 +88,177 @@ jobs:
|
|||||||
file: '/home/circleci/repo/platform/core/coverage/reports'
|
file: '/home/circleci/repo/platform/core/coverage/reports'
|
||||||
flags: 'core'
|
flags: 'core'
|
||||||
|
|
||||||
|
###
|
||||||
|
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
||||||
|
###
|
||||||
|
DOCKER_PR_PUBLISH:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
# Enable yarn workspaces
|
||||||
|
- run: yarn config set workspaces-experimental true
|
||||||
|
|
||||||
|
# Checkout code and ALL Git Tags
|
||||||
|
- checkout:
|
||||||
|
post:
|
||||||
|
- git fetch --all
|
||||||
|
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn and Cypress Package Cache
|
||||||
|
keys:
|
||||||
|
# when lock file changes, use increasingly general patterns to restore cache
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
|
- yarn-packages-
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- setup_remote_docker:
|
||||||
|
docker_layer_caching: false
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Build and push Docker image
|
||||||
|
command: |
|
||||||
|
# Remove npm config
|
||||||
|
rm -f ./.npmrc
|
||||||
|
# Set our version number using vars
|
||||||
|
echo $CIRCLE_BUILD_NUM
|
||||||
|
# Build our image, auth, and push
|
||||||
|
docker build --tag ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM .
|
||||||
|
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||||
|
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
|
||||||
|
|
||||||
|
###
|
||||||
|
# Workflow: DEPLOY
|
||||||
|
###
|
||||||
|
BUILD:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
# Checkout code and ALL Git Tags
|
||||||
|
- checkout:
|
||||||
|
post:
|
||||||
|
- git fetch --all
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn and Cypress Package Cache
|
||||||
|
keys:
|
||||||
|
# when lock file changes, use increasingly general patterns to restore cache
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
|
- yarn-packages-
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
|
- save_cache:
|
||||||
|
name: Save Yarn Package Cache
|
||||||
|
paths:
|
||||||
|
- ~/.cache ## Cache yarn and Cypress
|
||||||
|
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
|
# Build & Test
|
||||||
|
- run:
|
||||||
|
name: 'Build the OHIF Viewer'
|
||||||
|
command: yarn run build
|
||||||
|
no_output_timeout: 45m
|
||||||
|
# - run:
|
||||||
|
# name: 'Upload SourceMaps, Send Deploy Notification'
|
||||||
|
# command: |
|
||||||
|
# # export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
|
||||||
|
# # export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
|
||||||
|
# # export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
|
||||||
|
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
|
||||||
|
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
|
||||||
|
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
|
||||||
|
# curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
|
||||||
# Persist :+1:
|
# Persist :+1:
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/repo
|
root: ~/repo
|
||||||
paths: .
|
paths:
|
||||||
|
- platform/viewer/dist
|
||||||
|
- netlify.toml
|
||||||
|
- .netlify
|
||||||
|
|
||||||
NPM_PUBLISH:
|
DEPLOY_TO_DEV:
|
||||||
<<: *defaults
|
docker:
|
||||||
|
- image: circleci/node:12.9.1
|
||||||
|
environment:
|
||||||
|
TERM: xterm
|
||||||
|
NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
|
||||||
|
working_directory: ~/repo
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/repo
|
at: ~/repo
|
||||||
|
- run: cd .netlify && npm install
|
||||||
|
- run:
|
||||||
|
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||||
|
- run: cd .netlify && npm run deploy
|
||||||
|
|
||||||
|
DEPLOY_TO_STAGING:
|
||||||
|
docker:
|
||||||
|
- image: circleci/node:12.9.1
|
||||||
|
environment:
|
||||||
|
TERM: xterm
|
||||||
|
NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
|
||||||
|
working_directory: ~/repo
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: ~/repo
|
||||||
|
- run: cd .netlify && npm install
|
||||||
|
- run:
|
||||||
|
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||||
|
- run: cd .netlify && npm run deploy
|
||||||
|
|
||||||
|
DEPLOY_TO_PRODUCTION:
|
||||||
|
docker:
|
||||||
|
- image: circleci/node:12.9.1
|
||||||
|
environment:
|
||||||
|
TERM: xterm
|
||||||
|
NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
|
||||||
|
working_directory: ~/repo
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: ~/repo
|
||||||
|
- run: cd .netlify && npm install
|
||||||
|
- run:
|
||||||
|
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||||
|
- run: cd .netlify && npm run deploy
|
||||||
|
|
||||||
|
###
|
||||||
|
# Workflow: RELEASE
|
||||||
|
###
|
||||||
|
NPM_PUBLISH:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- run: yarn -v
|
||||||
|
# Checkout code and ALL Git Tags
|
||||||
|
- checkout:
|
||||||
|
post:
|
||||||
|
- git fetch --all
|
||||||
|
# Use increasingly general patterns to restore cache
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn and Cypress Package Cache
|
||||||
|
keys:
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
|
- yarn-packages-
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
|
- save_cache:
|
||||||
|
name: Save Yarn Package Cache
|
||||||
|
paths:
|
||||||
|
- ~/.cache/yarn
|
||||||
|
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
- run:
|
- run:
|
||||||
name: Avoid hosts unknown for github
|
name: Avoid hosts unknown for github
|
||||||
command:
|
command: |
|
||||||
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
|
rm -rf ~/.ssh
|
||||||
no\n" > ~/.ssh/config
|
mkdir ~/.ssh/
|
||||||
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||||
- run: git config --global user.name "ohif-bot"
|
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||||
|
git config --global user.name "ohif-bot"
|
||||||
- run:
|
- run:
|
||||||
name: Authenticate with registry
|
name: Authenticate with NPM registry
|
||||||
command:
|
command:
|
||||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||||
- run: npx lerna version
|
- run: npx lerna version
|
||||||
- run: npx lerna publish from-package
|
- run: npx lerna publish from-package
|
||||||
|
|
||||||
# Persist :+1:
|
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/repo
|
root: ~/repo
|
||||||
paths: .
|
paths: .
|
||||||
@@ -123,39 +266,39 @@ jobs:
|
|||||||
DOCS_PUBLISH:
|
DOCS_PUBLISH:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- checkout
|
||||||
at: ~/repo
|
|
||||||
- run:
|
- run:
|
||||||
name: Avoid hosts unknown for github
|
name: Avoid hosts unknown for github
|
||||||
command:
|
command: |
|
||||||
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
|
rm -rf ~/.ssh
|
||||||
no\n" > ~/.ssh/config
|
mkdir ~/.ssh/
|
||||||
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||||
- run: git config --global user.name "ohif-bot"
|
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||||
- run: yarn global add gitbook-cli
|
git config --global user.name "ohif-bot"
|
||||||
- run: yarn global add gh-pages
|
- run: yarn global add gitbook-cli gh-pages
|
||||||
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
||||||
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
||||||
|
|
||||||
DOCKER_PUBLISH:
|
DOCKER_MASTER_PUBLISH:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/repo
|
at: ~/repo
|
||||||
- setup_remote_docker:
|
- setup_remote_docker:
|
||||||
docker_layer_caching: true
|
docker_layer_caching: false
|
||||||
- run:
|
- run:
|
||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
command: |
|
command: |
|
||||||
# This file will exist if a new version was published by
|
# This file will exist if a new version was published by
|
||||||
# our `semantic-release` command in the previous job
|
# our command in the previous job. Created in npm postpublish hook
|
||||||
if [[ ! -e tmp/updated-version.txt ]]; then
|
# in the `platform/viewer` project.
|
||||||
|
if [[ ! -e platform/viewer/success_version.txt ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
# Remove npm config
|
# Remove npm config
|
||||||
rm -f ./.npmrc
|
rm -f ./.npmrc
|
||||||
# Set our version number using vars
|
# Set our version number using vars
|
||||||
export IMAGE_VERSION=$(cat tmp/updated-version.txt)
|
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
|
||||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
||||||
echo $IMAGE_VERSION
|
echo $IMAGE_VERSION
|
||||||
echo $IMAGE_VERSION_FULL
|
echo $IMAGE_VERSION_FULL
|
||||||
@@ -166,80 +309,34 @@ jobs:
|
|||||||
docker push ohif/$IMAGE_NAME:latest
|
docker push ohif/$IMAGE_NAME:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VIEWER_DEMO_BUILD:
|
|
||||||
<<: *defaults
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: ~/repo
|
|
||||||
# Build & Test
|
|
||||||
- run:
|
|
||||||
name: 'Build Demo Site'
|
|
||||||
command: yarn run build:demo
|
|
||||||
no_output_timeout: 30m
|
|
||||||
- run:
|
|
||||||
name: 'Upload SourceMaps, Send Deploy Notification'
|
|
||||||
command: |
|
|
||||||
# export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
|
|
||||||
# export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
|
|
||||||
# export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
|
|
||||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
|
|
||||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
|
|
||||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
|
|
||||||
curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
|
|
||||||
# Persist :+1:
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/repo
|
|
||||||
paths: .
|
|
||||||
|
|
||||||
VIEWER_DEMO_PUBLISH:
|
|
||||||
working_directory: ~/repo
|
|
||||||
docker:
|
|
||||||
- image: google/cloud-sdk
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: ~/repo
|
|
||||||
- setup_remote_docker:
|
|
||||||
docker_layer_caching: true
|
|
||||||
- run:
|
|
||||||
name: Deploy latest version to viewer.ohif.org
|
|
||||||
command: |
|
|
||||||
# https://circleci.com/docs/2.0/google-auth/#authorization
|
|
||||||
echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-
|
|
||||||
gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
|
|
||||||
gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
|
|
||||||
# Remove all files in Storage Bucket; Push latest
|
|
||||||
gsutil -m rm gs://$GOOGLE_STORAGE_BUCKET/**
|
|
||||||
gsutil -m rsync -R platform/viewer/dist gs://$GOOGLE_STORAGE_BUCKET
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
# PULL REQUESTS
|
|
||||||
PR_CHECKS:
|
PR_CHECKS:
|
||||||
jobs:
|
jobs:
|
||||||
- CHECKOUT:
|
- UNIT_TESTS:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore:
|
ignore:
|
||||||
- master
|
- master
|
||||||
- feature/*
|
- feature/*
|
||||||
- hotfix/*
|
- hotfix/*
|
||||||
- UNIT_TESTS:
|
|
||||||
requires:
|
|
||||||
- CHECKOUT
|
|
||||||
# E2E: PWA
|
# E2E: PWA
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: PWA'
|
name: 'E2E: PWA'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
|
record: false
|
||||||
store_artifacts: false
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build
|
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
wait-on: 'http://localhost:3000'
|
wait-on: 'http://localhost:3000'
|
||||||
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
|
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||||
no-workspace: true # Don't persist workspace
|
no-workspace: true # Don't persist workspace
|
||||||
post-steps:
|
post-steps:
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
@@ -249,16 +346,20 @@ workflows:
|
|||||||
# E2E: script-tag
|
# E2E: script-tag
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: Script Tag'
|
name: 'E2E: Script Tag'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
browser: chrome
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
|
record: false
|
||||||
store_artifacts: false
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build:package
|
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||||
start: yarn run test:e2e:dist
|
start: yarn run test:e2e:serve
|
||||||
|
spec: 'cypress/integration/common/**/*'
|
||||||
wait-on: 'http://localhost:3000'
|
wait-on: 'http://localhost:3000'
|
||||||
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
|
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||||
no-workspace: true # Don't persist workspace
|
no-workspace: true # Don't persist workspace
|
||||||
post-steps:
|
post-steps:
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
@@ -266,78 +367,124 @@ workflows:
|
|||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- UNIT_TESTS
|
||||||
|
|
||||||
# MERGE TO MASTER
|
PR_OPTIONAL_VISUAL_TESTS:
|
||||||
RELEASE:
|
|
||||||
jobs:
|
jobs:
|
||||||
- CHECKOUT:
|
- AWAIT_APPROVAL:
|
||||||
filters:
|
type: approval
|
||||||
branches:
|
# Update hub.docker.org
|
||||||
only: master
|
|
||||||
- UNIT_TESTS:
|
|
||||||
requires:
|
|
||||||
- CHECKOUT
|
|
||||||
# E2E: PWA + Persist
|
|
||||||
- cypress/run:
|
- cypress/run:
|
||||||
name: 'E2E: PWA'
|
name: 'Generate Percy Snapshots'
|
||||||
pre-steps:
|
executor: cypress/browsers-chrome76
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
|
||||||
add wait-on' # Use yarn latest
|
|
||||||
yarn: true
|
|
||||||
record: true
|
|
||||||
store_artifacts: true
|
|
||||||
working_directory: platform/viewer
|
|
||||||
build: yarn run build
|
|
||||||
start: yarn run test:e2e:dist
|
|
||||||
wait-on: 'http://localhost:3000'
|
|
||||||
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
|
|
||||||
no-workspace: true # Don't persist workspace
|
|
||||||
post-steps:
|
|
||||||
- store_artifacts:
|
|
||||||
path: platform/viewer/cypress/screenshots
|
|
||||||
- store_test_results:
|
|
||||||
path: cypress/results
|
|
||||||
requires:
|
|
||||||
- UNIT_TESTS
|
|
||||||
# E2E: script-tag
|
|
||||||
- cypress/run:
|
|
||||||
name: 'E2E: Script Tag'
|
|
||||||
pre-steps:
|
pre-steps:
|
||||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
add wait-on' # Use yarn latest
|
add wait-on' # Use yarn latest
|
||||||
yarn: true
|
yarn: true
|
||||||
store_artifacts: false
|
store_artifacts: false
|
||||||
working_directory: platform/viewer
|
working_directory: platform/viewer
|
||||||
build: yarn run build:package
|
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||||
start: yarn run test:e2e:dist
|
# start server --> verify running --> percy + chrome + cypress
|
||||||
wait-on: 'http://localhost:3000'
|
command: yarn run test:e2e:dist
|
||||||
cache-key: 'yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}'
|
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||||
no-workspace: true # Don't persist workspace
|
no-workspace: true # Don't persist workspace
|
||||||
post-steps:
|
post-steps:
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: platform/viewer/cypress/screenshots
|
path: platform/viewer/cypress/screenshots
|
||||||
- store_test_results:
|
|
||||||
path: cypress/results
|
|
||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- AWAIT_APPROVAL
|
||||||
# Update NPM
|
|
||||||
- NPM_PUBLISH:
|
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||||
requires:
|
jobs:
|
||||||
- 'E2E: Script Tag'
|
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||||
- 'E2E: PWA'
|
- AWAIT_APPROVAL:
|
||||||
- DOCS_PUBLISH:
|
type: approval
|
||||||
requires:
|
|
||||||
- 'E2E: Script Tag'
|
|
||||||
- 'E2E: PWA'
|
|
||||||
- VIEWER_DEMO_BUILD:
|
|
||||||
requires:
|
|
||||||
- 'E2E: Script Tag'
|
|
||||||
- 'E2E: PWA'
|
|
||||||
- VIEWER_DEMO_PUBLISH:
|
|
||||||
requires:
|
|
||||||
- VIEWER_DEMO_BUILD
|
|
||||||
# Update hub.docker.org
|
# Update hub.docker.org
|
||||||
# - DOCKER_PUBLISH:
|
- DOCKER_PR_PUBLISH:
|
||||||
# requires:
|
context: Docker Hub
|
||||||
# - NPM_PUBLISH
|
requires:
|
||||||
# Update viewer.ohif.org
|
- AWAIT_APPROVAL
|
||||||
# Update docs.ohif.org
|
|
||||||
|
###
|
||||||
|
# Our workflow for building, deploying, and promoting builds across our
|
||||||
|
# development, staging, and production environments.
|
||||||
|
###
|
||||||
|
DEPLOY:
|
||||||
|
jobs:
|
||||||
|
- BUILD:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
- DEPLOY_TO_DEV:
|
||||||
|
requires:
|
||||||
|
- BUILD
|
||||||
|
- PROMOTE_TO_STAGING:
|
||||||
|
type: approval
|
||||||
|
requires:
|
||||||
|
- DEPLOY_TO_DEV
|
||||||
|
- DEPLOY_TO_STAGING:
|
||||||
|
requires:
|
||||||
|
- PROMOTE_TO_STAGING
|
||||||
|
- PROMOTE_TO_PRODUCTION:
|
||||||
|
type: approval
|
||||||
|
requires:
|
||||||
|
- DEPLOY_TO_STAGING
|
||||||
|
- DEPLOY_TO_PRODUCTION:
|
||||||
|
requires:
|
||||||
|
- PROMOTE_TO_PRODUCTION
|
||||||
|
###
|
||||||
|
# Unit and E2E tests have already run for PR_CHECKS
|
||||||
|
# Re-running should not gain us any confidence here
|
||||||
|
###
|
||||||
|
RELEASE:
|
||||||
|
jobs:
|
||||||
|
- NPM_PUBLISH:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
- DOCS_PUBLISH:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
# Update base branch snapshots
|
||||||
|
# and record a Cypress dashboard test run
|
||||||
|
- cypress/run:
|
||||||
|
name: 'Generate Percy Snapshots'
|
||||||
|
executor: cypress/browsers-chrome76
|
||||||
|
pre-steps:
|
||||||
|
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||||
|
add wait-on' # Use yarn latest
|
||||||
|
yarn: true
|
||||||
|
store_artifacts: false
|
||||||
|
working_directory: platform/viewer
|
||||||
|
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||||
|
# start server --> verify running --> percy + chrome + cypress
|
||||||
|
command: yarn run test:e2e:dist
|
||||||
|
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||||
|
no-workspace: true # Don't persist workspace
|
||||||
|
post-steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: platform/viewer/cypress/screenshots
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
- DOCKER_MASTER_PUBLISH:
|
||||||
|
requires:
|
||||||
|
- NPM_PUBLISH
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Potentially use to E2E test STAGING environment
|
||||||
|
# before publishing to production
|
||||||
|
# - cypress/run:
|
||||||
|
# name: 'E2E: PWA'
|
||||||
|
# executor: deploy-to-prod-executor
|
||||||
|
# browser: chrome
|
||||||
|
# working_directory: platform/viewer
|
||||||
|
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||||
|
# yarn: true
|
||||||
|
# record: false
|
||||||
|
# no-workspace: true
|
||||||
|
# store_artifacts: true
|
||||||
|
# post-steps:
|
||||||
|
# - store_artifacts:
|
||||||
|
# path: platform/viewer/cypress/screenshots
|
||||||
|
# - store_test_results:
|
||||||
|
# path: cypress/results
|
||||||
@@ -8,11 +8,13 @@ coverage:
|
|||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
threshold: 0.10%
|
threshold: 0.5%
|
||||||
core:
|
core:
|
||||||
flags: core
|
flags: core
|
||||||
|
threshold: 0.5%
|
||||||
viewer:
|
viewer:
|
||||||
flags: viewer
|
flags: viewer
|
||||||
|
threshold: 0.5%
|
||||||
patch: off
|
patch: off
|
||||||
flags:
|
flags:
|
||||||
core:
|
core:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
|
||||||
|
if [ ! -z "$CLIENT_ID" ]
|
||||||
|
then
|
||||||
|
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
|
||||||
|
echo $CLIENT_ID
|
||||||
|
echo "Updating config..."
|
||||||
|
|
||||||
|
# - Use SED to replace the CLIENT_ID that is currently in google.js
|
||||||
|
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
|
||||||
|
|
||||||
|
# - Copy google.js to overwrite app-config.js
|
||||||
|
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting Nginx to serve the OHIF Viewer..."
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Reduces size of context and hides
|
||||||
|
# files from Docker (can't COPY or ADD these)
|
||||||
|
|
||||||
|
# Output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Root
|
||||||
|
README.md
|
||||||
|
Dockerfile
|
||||||
|
dockerfile
|
||||||
|
|
||||||
|
# Misc. Config
|
||||||
|
.git
|
||||||
|
.DS_Store
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.circleci
|
||||||
|
|
||||||
|
# Unnecessary things to pull into container
|
||||||
|
.circleci/
|
||||||
|
.github/
|
||||||
|
.netlify/
|
||||||
|
.scripts/
|
||||||
|
.vscode/
|
||||||
|
coverage/
|
||||||
|
docs/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PERCY_TOKEN=<your token here>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"cy": true,
|
"cy": true,
|
||||||
|
"before": true,
|
||||||
"context": true,
|
"context": true,
|
||||||
"Cypress": true,
|
"Cypress": true,
|
||||||
"assert": true
|
"assert": true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "\U0001F41B Bug report"
|
name: "\U0001F41B Bug report"
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: 'Community: Report :bug:'
|
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "\U0001F680 Feature request"
|
name: "\U0001F680 Feature request"
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ''
|
title: ''
|
||||||
labels: 'Community: Request :hand:'
|
labels: 'Community: Request :hand:, Triage :white_flag:'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "\U0001F917 Support Question"
|
name: "\U0001F917 Support Question"
|
||||||
about: "I have a question \U0001F4AC"
|
about: "I have a question \U0001F4AC"
|
||||||
title: ''
|
title: ''
|
||||||
labels: 'Community: Question :question:'
|
labels: 'Community: Question :question:, Triage :white_flag:'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
### PR Checklist
|
||||||
|
|
||||||
|
- [ ] Brief description of changes
|
||||||
|
- [ ] Links to any relevant issues
|
||||||
|
- [ ] Required status checks are passing
|
||||||
|
- [ ] User cases if changes impact the user's experience
|
||||||
|
- [ ] `@mention` a maintainer to request a review
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Links
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
|
||||||
|
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -8,14 +8,14 @@ daysUntilClose: 7
|
|||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- 'Story :raised_hands:'
|
- 'Story :raised_hands:'
|
||||||
- 'Bug: Verified 🐛'
|
- 'Bug: Verified :bug:'
|
||||||
- 'Task: CI/Tooling 🤖'
|
- 'Task: CI/Tooling :robot:'
|
||||||
- 'Task: Docs 📖'
|
- 'Task: Docs 📖'
|
||||||
- 'Task: Docs :book:'
|
- 'Task: Docs :book:'
|
||||||
- 'Task: Refactor 🛠'
|
- 'Task: Refactor 🛠'
|
||||||
- 'Task: Tests :microscope:'
|
- 'Task: Tests :microscope:'
|
||||||
- 'PR: Awaiting Review 👀'
|
- 'PR: Awaiting Review 👀'
|
||||||
- 'Triage 🏳️'
|
- 'Triage :white_flag:'
|
||||||
- 'Announcement 🎉'
|
- 'Announcement 🎉'
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: 'Stale :baguette_bread:'
|
staleLabel: 'Stale :baguette_bread:'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ npm-debug.log
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
|
||||||
# Common Example Data Directories
|
# Common Example Data Directories
|
||||||
sampledata/
|
sampledata/
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Specific to our non-deploy-preview deploys
|
||||||
|
# Confgure redirects using netlify.toml
|
||||||
|
|
||||||
|
# PWA Redirect
|
||||||
|
/* /index.html 200
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "netlify deploy --prod --dir ./../platform/viewer/dist"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"netlify-cli": "^2.21.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"editor.rulers": [80, 120],
|
"editor.rulers": [80, 120],
|
||||||
|
|
||||||
// ===
|
// ===
|
||||||
// Spacing
|
// Spacing
|
||||||
// ===
|
// ===
|
||||||
|
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.trimAutoWhitespace": true,
|
"editor.trimAutoWhitespace": true,
|
||||||
@@ -12,19 +10,23 @@
|
|||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.trimFinalNewlines": true,
|
"files.trimFinalNewlines": true,
|
||||||
|
|
||||||
// ===
|
// ===
|
||||||
// Event Triggers
|
// Event Triggers
|
||||||
// ===
|
// ===
|
||||||
|
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.autoFixOnSave": true,
|
"eslint.autoFixOnSave": true,
|
||||||
"eslint.run": "onSave",
|
"eslint.run": "onSave",
|
||||||
"eslint.validate": [
|
"eslint.validate": [
|
||||||
{ "language": "javascript", "autoFix": true },
|
{
|
||||||
{ "language": "javascriptreact", "autoFix": true }
|
"language": "javascript",
|
||||||
|
"autoFix": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "javascriptreact",
|
||||||
|
"autoFix": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"prettier.disableLanguages": [],
|
"prettier.disableLanguages": ["html"],
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"workbench.colorCustomizations": {}
|
"workbench.colorCustomizations": {}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,54 @@
|
|||||||
|
// ~~ ENV
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
//
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
const PACKAGE = require('../platform/viewer/package.json');
|
||||||
|
// ~~ RULES
|
||||||
const loadShadersRule = require('./rules/loadShaders.js');
|
const loadShadersRule = require('./rules/loadShaders.js');
|
||||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||||
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
|
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
|
||||||
|
// ~~ PLUGINS
|
||||||
|
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||||
|
// ~~ ENV VARS
|
||||||
|
const NODE_ENV = process.env.NODE_ENV;
|
||||||
|
const QUICK_BUILD = process.env.QUICK_BUILD;
|
||||||
|
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
|
||||||
|
|
||||||
|
//
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||||
if (!process.env.NODE_ENV) {
|
if (!process.env.NODE_ENV) {
|
||||||
throw new Error('process.env.NODE_ENV not set');
|
throw new Error('process.env.NODE_ENV not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode =
|
const mode = NODE_ENV === 'production' ? 'production' : 'development';
|
||||||
process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
const isProdBuild = NODE_ENV === 'production';
|
||||||
|
const isQuickBuild = QUICK_BUILD === 'true';
|
||||||
|
|
||||||
return {
|
const config = {
|
||||||
mode,
|
mode: isProdBuild ? 'production' : 'development',
|
||||||
|
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
|
||||||
entry: {
|
entry: {
|
||||||
app: `${SRC_DIR}/index.js`,
|
app: `${SRC_DIR}/index.js`,
|
||||||
},
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isProdBuild,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
context: SRC_DIR,
|
context: SRC_DIR,
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
transpileJavaScriptRule(mode),
|
transpileJavaScriptRule(mode),
|
||||||
@@ -45,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||||
|
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
|
||||||
|
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
|
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
|
||||||
@@ -53,4 +86,23 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
fs: 'empty',
|
fs: 'empty',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isProdBuild) {
|
||||||
|
config.optimization.minimizer = [
|
||||||
|
new TerserJSPlugin({
|
||||||
|
// Supports:
|
||||||
|
// source-map and inline-source-map
|
||||||
|
sourceMap: isProdBuild && !isQuickBuild,
|
||||||
|
parallel: true,
|
||||||
|
terserOptions: {},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQuickBuild) {
|
||||||
|
config.optimization.minimize = false;
|
||||||
|
config.devtool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# This dockerfile is used to publish the `ohif/viewer` image on dockerhub.
|
||||||
|
#
|
||||||
|
# It's a good example of how to build our static application and package it
|
||||||
|
# with a web server capable of hosting it as static content.
|
||||||
|
#
|
||||||
|
# docker build
|
||||||
|
# --------------
|
||||||
|
# If you would like to use this dockerfile to build and tag an image, make sure
|
||||||
|
# you set the context to the project's root directory:
|
||||||
|
# https://docs.docker.com/engine/reference/commandline/build/
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# SUMMARY
|
||||||
|
# --------------
|
||||||
|
# This dockerfile has two stages:
|
||||||
|
#
|
||||||
|
# 1. Building the React application for production
|
||||||
|
# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 1: Build the application
|
||||||
|
# docker build -t ohif/viewer:latest .
|
||||||
|
FROM node:10.16.3-slim as builder
|
||||||
|
|
||||||
|
RUN mkdir /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy Files
|
||||||
|
COPY .docker /usr/src/app/.docker
|
||||||
|
COPY .webpack /usr/src/app/.webpack
|
||||||
|
COPY extensions /usr/src/app/extensions
|
||||||
|
COPY platform /usr/src/app/platform
|
||||||
|
COPY .browserslistrc /usr/src/app/.browserslistrc
|
||||||
|
COPY aliases.config.js /usr/src/app/aliases.config.js
|
||||||
|
COPY babel.config.js /usr/src/app/babel.config.js
|
||||||
|
COPY lerna.json /usr/src/app/lerna.json
|
||||||
|
COPY package.json /usr/src/app/package.json
|
||||||
|
COPY postcss.config.js /usr/src/app/postcss.config.js
|
||||||
|
COPY yarn.lock /usr/src/app/yarn.lock
|
||||||
|
|
||||||
|
# Run the install before copying the rest of the files
|
||||||
|
RUN yarn config set workspaces-experimental true
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||||
|
ENV QUICK_BUILD true
|
||||||
|
# ENV GENERATE_SOURCEMAP=false
|
||||||
|
# ENV REACT_APP_CONFIG=config/default.js
|
||||||
|
|
||||||
|
RUN yarn run build
|
||||||
|
|
||||||
|
# Stage 2: Bundle the built application into a Docker container
|
||||||
|
# which runs Nginx using Alpine Linux
|
||||||
|
FROM nginx:1.15.5-alpine
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
RUN rm -rf /etc/nginx/conf.d
|
||||||
|
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
||||||
|
COPY .docker/Viewer-v2.x/entrypoint.sh /usr/src/
|
||||||
|
RUN chmod 777 /usr/src/entrypoint.sh
|
||||||
|
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -22,10 +22,12 @@
|
|||||||
[![NPM downloads][npm-downloads-image]][npm-url]
|
[![NPM downloads][npm-downloads-image]][npm-url]
|
||||||
[![Pulls][docker-pulls-img]][docker-image-url]
|
[![Pulls][docker-pulls-img]][docker-image-url]
|
||||||
[![MIT License][license-image]][license-url]
|
[![MIT License][license-image]][license-url]
|
||||||
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_shield)
|
||||||
|
|
||||||
[![Netlify Status][netlify-image]][netlify-url]
|
[![Netlify Status][netlify-image]][netlify-url]
|
||||||
[![CircleCI][circleci-image]][circleci-url]
|
[![CircleCI][circleci-image]][circleci-url]
|
||||||
[![codecov][codecov-image]][codecov-url]
|
[![codecov][codecov-image]][codecov-url]
|
||||||
|
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url)
|
||||||
[](#contributors)
|
[](#contributors)
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ We offer support through
|
|||||||
|
|
||||||
For commercial support, academic collaberations, and answers to common
|
For commercial support, academic collaberations, and answers to common
|
||||||
questions; please read our
|
questions; please read our
|
||||||
[documented FAQ](https://docs.ohif.org/frequently-asked-questions.html#does-ohif-offer-commercial-support).
|
[documented FAQ](https://docs.ohif.org/faq/index.html#does-ohif-offer-commercial-support).
|
||||||
|
|
||||||
## Quick Start Deployment
|
## Quick Start Deployment
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ deployment recipe.
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
||||||
- [Node 8+](https://nodejs.org/en/)
|
- [Node 10+](https://nodejs.org/en/)
|
||||||
- Yarn Workspaces should be enabled on your machine:
|
- Yarn Workspaces should be enabled on your machine:
|
||||||
- `yarn config set workspaces-experimental true`
|
- `yarn config set workspaces-experimental true`
|
||||||
|
|
||||||
@@ -269,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
|
|||||||
<!-- Badges -->
|
<!-- Badges -->
|
||||||
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
|
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
|
||||||
[lerna-url]: https://lerna.js.org/
|
[lerna-url]: https://lerna.js.org/
|
||||||
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
|
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
|
||||||
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
|
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
|
||||||
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
|
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
|
||||||
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
|
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
|
||||||
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
|
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
|
||||||
@@ -288,18 +290,20 @@ MIT © [OHIF](https://github.com/OHIF)
|
|||||||
[docker-image-url]: https://hub.docker.com/r/ohif/viewer
|
[docker-image-url]: https://hub.docker.com/r/ohif/viewer
|
||||||
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
|
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
|
||||||
[license-url]: LICENSE
|
[license-url]: LICENSE
|
||||||
|
[percy-image]: https://percy.io/static/images/percy-badge.svg
|
||||||
|
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
|
||||||
<!-- Links -->
|
<!-- Links -->
|
||||||
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
||||||
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
||||||
[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||||
[ohif-architecture]: https://docs.ohif.org/advanced/architecture.html
|
[ohif-architecture]: https://docs.ohif.org/architecture/index.html
|
||||||
[ohif-extensions]: https://docs.ohif.org/advanced/architecture.html
|
[ohif-extensions]: https://docs.ohif.org/architecture/index.html
|
||||||
[deployment-docs]: https://docs.ohif.org/deployment/
|
[deployment-docs]: https://docs.ohif.org/deployment/
|
||||||
[react-url]: https://reactjs.org/
|
[react-url]: https://reactjs.org/
|
||||||
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
|
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
|
||||||
[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/viewer
|
[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/viewer
|
||||||
[configuration-url]: https://docs.ohif.org/essentials/configuration.html
|
[configuration-url]: https://docs.ohif.org/configuring/
|
||||||
[extensions-url]: https://docs.ohif.org/advanced/extensions.html
|
[extensions-url]: https://docs.ohif.org/extensions/
|
||||||
<!-- Platform -->
|
<!-- Platform -->
|
||||||
[platform-core]: platform/core/README.md
|
[platform-core]: platform/core/README.md
|
||||||
[core-npm]: https://www.npmjs.com/package/@ohif/core
|
[core-npm]: https://www.npmjs.com/package/@ohif/core
|
||||||
@@ -321,3 +325,5 @@ MIT © [OHIF](https://github.com/OHIF)
|
|||||||
[extension-vtk]: extensions/vtk/README.md
|
[extension-vtk]: extensions/vtk/README.md
|
||||||
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
|
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
|
||||||
@@ -38,9 +38,9 @@ our integration and extension points.
|
|||||||
Check out these helpful links:
|
Check out these helpful links:
|
||||||
|
|
||||||
- Ready to dive into some code? Check out our
|
- Ready to dive into some code? Check out our
|
||||||
[Getting Started Guide](./essentials/getting-started.md).
|
[Getting Started Guide](./development/getting-started.md).
|
||||||
- We're an active, vibrant community.
|
- We're an active, vibrant community.
|
||||||
[Learn how you can be more involved.](./contributing/index.md)
|
[Learn how you can be more involved.](./development/contributing.md)
|
||||||
- Feeling lost? Read our [help page](./help.md).
|
- Feeling lost? Read our [help page](./help.md).
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -1,31 +1,41 @@
|
|||||||
# OHIF Viewers
|
# OHIF Viewers
|
||||||
|
|
||||||
- Essentials
|
- [Our Process](our-process.md)
|
||||||
- [Getting Started](essentials/getting-started.md)
|
- [Quick Start](quick-start.md)
|
||||||
- [Installation](essentials/installation.md)
|
- Development
|
||||||
- [Data Source](essentials/data-source.md)
|
- [Getting Started](development/getting-started.md)
|
||||||
- [Configuration](essentials/configuration.md)
|
- [Contributing](development/contributing.md)
|
||||||
- [Themeing](essentials/themeing.md)
|
- [Continuous Integration](development/continous-integration.md)
|
||||||
- [Translating](essentials/translating.md)
|
- [Configuring](configuring/index.md)
|
||||||
- [Troubleshooting](essentials/troubleshooting.md)
|
- [Data Source](configuring/data-source.md)
|
||||||
- [Scope of Project](essentials/scope-of-project.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Advanced](advanced/index.md)
|
- [Architecture](architecture/index.md)
|
||||||
- [Architecture](advanced/architecture.md)
|
- [Viewer](viewer/index.md)
|
||||||
- [Overview](advanced/architecture.md#overview)
|
- [Configuration](viewer/configuration.md)
|
||||||
- [Business Logic](advanced/architecture.md#business-logic)
|
- [Themeing](viewer/themeing.md)
|
||||||
- [Component Library](advanced/architecture.md#react-component-library)
|
- [Internationalization](viewer/internationalization.md)
|
||||||
- [Extensions](advanced/architecture.md#misc-extensions)
|
- [Extensions](extensions/index.md)
|
||||||
- [Diagram](advanced/architecture.md#diagram)
|
- [Registering](extensions/index.md#registering-an-extension)
|
||||||
- [Common Questions](advanced/architecture.md#common-questions)
|
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
|
||||||
- [Extensions](advanced/extensions.md)
|
- [preRegistration](extensions/lifecycle/pre-registration.md)
|
||||||
- [Overview](advanced/extensions.md#overview)
|
- [Modules](extensions/index.md#modules)
|
||||||
- [Modules](advanced/extensions.md#modules)
|
- [Commands](extensions/modules/commands.md)
|
||||||
- [Registering](advanced/extensions.md#registering-extensions)
|
- [Panel](extensions/modules/panel.md)
|
||||||
- [OHIF Maintained](advanced/extensions.md#ohif-maintained-extensions)
|
- [SOP Class Handler](extensions/modules/sop-class-handler.md)
|
||||||
- [Custom Tools](advanced/custom-tools.md)
|
- [Toolbar](extensions/modules/toolbar.md)
|
||||||
|
- [Viewport](extensions/modules/viewport.md)
|
||||||
|
- [Contexts](extensions/index.md#contexts)
|
||||||
|
- [ExtensionManager](extensions/index.md#extensionmanager)
|
||||||
|
- [OHIF Maintained](extensions/index.md#maintained-extensions)
|
||||||
|
- [Services](services/index.md)
|
||||||
|
- [Default](services/default/index.md)
|
||||||
|
- [Measurements](services/default/measurements.md)
|
||||||
|
- [UI](services/ui/index.md)
|
||||||
|
- [Dialog Service](services/ui/ui-dialog-service.md)
|
||||||
|
- [Modal Service](services/ui/ui-modal-service.md)
|
||||||
|
- [Notification Service](services/ui/ui-notification-service.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,7 +55,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [FAQ](frequently-asked-questions.md)
|
- [FAQ](faq/index.md)
|
||||||
- [Contributing](contributing/index.md)
|
- [Scope of Project](faq/scope-of-project.md)
|
||||||
- [Browser Support](browser-support.md)
|
- [Browser Support](faq/browser-support.md)
|
||||||
|
- [PWA vs Packaged](faq/pwa-vs-packaged.md)
|
||||||
- [Help](help.md)
|
- [Help](help.md)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Tool Management
|
|
||||||
|
|
||||||
This is not yet exposed in an easy/convenient way. Most tools are currently
|
|
||||||
added by creating new Viewport, Toolbar, and SOPInstanceHandler extension
|
|
||||||
modules. You can read more about that approach in [extensions](./extensions.md).
|
|
||||||
|
|
||||||
In the near future, we intend to improve the extensibility of tools for existing
|
|
||||||
Viewports (like our Cornerstone.js and VTK.js viewports).
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
# Extensions
|
|
||||||
|
|
||||||
Extensions add new functionality to the viewer by registering one or more
|
|
||||||
modules. They go one step further than configuration in that they allow us to
|
|
||||||
inject custom React components, so long as they adhere to the module's
|
|
||||||
interface. This can be something as simple as adding a new button to the
|
|
||||||
toolbar, or as complex as a new viewport capable of rendering volumes in 3D.
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Modules](#modules)
|
|
||||||
- [Commands](#commands)
|
|
||||||
- [Hotkeys](#hotkeys)
|
|
||||||
- [Toolbar](#toolbar)
|
|
||||||
- [Panel](#panel)
|
|
||||||
- [Viewport](#viewport)
|
|
||||||
- [SOP Class Handler](#sopclasshandler)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
At a glance, an extension is a javascript object that has an `id` property, and
|
|
||||||
one or more "module" methods. You can find an abbreviated extension below, or
|
|
||||||
[view the source][example-ext-src] of our example extension.
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* Only required property. Should be a unique value across all extensions.
|
|
||||||
*/
|
|
||||||
id: 'example-extension',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers one or more named commands scoped to a context. Commands are
|
|
||||||
* the primary means for...
|
|
||||||
*/
|
|
||||||
getCommandsModule() {
|
|
||||||
return {
|
|
||||||
defaultContext: 'VIEWER'
|
|
||||||
actions: { ... },
|
|
||||||
definitions: { ... }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows you to provide toolbar definitions that will be merged with any
|
|
||||||
* existing application toolbar configuration. Used to determine which
|
|
||||||
* buttons should be visible when, their order, what happens when they're
|
|
||||||
* clicked, etc.
|
|
||||||
*/
|
|
||||||
getToolbarModule() {
|
|
||||||
return {
|
|
||||||
definitions: [ ... ],
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not yet implemented
|
|
||||||
*/
|
|
||||||
getPanelModule: () => null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a ReactComponent that should be used to render data in a
|
|
||||||
* Viewport. The first registered viewport is our "default viewport". If
|
|
||||||
* more than one viewport is registered, we use `SopClassHandlers` to
|
|
||||||
* determine which viewport should be used.
|
|
||||||
*/
|
|
||||||
getViewportModule: () => reactViewportComponent,
|
|
||||||
|
|
||||||
/** Provides a whitelist of SOPClassUIDs the viewport is capable of rendering.
|
|
||||||
* Can modify default behavior for methods like `getDisplaySetFromSeries` */
|
|
||||||
getSopClassHandler: () => {
|
|
||||||
id: 'some-other-unique-id',
|
|
||||||
sopClassUids: [ ... ],
|
|
||||||
getDisplaySetFromSeries: (series, study, dicomWebClient, authorizationHeaders) => { ... }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modules
|
|
||||||
|
|
||||||
There are a few different module types. Each module type allows us to extend the
|
|
||||||
viewer in a different way, and provides a consistent API for us to do so. You
|
|
||||||
can find a full list of the different types of modules
|
|
||||||
[`in ohif-core`][module-types]. Information on each type of module, it's API,
|
|
||||||
and how we determine when/where it should be used is included below.
|
|
||||||
|
|
||||||
> NOTE: Modifying the extensions/modules registered to the OHIF Viewer currently
|
|
||||||
> requires us to import and pass extensions to the ExtensionManager in
|
|
||||||
> `src/App.js`, then rebuild the application. Long-term, we intend to make it
|
|
||||||
> possible to accomplish this without a build step.
|
|
||||||
|
|
||||||
#### Commands
|
|
||||||
|
|
||||||
The Commands Module allows us to register one or more commands scoped to
|
|
||||||
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
|
|
||||||
and any registered custom react component (like a [viewport][#] or [panel][#]).
|
|
||||||
Here is a simple example commands module:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
getCommandsModule() {
|
|
||||||
return {
|
|
||||||
actions: {
|
|
||||||
speak: ({ viewports, words }) => {
|
|
||||||
console.log(viewports, words);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
definitions: {
|
|
||||||
rotateViewportCW: {
|
|
||||||
commandFn: actions.rotateViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { rotation: 90 }
|
|
||||||
},
|
|
||||||
rotateViewportCCW: {
|
|
||||||
commandFn: actions.rotateViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { rotation: -90 },
|
|
||||||
context: 'ACTIVE_VIEWER::CORNERSTONE'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultContext: 'VIEWER'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Viewport
|
|
||||||
|
|
||||||
An extension can register a Viewport Module by providing a `getViewportModule()`
|
|
||||||
method that returns a React Component. The React component will receive the
|
|
||||||
following props:
|
|
||||||
|
|
||||||
```js
|
|
||||||
children: PropTypes.arrayOf(PropTypes.element)
|
|
||||||
studies: PropTypes.object,
|
|
||||||
displaySet: PropTypes.object,
|
|
||||||
viewportData: PropTypes.object, // { studies, displaySet }
|
|
||||||
viewportIndex: PropTypes.number,
|
|
||||||
children: PropTypes.node,
|
|
||||||
customProps: PropTypes.object
|
|
||||||
```
|
|
||||||
|
|
||||||
Viewport components are managed by the `LayoutManager`. Which Viewport component
|
|
||||||
is used depends on:
|
|
||||||
|
|
||||||
- The Layout Configuration
|
|
||||||
- Registered SopClassHandlers
|
|
||||||
- The SopClassUID for visible/selected datasets
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<center><i>An example of three Viewports</i></center>
|
|
||||||
|
|
||||||
For a complete example implementation,
|
|
||||||
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
|
|
||||||
|
|
||||||
#### Toolbar
|
|
||||||
|
|
||||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
|
||||||
method that returns a React Component. The component does not receive any props.
|
|
||||||
If you want to modify or react to state, you will need to connect to the redux
|
|
||||||
store.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<center><i>A toolbar extension example</i></center>
|
|
||||||
|
|
||||||
Toolbar components are rendered in the `ToolbarRow` component.
|
|
||||||
|
|
||||||
For a complete example implementation,
|
|
||||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
|
||||||
|
|
||||||
#### SopClassHandler
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
#### Panel
|
|
||||||
|
|
||||||
> The panel module is not yet in use.
|
|
||||||
|
|
||||||
#### Hotkeys
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
### Registering Extensions
|
|
||||||
|
|
||||||
Extensions are registered for the application at startup. The
|
|
||||||
`ExtensionManager`, exposed by `ohif-core`, registers a list of extensions with
|
|
||||||
our application's store. Each module provided by the extension becomes available
|
|
||||||
via `state.plugins.availablePlugins`, and consists of three parts: id, type
|
|
||||||
([PLUGIN_TYPE](https://github.com/OHIF/ohif-core/blob/43c08a29eff3fb646a0e83a03a236ddd84f4a6e8/src/plugins.js#L1-L6)),
|
|
||||||
and the return value of the module method.
|
|
||||||
|
|
||||||
In a future version, we will likely expose a way to provide the extensions you
|
|
||||||
would like included at startup.
|
|
||||||
|
|
||||||
_app.js_
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { createStore, combineReducers } from 'redux';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
import OHIFCornerstoneExtension from 'ohif-cornerstone-extension';
|
|
||||||
|
|
||||||
const combined = combineReducers(OHIF.redux.reducers);
|
|
||||||
const store = createStore(combined);
|
|
||||||
const extensions = [new OHIFCornerstoneExtension()];
|
|
||||||
|
|
||||||
// Dispatches the `addPlugin` action to the store
|
|
||||||
// Adding extension modules to `state.plugins.availablePlugins`
|
|
||||||
ExtensionManager.registerExtensions(store, extensions);
|
|
||||||
```
|
|
||||||
|
|
||||||
## OHIF Maintained Extensions
|
|
||||||
|
|
||||||
A small number of powerful extensions for popular use cases are maintained by
|
|
||||||
OHIF. They're co-located in the
|
|
||||||
[`OHIF/Viewers`](https://github.com/OHIF/Viewers) repository, in the top level
|
|
||||||
[`extensions/`](https://github.com/OHIF/Viewers/tree/master/extensions)
|
|
||||||
directory.
|
|
||||||
|
|
||||||
{% include "./_maintained-extensions-table.md" %}
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[example-ext-src]: https://github.com/OHIF/Viewers/tree/master/extensions/_example/src
|
|
||||||
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Advanced
|
|
||||||
|
|
||||||
Advanced topics go beyond basic configuration and deployment. Their goal is to provide insight into this project's architecture and guidance on leveraging extensions.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# MonoRepos: A Crash Course
|
|
||||||
|
|
||||||
- [Lerna][lerna]
|
|
||||||
|
|
||||||
Solutions:
|
|
||||||
|
|
||||||
## Semantic-Release
|
|
||||||
|
|
||||||
- [Semantic-Release](https://github.com/semantic-release/semantic-release/issues/193#issuecomment-462063871)
|
|
||||||
- [Multi-semantic-release](https://github.com/dhoulb/multi-semantic-release)
|
|
||||||
- [semantic-release-monorepo](https://github.com/Updater/semantic-release-monorepo)
|
|
||||||
|
|
||||||
# Netlify
|
|
||||||
|
|
||||||
- https://community.netlify.com/t/best-practices-for-deploying-sites-from-monorepos/818
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[lerna]: https://github.com/lerna/lerna
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -10,6 +10,13 @@ allows our code to be used in more ways, it also increases complexity. Below, we
|
|||||||
aim to demistify that complexity by providing insight into how our Viewer is
|
aim to demistify that complexity by providing insight into how our Viewer is
|
||||||
architected, and the role each of it's dependent libraries plays.
|
architected, and the role each of it's dependent libraries plays.
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Business Logic](#business-logic)
|
||||||
|
- [Component Library](#react-component-library)
|
||||||
|
- [Internationalization](#internationalization-i18n)
|
||||||
|
- [Extensions & Configuration](#extensions--configuration)
|
||||||
|
- [Common Questions](#common-questions)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
|
The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
|
||||||
@@ -39,27 +46,47 @@ you'll see the following:
|
|||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Continue reading to see how these libraries work together to create the OHIF
|
The `platform` directory contains the business logic library, component library,
|
||||||
Viewer.
|
and the application library that combines them to create a powerful medical
|
||||||
|
imaging viewer.
|
||||||
|
|
||||||
### Business Logic
|
The `extensions` directory contains many packages that can be registered with
|
||||||
|
`@ohif/core`'s `ExtensionManager` to expand an application's supported features
|
||||||
|
and functionality.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>architecture diagram</i></center>
|
||||||
|
|
||||||
|
This diagram is a conceptual illustration of how the Viewer is architected.
|
||||||
|
|
||||||
|
1. (optional) `extensions` can be registered with `@ohif/core`'s
|
||||||
|
`ExtensionManager`
|
||||||
|
2. `@ohif/core` provides bussiness logic and a way for `@ohif/viewer` to access
|
||||||
|
registered extensions
|
||||||
|
3. The `@ohif/viewer` composes and provides data to components from our
|
||||||
|
component library (`@ohif/ui`)
|
||||||
|
4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
|
||||||
|
embeddable package ([`@ohif/viewer`][viewer-npm])
|
||||||
|
|
||||||
|
## Business Logic
|
||||||
|
|
||||||
The [`@ohif/core`][core-github] project offers pre-packaged solutions for
|
The [`@ohif/core`][core-github] project offers pre-packaged solutions for
|
||||||
features common to Web-based medical imaging viewers. For example:
|
features common to Web-based medical imaging viewers. For example:
|
||||||
|
|
||||||
- Hotkeys
|
- [Hotkeys](#)
|
||||||
- DICOM Web requests
|
- DICOM Web requests
|
||||||
- Hanging Protocols
|
- Hanging Protocols
|
||||||
- Managing a study's measurements
|
- [Managing a study's measurements](../services/default/measurements.md)
|
||||||
- Managing a study's DICOM metadata
|
- Managing a study's DICOM metadata
|
||||||
- A flexible pattern for extensions
|
- [A flexible pattern for extensions](../extensions/index.md)
|
||||||
- And many others
|
- And many others
|
||||||
|
|
||||||
It does this while remaining decoupled from any particular view library or
|
It does this while remaining decoupled from any particular view library or
|
||||||
rendering logic. While we use it to power our React Viewer, it can be used with
|
rendering logic. While we use it to power our React Viewer, it can be used with
|
||||||
Vue, React, Vanilla JS, or any number of other frameworks.
|
Vue, React, Vanilla JS, or any number of other frameworks.
|
||||||
|
|
||||||
### React Component Library
|
## React Component Library
|
||||||
|
|
||||||
[`@ohif/ui`][ui-github] is a React Component library that contains the reusable
|
[`@ohif/ui`][ui-github] is a React Component library that contains the reusable
|
||||||
components that power the OHIF Viewer. It allows us to build, compose, and test
|
components that power the OHIF Viewer. It allows us to build, compose, and test
|
||||||
@@ -72,15 +99,11 @@ application.
|
|||||||
|
|
||||||
[Check out our component library!](https://react.ohif.org/)
|
[Check out our component library!](https://react.ohif.org/)
|
||||||
|
|
||||||
### Internationalization (i18n)
|
## Internationalization (i18n)
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
### The Viewer
|
## Extensions & Configuration
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
### Extensions & Configuration
|
|
||||||
|
|
||||||
While OHIF maintains several high value and commonly requested features in its
|
While OHIF maintains several high value and commonly requested features in its
|
||||||
own extensions, there are many instances where one may wish to further extend
|
own extensions, there are many instances where one may wish to further extend
|
||||||
@@ -94,33 +117,16 @@ the viewer. Some common use cases include:
|
|||||||
- Adding surgical templating tools and reports
|
- Adding surgical templating tools and reports
|
||||||
- and many others
|
- and many others
|
||||||
|
|
||||||
We expose common integration points via [extensions](./extensions.md) to make
|
We expose common integration points via [extensions](../extensions/index.md) to
|
||||||
this possible. The viewer and many of our own extensions also offer
|
make this possible. The viewer and many of our own extensions also offer
|
||||||
[configuration][configuration]. For a list of extensions maintained by OHIF,
|
[configuration][configuration]. For a list of extensions maintained by OHIF,
|
||||||
[check out this helpful table](./extensions.html#ohif-maintained-extensions).
|
[check out this helpful table](../extensions/index.mdl#maintained-extensions).
|
||||||
|
|
||||||
If you find yourself thinking "I wish the Viewer could do X", and you can't
|
If you find yourself thinking "I wish the Viewer could do X", and you can't
|
||||||
accomplish it with an extension today, create a GitHub issue! We're actively
|
accomplish it with an extension today, create a GitHub issue! We're actively
|
||||||
looking for ways to improve our extensibility ^\_^
|
looking for ways to improve our extensibility ^\_^
|
||||||
|
|
||||||
[Click here to read more about extensions!](./extensions.md)
|
[Click here to read more about extensions!](../extensions/index.md)
|
||||||
|
|
||||||
### Diagram
|
|
||||||
|
|
||||||
This diagram is a conceptual illustration of how the Viewer is architected.
|
|
||||||
|
|
||||||
1. (optional) `extensions` can be registered with `@ohif/core`'s extension
|
|
||||||
manager
|
|
||||||
2. `@ohif/core` provides bussiness logic and a way for `@ohif/viewer` to access
|
|
||||||
registered extensions
|
|
||||||
3. The `@ohif/viewer` composes and provides data to components from our
|
|
||||||
component library (`@ohif/ui`)
|
|
||||||
4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
|
|
||||||
embeddable package ([`@ohif/viewer`][viewer-npm])
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<center><i>architecture diagram</i></center>
|
|
||||||
|
|
||||||
## Common Questions
|
## Common Questions
|
||||||
|
|
||||||
@@ -141,11 +147,12 @@ you would need to recreate.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
[monorepo]: #
|
||||||
[viewers-project]: https://github.com/OHIF/Viewers
|
[viewers-project]: https://github.com/OHIF/Viewers
|
||||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||||
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
||||||
[configuration]: ../essentials/configuration.md
|
[configuration]: ../configuring/index.md
|
||||||
[extensions]: ./extensions.md
|
[extensions]: ../extensions/index.md
|
||||||
[core-github]: https://github.com/OHIF/viewers/platform/core
|
[core-github]: https://github.com/OHIF/viewers/platform/core
|
||||||
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
|
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 260.4 82.9" enable-background="new 0 0 260.4 82.9" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path fill="#525DF9" d="M31.7,8.9c9.6,0,16.5,4.9,19,13.4l0.1,0.4h9.6l-0.1-0.6C57.6,8.5,46.7,0,31.8,0C13.7,0,0,14.1,0,32.9 s13.7,32.9,31.8,32.9c14.9,0,25.8-8.5,28.6-22.1l0.1-0.6h-9.6l-0.1,0.4c-2.5,8.5-9.5,13.4-19,13.4c-13.1,0-21.9-9.6-21.9-24 C9.8,18.6,18.6,8.9,31.7,8.9z"/>
|
|
||||||
<path fill="#525DF9" d="M110.1,58c-2.3,0-3.5-1.4-3.5-4.2V32.7c0-8.8-7.2-14.7-17.9-14.7c-7.5,0-16.7,3.9-18.3,14.9l-0.1,0.6h8.4 l0.1-0.4c1.4-5.7,5.8-6.9,9.3-6.9c6,0,9.5,2.7,9.5,7.4v2.6l-13.6,1.4c-7.6,0.8-15.7,5-15.7,14.6c0,8.1,6,13.5,14.8,13.5 c6.5,0,11.8-3.4,15-6.8c1.3,4.2,4.5,6.5,9.1,6.5c1.7,0,3.2-0.3,5.2-0.9l0.3-0.1v-7l-0.6,0.2C111.5,57.9,110.9,58,110.1,58z M97.7,43.5v7.3c-4.6,4.6-8.9,6.7-13.5,6.7c-2,0-6.8-0.6-6.8-5.7c0-3.8,2.8-6.3,7.4-6.9L97.7,43.5z"/>
|
|
||||||
<path fill="#525DF9" d="M146.5,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C161.9,23.7,156.1,18,146.5,18z"/>
|
|
||||||
<path fill="#525DF9" d="M195.8,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C211.2,23.7,205.4,18,195.8,18z"/>
|
|
||||||
<polygon fill="#525DF9" points="251.3,18.9 238.6,51.8 237.8,47.9 225.6,18.9 216.2,18.9 234.3,62.4 226,82.9 235,82.9 260.4,18.9 "/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 445.9 638" enable-background="new 0 0 445.9 638" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path fill="#5F5DF9" d="M224.5,638C101.1,638,0,537.8,0,414.7V223.3C0,100.2,101.1,0,224.5,0c109.6,0,202.8,78.3,220.9,186.1
|
|
||||||
c2.9,17.4-8.6,33.8-26,36.8c-17.4,2.9-33.7-8.8-36.7-26.2c-13-77-78.6-132.9-157-132.9c-88.2,0-159.3,71.6-159.3,159.5v191.4
|
|
||||||
c0,88,71.1,159.5,159.3,159.5c78.3,0,144.2-55.9,157.1-132.9c2.9-17.4,19.3-29.1,36.6-26.2c17.4,2.9,28.6,19.4,25.7,36.7
|
|
||||||
C427.2,559.7,334,638,224.5,638z"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.5">
|
|
||||||
<path fill="#5F5DF9" d="M153.6,347.7c-17.6,0-30.7-14.3-30.7-31.9v-92.3c0-56.8,45.8-103,102.8-103c38.9,0,73.9,21.6,91.7,56.4
|
|
||||||
c8,15.7,1.7,34.9-14,42.9c-15.7,8-35,1.8-43-13.9c-6.8-13.4-21-21.7-35.8-21.7c-21.8,0-40.2,17.6-40.2,39.2v92.3
|
|
||||||
C184.3,333.4,171.2,347.7,153.6,347.7z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -1,8 +1,9 @@
|
|||||||
# Data Source
|
# Data Source
|
||||||
|
|
||||||
After following the steps outlined in [Getting Started](./getting-started.md),
|
After following the steps outlined in
|
||||||
you'll notice that the OHIF Viewer has data for several studies and their
|
[Getting Started](./../development/getting-started.md), you'll notice that the
|
||||||
images. You didn't add this data, so where is it coming from?
|
OHIF Viewer has data for several studies and their images. You didn't add this
|
||||||
|
data, so where is it coming from?
|
||||||
|
|
||||||
By default, the viewer is configured to connect to a remote server hosted by the
|
By default, the viewer is configured to connect to a remote server hosted by the
|
||||||
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
|
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
|
||||||
@@ -84,7 +85,7 @@ yarn run dev:orthanc
|
|||||||
#### Configuration: Learn More
|
#### Configuration: Learn More
|
||||||
|
|
||||||
> For more configuration fun, check out the
|
> For more configuration fun, check out the
|
||||||
> [Essentials Configuration](./configuration.md) guide.
|
> [Essentials Configuration](./index.md) guide.
|
||||||
|
|
||||||
Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
|
Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
|
||||||
is running the `dev:orthanc` script in our project's `package.json`. That script
|
is running the `dev:orthanc` script in our project's `package.json`. That script
|
||||||
@@ -130,7 +131,7 @@ window.config = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
To learn more about how you can configure the OHIF Viewer, check out our
|
To learn more about how you can configure the OHIF Viewer, check out our
|
||||||
[Configuration Guide](./configuration.md).
|
[Configuration Guide](./index.md).
|
||||||
|
|
||||||
## Open Source DICOM Image Archives
|
## Open Source DICOM Image Archives
|
||||||
|
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
> This step assumes you have an imaging archive. If you need assistance setting
|
> This step assumes you have an imaging archive. If you need assistance setting
|
||||||
> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment
|
> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment
|
||||||
> recipe that contains an open source Image Archive
|
> recipe that contains an open Image Archive
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Configuration Files](#configuration-files)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [How do I configure my project?](#how-do-i-configure-my-project)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -16,8 +21,8 @@ and registered extension's features, are configured using this file.
|
|||||||
|
|
||||||
**Embedded Use Note:**
|
**Embedded Use Note:**
|
||||||
|
|
||||||
Alternatively, when using the `commonjs` bundle for embedded use cases, these
|
Alternatively, when using the `umd` bundle for embedded use cases, these same
|
||||||
same values are what you'll pass to `installViewer` method:
|
values are what you'll pass to `installViewer` method:
|
||||||
|
|
||||||
`OHIFStandaloneViewer.installViewer(window.config)`
|
`OHIFStandaloneViewer.installViewer(window.config)`
|
||||||
|
|
||||||
@@ -36,10 +41,6 @@ alternative data source (or even specify different default hotkeys).
|
|||||||
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
|
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
|
||||||
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
|
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
|
||||||
|
|
||||||
### Registering Extensions
|
|
||||||
|
|
||||||
> :warn: Instructions coming soon
|
|
||||||
|
|
||||||
## How do I configure my project?
|
## How do I configure my project?
|
||||||
|
|
||||||
The simplest way is to update the existing default config:
|
The simplest way is to update the existing default config:
|
||||||
@@ -65,6 +66,39 @@ window.config = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.config = ({ servicesManager } = {}) => {
|
||||||
|
const { UIDialogService } = servicesManager.services;
|
||||||
|
return {
|
||||||
|
cornerstoneExtensionConfig: {
|
||||||
|
tools: {
|
||||||
|
ArrowAnnotate: {
|
||||||
|
configuration: {
|
||||||
|
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routerBasename: '/',
|
||||||
|
servers: {
|
||||||
|
dicomWeb: [
|
||||||
|
{
|
||||||
|
name: 'DCM4CHEE',
|
||||||
|
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||||
|
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||||
|
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||||
|
qidoSupportsIncludeField: true,
|
||||||
|
imageRendering: 'wadors',
|
||||||
|
thumbnailRendering: 'wadors',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
You can also create a new config file and specify its path relative to the build
|
You can also create a new config file and specify its path relative to the build
|
||||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
||||||
value of this environment variable a few different ways:
|
value of this environment variable a few different ways:
|
||||||
@@ -10,9 +10,9 @@ your Viewer will display).
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Our goal is to make deployment as simple and painless as possible; however,
|
Our goal is to make deployment as simple and painless as possible; however,
|
||||||
there is an inherent amount of complexity in customizing, optimizing, and
|
there is an inherent amount of complexity in configuring and deploying web
|
||||||
deploying web applications. If you find yourself a little lost, please don't
|
applications. If you find yourself a little lost, please don't hesitate to
|
||||||
hesitate to [reach out for help](/help.md)
|
[reach out for help](/help.md)
|
||||||
|
|
||||||
## Deployment Scenarios
|
## Deployment Scenarios
|
||||||
|
|
||||||
@@ -35,8 +35,7 @@ benefits, but comes at the cost of time and complexity. Some benefits include:
|
|||||||
|
|
||||||
_Today:_
|
_Today:_
|
||||||
|
|
||||||
- Leverage [extensions](/advanced/extensions.md) to drop-in powerful new
|
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
|
||||||
features
|
|
||||||
- Add routes and customize the viewer's workflow
|
- Add routes and customize the viewer's workflow
|
||||||
- Finer control over styling and whitelabeling
|
- Finer control over styling and whitelabeling
|
||||||
|
|
||||||
@@ -97,12 +96,12 @@ support it yet, but it is gaining wider adoption.
|
|||||||
If you have an existing archive and intend to host the OHIF Viewer at the same
|
If you have an existing archive and intend to host the OHIF Viewer at the same
|
||||||
domain name as your archive, then connecting the two is as simple as following
|
domain name as your archive, then connecting the two is as simple as following
|
||||||
the steps layed out in our
|
the steps layed out in our
|
||||||
[Configuration Essentials Guide](./../essentials/configuration.md).
|
[Configuration Essentials Guide](./../configuring/index.md).
|
||||||
|
|
||||||
#### What if I don't have an imaging archive?
|
#### What if I don't have an imaging archive?
|
||||||
|
|
||||||
We provide some guidance on configuring a local image archive in our
|
We provide some guidance on configuring a local image archive in our
|
||||||
[Data Source Essentials](./../essentials/data-source.md) guide. Hosting an
|
[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an
|
||||||
archive remotely is a little trickier. You can check out some of our
|
archive remotely is a little trickier. You can check out some of our
|
||||||
[advanced recipes](#recipes) for modeled setups that may work for you.
|
[advanced recipes](#recipes) for modeled setups that may work for you.
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ The OHIF Viewer can be configured to work with authorization servers that
|
|||||||
support one or more of the OpenID-Connect authorization flows. The Viewer finds
|
support one or more of the OpenID-Connect authorization flows. The Viewer finds
|
||||||
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
|
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
|
||||||
values following the instructions laid out in the
|
values following the instructions laid out in the
|
||||||
[Configuration Essentials Guide](./../essentials/configuration.md).
|
[Configuration Essentials Guide](./../configuring/index.md).
|
||||||
|
|
||||||
_Example OpenID-Connect Settings:_
|
_Example OpenID-Connect Settings:_
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Build for Production
|
# Build for Production
|
||||||
|
|
||||||
> If you've already followed the
|
> If you've already followed the
|
||||||
> ["Getting Started" Guide](/essentials/getting-started.md), you can skip ahead
|
> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead
|
||||||
> to [Configuration](#configuration)
|
> to [Configuration](#configuration)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -48,7 +48,7 @@ If everything worked as expected, you should have a new `dist/` directory in the
|
|||||||
project's folder. It should roughly resemble the following:
|
project's folder. It should roughly resemble the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dist/
|
<root>platform/viewer/dist/
|
||||||
├── app-config.js
|
├── app-config.js
|
||||||
├── app.bundle.js
|
├── app.bundle.js
|
||||||
├── app.css
|
├── app.css
|
||||||
@@ -73,7 +73,7 @@ and registered extension's features, are configured using this file.
|
|||||||
|
|
||||||
The easiest way to apply your own configuration is to modify the `default.js`
|
The easiest way to apply your own configuration is to modify the `default.js`
|
||||||
file. For more advanced cofiguration options, check out our
|
file. For more advanced cofiguration options, check out our
|
||||||
[configuration essentials guide](/essentials/configuration.md).
|
[configuration essentials guide](/configuring/index.md).
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ include tags. Here's how it works:
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ol start="2">
|
<ol start="2">
|
||||||
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
|
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
|
||||||
example values that would allow the viewer to hit our public PACS:</li>
|
example values that would allow the viewer to hit our public PACS:</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Set before importing `ohif-viewer`
|
// Set before importing `ohif-viewer` (JS Object)
|
||||||
window.config = {
|
window.config = {
|
||||||
// default: '/'
|
// default: '/'
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
@@ -49,6 +49,9 @@ window.config = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To learn more about how you can configure the OHIF Viewer, check out our
|
||||||
|
[Configuration Guide](./index.md).
|
||||||
|
|
||||||
<ol start="3"><li>
|
<ol start="3"><li>
|
||||||
Render the viewer in the web page's target <code>div</code>
|
Render the viewer in the web page's target <code>div</code>
|
||||||
</li></ol>
|
</li></ol>
|
||||||
@@ -98,7 +101,7 @@ extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
|
|||||||
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
|
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
|
||||||
|
|
||||||
You can read more about extensions and how to create your own in our
|
You can read more about extensions and how to create your own in our
|
||||||
[extensions guide](/advanced/extensions.md).
|
[extensions guide](/extensions/index.md).
|
||||||
|
|
||||||
#### FAQ
|
#### FAQ
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Continous Integration (CI)
|
||||||
|
|
||||||
|
This repository uses `CircleCI` and `Netlify` for continous integration.
|
||||||
|
|
||||||
|
## Deploy Previews
|
||||||
|
|
||||||
|
[Netlify Deploy previews][deploy-previews] are generated for every pull request.
|
||||||
|
They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if
|
||||||
|
the changes had been merged.
|
||||||
|
|
||||||
|
Deploy previews can be configured by modifying the `netlify.toml` file in the
|
||||||
|
root of the repository. Some additional scripts/assets for netlify are included
|
||||||
|
in the root `.netlify` directory.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
[CircleCI Workflows][circleci-workflows] are a set of rules for defining a
|
||||||
|
collection of jobs and their run order. They are self-documenting and their
|
||||||
|
configuration can be found in our CircleCI configuration file:
|
||||||
|
`.circleci/config.yml`.
|
||||||
|
|
||||||
|
### Workflow: PR_CHECKS
|
||||||
|
|
||||||
|
The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and
|
||||||
|
end-to-end tests for every code check-in. These tests must all pass before code
|
||||||
|
can be merged to our `master` branch.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/WORKFLOW_PR_CHECKS.png">
|
||||||
|
<img src="/assets/img/WORKFLOW_PR_CHECKS.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Workflow diagram for PR_CHECKS</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
||||||
|
|
||||||
|
The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish
|
||||||
|
the pull request as a tagged docker image. This is helpful when changes need to
|
||||||
|
be tested with the Google Adapter before merging to `master`.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png">
|
||||||
|
<img src="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Workflow diagram for PR_WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> NOTE: This workflow will fail unless it's for a branch on our `upstream`
|
||||||
|
> repository. If you need this functionality, but the branch is from a fork,
|
||||||
|
> merge the changes to a short-lived `feature/` branch on `upstream`
|
||||||
|
|
||||||
|
### Workflow: DEPLOY
|
||||||
|
|
||||||
|
The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master.
|
||||||
|
It uses the Netlify CLI to deploy assets created as part of the repository's PWA
|
||||||
|
Build process (`yarn run build`). The workflow allows for "Manual Approval" to
|
||||||
|
promote the build to `STAGING` and `PRODUCTION` environments.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/WORKFLOW_DEPLOY.png">
|
||||||
|
<img src="/assets/img/WORKFLOW_DEPLOY.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
| Environment | Description | URL |
|
||||||
|
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||||
|
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
|
||||||
|
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
|
||||||
|
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
|
||||||
|
|
||||||
|
### Workflow: RELEASE
|
||||||
|
|
||||||
|
The RELEASE workflow publishes our `npm` packages, updated documentation, and
|
||||||
|
`docker` image when changes are merged to master. `Lerna` and "Semantic Commit
|
||||||
|
Syntax" are used to independently version and publish the many packages in our
|
||||||
|
monorepository. If a new version is cut/released, a Docker image is created.
|
||||||
|
Documentation is generated with `gitbook` and pushed to our `gh-pages` branch.
|
||||||
|
GitHub hosts the `gh-pages` branch with GitHub Pages.
|
||||||
|
|
||||||
|
- Platform Packages: https://github.com/ohif/viewers/#platform
|
||||||
|
- Extension Packages: https://github.com/ohif/viewers/#extensions
|
||||||
|
- Documentation: https://docs.ohif.org/
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/WORKFLOW_RELEASE.png">
|
||||||
|
<img src="/assets/img/WORKFLOW_RELEASE.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Workflow diagram for WORKFLOW_RELEASE</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### HOTFIX
|
||||||
|
|
||||||
|
_Not yet implemented_
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
|
||||||
|
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
|
||||||
|
[netlify-dev]: https://ohif-dev.netlify.com
|
||||||
|
[netlify-stage]: https://ohif-stage.netlify.com
|
||||||
|
[netlify-prod]: https://ohif-prod.netlify.com
|
||||||
|
[ohif-dev]: https://viewer-dev.ohif.org
|
||||||
|
[ohif-stage]: https://viewer-stage.ohif.org
|
||||||
|
[ohif-prod]: https://viewer-prod.ohif.org
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
|
|||||||
should be implemented, please don't hesitate to create a new issue. At a
|
should be implemented, please don't hesitate to create a new issue. At a
|
||||||
minimum, you may want to read the following documentation:
|
minimum, you may want to read the following documentation:
|
||||||
|
|
||||||
- [Essentials: Getting Started](./essentials/getting-started.md)
|
- [Getting Started](/development/getting-started.md)
|
||||||
- [Advanced: Architecture](./advanced/architecture.md)
|
- [Architecture](/architecture/index.md)
|
||||||
|
|
||||||
Pull requests that are:
|
Pull requests that are:
|
||||||
|
|
||||||
@@ -85,20 +85,12 @@ yarn run build
|
|||||||
yarn run build:package
|
yarn run build:package
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
Take a look at the sidebar. Depending on how you intend to use The OHIF Viewer,
|
|
||||||
different topics may be more pertinent than others. At a minimum, reading "The
|
|
||||||
Essentials" and the deployment recipes should be a good starting point.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- If you receive a _"No Studies Found"_ message and do not see your studies, try
|
- If you receive a _"No Studies Found"_ message and do not see your studies, try
|
||||||
changing the Study Date filters to a wider range.
|
changing the Study Date filters to a wider range.
|
||||||
- If you see a 'Loading' message which never resolves, check your browser
|
- If you see a 'Loading' message which never resolves, check your browser
|
||||||
JavaScript console inside the Developer Tools to identify any errors.
|
JavaScript console inside the Developer Tools to identify any errors.
|
||||||
- If you see any errors in your server console, check the
|
|
||||||
[Troubleshooting](./troubleshooting.md) page for more in depth advice.
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Links
|
Links
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Troubleshooting
|
|
||||||
|
|
||||||
Common GitHub issues that are not easily remedied with cleaner code or
|
|
||||||
documentation will be recorded here. Please feel free to make PRs to update this
|
|
||||||
page.
|
|
||||||
|
|
||||||
## Common Problems
|
|
||||||
|
|
||||||
| Problem | Most Common Reasons |
|
|
||||||
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| ** Can't retrieve Study List over DICOMWeb** | 1. QIDO root URL is incorrect<br> 2. DICOM Web is not enabled on PACS |
|
|
||||||
| ** Can't retrieve images** | 1. WADO Root URL is incorrect<br> 2. DICOM Web is not enabled on PACS<br> 3. HTTP Basic Authentication username and password are incorrect or not provided. |
|
|
||||||
|
|
||||||
## Debugging Steps
|
|
||||||
|
|
||||||
### Can't retrieve Study List over DICOMWeb
|
|
||||||
|
|
||||||
1. Check that you can query your PACS using an alternative DICOM Web client
|
|
||||||
(e.g. cURL, or a Web Browser). If you cannot, then your PACS is configured
|
|
||||||
incorrectly. Refer to the documentation of the image archive.
|
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# Extensions
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Concepts](#concepts)
|
||||||
|
- [Extension Skeleton](#extension-skeleton)
|
||||||
|
- [Registering an Extension](#registering-an-extension)
|
||||||
|
- [Lifecylce Hooks](#lifecycle-hooks)
|
||||||
|
- [Modules](#modules)
|
||||||
|
- [Contexts](#contexts)
|
||||||
|
- [Consuming Extensions](#consuming-extensions)
|
||||||
|
- [Extension Manager](#extensionmanager)
|
||||||
|
- [Maintained Extensions](#maintained-extensions)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
We use extensions to help us isolate and package groups of related features.
|
||||||
|
Extensions provide functionality, ui components, and new behaviors. Ideally,
|
||||||
|
they're built in a way that allows them to extend entirely different
|
||||||
|
implementations of the `@ohif/viewer` project.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/extensions-diagram.png">
|
||||||
|
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Diagram showing how extensions are configured and accessed.</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The `@ohif/viewer`'s application level configuration gives us the ability to add
|
||||||
|
and configure extensions. When the application starts, extensions are registered
|
||||||
|
with the `ExtensionManager`. Different portions of the `@ohif/viewer` project
|
||||||
|
will use registered extensions to influence application behavior.
|
||||||
|
|
||||||
|
Extensions allow us to:
|
||||||
|
|
||||||
|
- Wrap and integrate functionality of 3rd party dependencies in a reusable way
|
||||||
|
- Change how application data is mapped and transformed
|
||||||
|
- Display a consistent/cohesive UI
|
||||||
|
- Inject custom components to override built-in components
|
||||||
|
|
||||||
|
Practical examples of extensions include:
|
||||||
|
|
||||||
|
- A set of segmentation tools that build on top of the `cornerstone` viewport
|
||||||
|
- Showing ML/AI report summaries for the selected study/series/image
|
||||||
|
- Support for parsing DICOM structured reports and displaying them in a user
|
||||||
|
friendly way
|
||||||
|
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
### Extension Skeleton
|
||||||
|
|
||||||
|
An extension is a plain JavaScript object that has an `id` property, and one or
|
||||||
|
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
|
||||||
|
|
||||||
|
```js
|
||||||
|
// prettier-ignore
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id: 'example-extension',
|
||||||
|
|
||||||
|
// Lifecyle
|
||||||
|
preRegistration() { /* */ },
|
||||||
|
// Modules
|
||||||
|
getCommandsModule() { /* */ },
|
||||||
|
getToolbarModule() { /* */ },
|
||||||
|
getPanelModule() { /* */ },
|
||||||
|
getSopClassHandler() { /* */ },
|
||||||
|
getViewportModule() { /* */ },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering an Extension
|
||||||
|
|
||||||
|
There are two different ways to register and configure extensions: At
|
||||||
|
[runtime](#runtime-extensions) and at [build time](#bundled-extensions).
|
||||||
|
|
||||||
|
You can leverage one or both strategies. Which one(s) you choose depend on your
|
||||||
|
application's requirements. Each [module](#modules) defined by the extension
|
||||||
|
becomes available to the core application via the `ExtensionManager`.
|
||||||
|
|
||||||
|
#### Registering at Runtime
|
||||||
|
|
||||||
|
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||||
|
that file includes an `Extensions` key that supports an array of extensions to
|
||||||
|
register.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// prettier-ignore
|
||||||
|
const config = {
|
||||||
|
extensions: [
|
||||||
|
MyFirstExtension,
|
||||||
|
[
|
||||||
|
MySecondExtension,
|
||||||
|
{ /* MySecondExtensions Configuration */ },
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Registering at Build Time
|
||||||
|
|
||||||
|
The `@ohif/viewer` works best when built as a "Progressive Web Application"
|
||||||
|
(PWA). If you know the extensions your application will need, you can specify
|
||||||
|
them at "build time" to leverage advantages afforded to us by modern tooling:
|
||||||
|
|
||||||
|
- Code Splitting (dynamic imports)
|
||||||
|
- Tree Shaking
|
||||||
|
- Dependency deduplication
|
||||||
|
|
||||||
|
You can update the list of bundled extensions by:
|
||||||
|
|
||||||
|
1. Having your `@ohif/viewer` project depend on the extension
|
||||||
|
2. Importing and adding it to the list of extensions in the
|
||||||
|
`<repo-root>/platform/src/index.js` entrypoint.
|
||||||
|
|
||||||
|
### Lifecycle Hooks
|
||||||
|
|
||||||
|
Currently, there is only a single lifecycle hook for extensions:
|
||||||
|
[`preRegistration`](./lifecycle/pre-registration.md)
|
||||||
|
|
||||||
|
If an extension defines the [`preRegistration`](./lifecycle/pre-registration.md)
|
||||||
|
lifecycle hook, it is called before any modules are registered in the
|
||||||
|
`ExtensionManager`. It's most commonly used to wire up extensions to
|
||||||
|
[services](./../services/index.md) and [commands](./modules/commands.md), and to
|
||||||
|
bootstrap 3rd party libraries.
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
|
||||||
|
Modules are the meat of extensions. They provide "definitions", components, and
|
||||||
|
filtering/mapping logic that are then made available by various managers and
|
||||||
|
services.
|
||||||
|
|
||||||
|
Each module type has a special purpose, and is consumed by our viewer
|
||||||
|
differently.
|
||||||
|
|
||||||
|
| Type | Description | Examples |
|
||||||
|
| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| [Commands](./modules/commands.md) | Adds named commands, scoped to a context, to the CommandsManager | `setToolActive()`, `nextSeries()` |
|
||||||
|
| [Panel](./modules/panel.md) | Adds left or right hand side panels | `<ThumbnailList />`, `<MeasurementsTable />` |
|
||||||
|
| [SOPClassHandler](./modules/sop-class-handler.md) | Determines how retrieved study data is split into "DisplaySets" | `getDisplaySetFromSeries()` |
|
||||||
|
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
|
||||||
|
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
|
||||||
|
|
||||||
|
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
|
||||||
|
|
||||||
|
### Contexts
|
||||||
|
|
||||||
|
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
|
||||||
|
their functionality. Some example contexts being:
|
||||||
|
|
||||||
|
- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST`
|
||||||
|
- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK`
|
||||||
|
|
||||||
|
An extension module can use these to say "Only show this Toolbar Button if the
|
||||||
|
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
|
||||||
|
and behaviors depending on the current contexts.
|
||||||
|
|
||||||
|
For example, if we have hotkey that "rotates the active viewport", each Viewport
|
||||||
|
module that supports this behavior can add a command with the same name, scoped
|
||||||
|
to the appropriate context. When the `command` is fired, the "active contexts"
|
||||||
|
are used to determine the appropriate implementation of the rotate behavior.
|
||||||
|
|
||||||
|
## Consuming Extensions
|
||||||
|
|
||||||
|
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
|
||||||
|
project.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const extensionManager = new ExtensionManager({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
extensionManager.registerExtensions([ /** **/ ]);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
|
||||||
|
services, and is the home to the more bespoke application logic that doesn't
|
||||||
|
make as much sense to make reusable.
|
||||||
|
|
||||||
|
Long-term, replacing the `@ohif/viewer` application and consuming extensions
|
||||||
|
(and the `ExtensionManager`) in your own project is the ideal path for
|
||||||
|
applications requiring a high degree of customization that can't be achieved
|
||||||
|
with current theming, configuration, extension, and services support.
|
||||||
|
|
||||||
|
If you're not sure how to achieve your goals with the extensibility available
|
||||||
|
today, create a GitHub issue!
|
||||||
|
|
||||||
|
### `ExtensionManager`
|
||||||
|
|
||||||
|
The `ExtensionManager` is a class made available to us via the `@ohif/core`
|
||||||
|
project (platform/core). Our application instantiates a single instance of it,
|
||||||
|
and provides a `ServicesManager` and `CommandsManager`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const commandsManager = new CommandsManager();
|
||||||
|
const servicesManager = new ServicesManager();
|
||||||
|
const extensionManager = new ExtensionManager({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ExtensionManager` only has a few public members:
|
||||||
|
|
||||||
|
- `registerExtension` - Registers a single extension
|
||||||
|
- `registerExtensions` - Registers an array of extensions
|
||||||
|
- `modules` - An object containing registered extensions by `MODULE_TYPE`
|
||||||
|
|
||||||
|
During registration, lifecycle hooks and modules have access to
|
||||||
|
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
|
||||||
|
|
||||||
|
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
|
||||||
|
appropriate places in our application.
|
||||||
|
|
||||||
|
## Maintained Extensions
|
||||||
|
|
||||||
|
A small number of powerful extensions for popular use cases are maintained by
|
||||||
|
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
|
||||||
|
the top level [`extensions/`][ext-source] directory.
|
||||||
|
|
||||||
|
{% include "./_maintained-extensions-table.md" %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[viewers-repo]: https://github.com/OHIF/Viewers
|
||||||
|
[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions
|
||||||
|
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Lifecylce Hook: preRegistration
|
||||||
|
|
||||||
|
If an extension defines the `preRegistration` lifecycle hook, it is called
|
||||||
|
before any modules are registered in the `ExtensionManager`. This hook can be
|
||||||
|
used to:
|
||||||
|
|
||||||
|
- initialize 3rd party libraries
|
||||||
|
- register event listeners
|
||||||
|
- add or call services
|
||||||
|
- add or call commands
|
||||||
|
|
||||||
|
The `preRegistration` hook receives an object containing the
|
||||||
|
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
|
||||||
|
`configuration` that was provided with the extension at time of registration.
|
||||||
|
|
||||||
|
_Example `preRegistration` hook implementation_
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
id: 'MyExampleExtension',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {object} params.configuration
|
||||||
|
* @param {ServicesManager} params.servicesManager
|
||||||
|
* @param {CommandsManager} params.commandsManager
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
preRegistration({ servicesManager, commandsManager, configuration }) {
|
||||||
|
console.log('Wiring up important stuff.');
|
||||||
|
|
||||||
|
window.importantStuff = () => {
|
||||||
|
console.log(configuration);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Important stuff has been wired.');
|
||||||
|
window.importantStuff();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Module: Commands
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Command Definitions](#command-definitions)
|
||||||
|
- [Commands Manager](#commands-manager)
|
||||||
|
- [Instantiating](#instatiating)
|
||||||
|
- [Public API](#public-api)
|
||||||
|
- [Contexts](#contexts)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An extension can register a Commands Module by defining a `getCommandsModule`
|
||||||
|
method. The Commands Module allows us to register one or more commands scoped to
|
||||||
|
specific [contexts](./../index.md#contexts). Commands have several unique
|
||||||
|
characteristics that make them tremendously powerful:
|
||||||
|
|
||||||
|
- Multiple implementations for the same command can be defined
|
||||||
|
- Only the correct command's implementation will be run, dependent on the
|
||||||
|
application's "context"
|
||||||
|
- Commands can be called from extensions, modules, and the consuming application
|
||||||
|
|
||||||
|
Here is a simple example commands module:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
id: 'example-commands-module',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {ServicesManager} params.servicesManager
|
||||||
|
* @param {CommandsManager} params.commandsManager
|
||||||
|
*/
|
||||||
|
getCommandsModule({ servicesManager, commandsManager }) {
|
||||||
|
return {
|
||||||
|
definitions: {
|
||||||
|
sayHello: {
|
||||||
|
commandFn: ({ words }) => {
|
||||||
|
console.log(words);
|
||||||
|
},
|
||||||
|
options: { words: 'Hello!' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultContext: 'VIEWER',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Each definition returned by the Commands Module is registered to the
|
||||||
|
`ExtensionManager`'s `CommandsManager`.
|
||||||
|
|
||||||
|
## Command Definitions
|
||||||
|
|
||||||
|
The command definition consists of a named command (`myCommandName` below) and a
|
||||||
|
`commandFn`. The command name is used to call the command, and the `commandFn`
|
||||||
|
is the "command" that is actioned.
|
||||||
|
|
||||||
|
```js
|
||||||
|
myCommandName: {
|
||||||
|
commandFn: ({ viewports, other, options }) => { },
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { words: 'Just kidding! Goodbye!' },
|
||||||
|
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
|
||||||
|
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
|
||||||
|
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
|
||||||
|
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
|
||||||
|
|
||||||
|
## Command Behavior
|
||||||
|
|
||||||
|
**I have many similar commands. How can I share their `commandFn` and make it
|
||||||
|
reusable?**
|
||||||
|
|
||||||
|
This is where `storeContexts` and `options` come in. We use these in our
|
||||||
|
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
|
||||||
|
and `options` allow us to pass in the name of a tool we would like to set as
|
||||||
|
active.
|
||||||
|
|
||||||
|
**If there are multiple valid commands for the application's active contexts**
|
||||||
|
|
||||||
|
- What happens: all commands are run
|
||||||
|
- When to use: A `clearData` command that cleans up state for multiple
|
||||||
|
extensions
|
||||||
|
|
||||||
|
**If no commands are valid for the application's active contexts**
|
||||||
|
|
||||||
|
- What happens: a warning is printed to the console
|
||||||
|
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
|
||||||
|
current viewport (PDF or HTML)
|
||||||
|
|
||||||
|
## `CommandsManager`
|
||||||
|
|
||||||
|
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
|
||||||
|
instance of it should be defined in the consuming application, and it should be
|
||||||
|
used when constructing the `ExtensionManager`.
|
||||||
|
|
||||||
|
### Instantiating
|
||||||
|
|
||||||
|
When we instantiate the `CommandsManager`, we need to pass it two methods:
|
||||||
|
|
||||||
|
- `getAppState` - Should return the application's state when called
|
||||||
|
- `getActiveContexts` - Should return the application's active contexts when
|
||||||
|
called
|
||||||
|
|
||||||
|
These methods are used internally to help determine which commands are currently
|
||||||
|
valid, and how to provide them with any state they may need at the time they are
|
||||||
|
called.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const commandsManager = new CommandsManager({
|
||||||
|
getAppState,
|
||||||
|
getActiveContexts,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public API
|
||||||
|
|
||||||
|
If you would like to run a command in the consuming app or an extension, you can
|
||||||
|
use one of the following methods:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Returns all commands for a given context
|
||||||
|
commandsManager.getContext('string');
|
||||||
|
|
||||||
|
// Attempts to run a command
|
||||||
|
commandsManager.runCommand('speak', { command: 'hello' });
|
||||||
|
|
||||||
|
// Run command, but override the active contexts
|
||||||
|
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ExtensionManager` handles registering commands and creating contexts, so
|
||||||
|
most consumer's won't need these methods. If you find yourself using these, ask
|
||||||
|
yourself "why can't I register these commands via an extension?"
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Used by the `ExtensionManager` to register new commands
|
||||||
|
commandsManager.registerCommand('context', 'name', commandDefinition);
|
||||||
|
|
||||||
|
// Creates a new context; clears the context if it already exists
|
||||||
|
commandsManager.createContext('string');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contexts
|
||||||
|
|
||||||
|
It is up to the consuming application to define what contexts are possible, and
|
||||||
|
which ones are currently active. As extensions depend heavily on these, we will
|
||||||
|
likely publish guidance around creating contexts, and ways to override extension
|
||||||
|
defined contexts in the near future. If you would like to discuss potential
|
||||||
|
changes to how contexts work, please don't hesistate to createa new GitHub
|
||||||
|
issue.
|
||||||
|
|
||||||
|
[Some additional information on Contexts can be found here.](./../index.md#contexts)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Module: Panel
|
||||||
|
|
||||||
|
An extension can register a Panel Module by defining a `getPanelModule` method.
|
||||||
|
The panel module provides the ability to define `menuOptions` and `components`
|
||||||
|
that can be used by the consuming application. `components` are React Components
|
||||||
|
that can be displayed in the consuming application's "Panel" Component.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>A panel extension example</i></center>
|
||||||
|
|
||||||
|
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
|
||||||
|
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
|
||||||
|
optional provide it's own `context` value.
|
||||||
|
|
||||||
|
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
||||||
|
associated `ServicesManager` and `CommandsManager`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import MyComponent from './MyComponent.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: 'example-panel-module',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {ServicesManager} params.servicesManager
|
||||||
|
* @param {CommandsManager} params.commandsManager
|
||||||
|
*/
|
||||||
|
getPanelModule({ servicesManager, commandsManager }) {
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
// A suggested icon
|
||||||
|
// Available icons determined by consuming app
|
||||||
|
icon: 'list',
|
||||||
|
// A suggested label
|
||||||
|
label: 'Magic',
|
||||||
|
// 'right' or 'left'
|
||||||
|
from: 'right',
|
||||||
|
// The target component to toggle open/close
|
||||||
|
target: 'target-component-id',
|
||||||
|
// Overrides `defaultContext`, if specified
|
||||||
|
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'target-component-id',
|
||||||
|
component: MyComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['ROUTE:VIEWER'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Module: SOP Class Handler
|
||||||
|
|
||||||
|
An extension can register a SOP Class Handler Module by defining a
|
||||||
|
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
|
||||||
|
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
|
||||||
|
own components. It instead defines:
|
||||||
|
|
||||||
|
- `sopClassUids`: an array of string SOP Class UIDs that the
|
||||||
|
`getDisplaySetFromSeries` method should be applied to.
|
||||||
|
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||||
|
display set
|
||||||
|
|
||||||
|
A `displaySet` has the following shape:
|
||||||
|
|
||||||
|
```js
|
||||||
|
return {
|
||||||
|
plugin: 'html',
|
||||||
|
modality: 'SR',
|
||||||
|
displaySetInstanceUid: 0,
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(),
|
||||||
|
authorizationHeaders,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Where the `plugin` key is used to influence the default `ViewportComponent` for
|
||||||
|
rendering the `displaySet`. Additional properties are passed to the
|
||||||
|
`ViewportComponent` and used by the default `StudyBrowser` to render
|
||||||
|
"thumbnails" for each `displaySet`
|
||||||
|
|
||||||
|
## Example SOP Class Handler Module
|
||||||
|
|
||||||
|
```js
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||||
|
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: 'example-sop-class-handler-module',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {ServicesManager} params.servicesManager
|
||||||
|
* @param {CommandsManager} params.commandsManager
|
||||||
|
*/
|
||||||
|
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||||
|
return {
|
||||||
|
id: 'OHIFDicomHtmlSopClassHandler',
|
||||||
|
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} series -
|
||||||
|
* @param {object} study -
|
||||||
|
* @param {object} dicomWebClient -
|
||||||
|
* @param {object} authorizationHeaders -
|
||||||
|
*/
|
||||||
|
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugin: 'html',
|
||||||
|
displaySetInstanceUid: 0,
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(),
|
||||||
|
authorizationHeaders,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## `@ohif/viewer` usage
|
||||||
|
|
||||||
|
We use the `sopClassHandlerModule`s in three different places:
|
||||||
|
|
||||||
|
- `ViewerLocalFileData.js`
|
||||||
|
- `ViewerRetrieveStudyData.js`
|
||||||
|
- `StandaloneRouting.js`
|
||||||
|
|
||||||
|
Each time, it is used to map study and series data to `displaySets`. It does
|
||||||
|
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
|
||||||
|
manager has the method `createDisplaySets` that takes an array of
|
||||||
|
`sopClassHandlerModules`.
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Module: Toolbar
|
||||||
|
|
||||||
|
An extension can register a Toolbar Module by defining a `getToolbarModule`
|
||||||
|
method. This module is commonly used to define:
|
||||||
|
|
||||||
|
- [Toolbar buttons](#button-definitions)
|
||||||
|
- [Nested toolbar menus](#nested-toolbar-menus)
|
||||||
|
- [Custom components](#custom-components)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
|
||||||
|
|
||||||
|
## Example Toolbar Module
|
||||||
|
|
||||||
|
The Toolbar Module should return an array of `definitions` and a
|
||||||
|
`defaultContext`. There are currently a few different variations of definitions,
|
||||||
|
each one is detailed further down.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
id: 'example-toolbar-module',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {ServicesManager} params.servicesManager
|
||||||
|
* @param {CommandsManager} params.commandsManager
|
||||||
|
*/
|
||||||
|
getToolbarModule({ servicesManager, commandsManager }) {
|
||||||
|
return {
|
||||||
|
definitions: [
|
||||||
|
/* Array of definitions */
|
||||||
|
],
|
||||||
|
defaultContext: ['ROUTE:VIEWER'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Button Definitions
|
||||||
|
|
||||||
|
The simplest definition has the following properties:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'StackScroll',
|
||||||
|
label: 'Stack Scroll',
|
||||||
|
icon: 'bars',
|
||||||
|
type: 'setToolActive',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'StackScroll' },
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
| property | description | values |
|
||||||
|
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
||||||
|
| `id` | Unique string identifier for the definition | \* |
|
||||||
|
| `label` | User/display friendly to show in UI | \* |
|
||||||
|
| `icon` | A string name for an icon supported by the consuming application. | \* |
|
||||||
|
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
|
||||||
|
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
|
||||||
|
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
|
||||||
|
| `context` | (optional) Overrides module's `defaultContext` | Array of string context names |
|
||||||
|
|
||||||
|
Where a button with a `type` of `setToolActive` has an "active" styling applied
|
||||||
|
when clicked; removing the active styling from all other buttons.
|
||||||
|
|
||||||
|
## Nested Toolbar Menus
|
||||||
|
|
||||||
|
You can indicate that buttons should be grouped and nested in a submenu by
|
||||||
|
including `buttons` property in a definition:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'More',
|
||||||
|
label: 'More',
|
||||||
|
icon: 'ellipse-circle',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 'cstInvert',
|
||||||
|
label: 'Invert',
|
||||||
|
icon: 'circle',
|
||||||
|
type: 'command',
|
||||||
|
commandName: 'invertViewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>Example toolbar button demonstrating nested buttons.</i></center>
|
||||||
|
|
||||||
|
## Custom Components
|
||||||
|
|
||||||
|
The Toolbar Modules supports rendering custom components in place of the
|
||||||
|
application's default. In place of the `type`, `commandName`, and
|
||||||
|
`commandOptions` properties, we instead specify a `CustomComponent`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'Custom',
|
||||||
|
label: 'Custom',
|
||||||
|
icon: 'custom-icon',
|
||||||
|
CustomComponent: CustomToolbarComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The `CustomComponent` components will receive the following props:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<CustomComponent
|
||||||
|
parentContext="{parentContext}"
|
||||||
|
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
|
||||||
|
button="{button}"
|
||||||
|
key="{button.id}"
|
||||||
|
activeButtons="{activeButtonsIds}"
|
||||||
|
isActive="{isActive}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| ---------------------- | -------- | ------------------------------- |
|
||||||
|
| `activeButtons` | string[] | list of active buttons |
|
||||||
|
| `button` | object | its own definition object |
|
||||||
|
| `key` | string | React key prop |
|
||||||
|
| `isActive` | boolean | If current button is active |
|
||||||
|
| `parentContext` | ? | The parent component's context? |
|
||||||
|
| `toolbarClickCallback` | func | Callback method for clicks |
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Module: Viewport
|
||||||
|
|
||||||
|
An extension can register a Viewport Module by defining a `getViewportModule`
|
||||||
|
method that returns a React component. Currently, we use viewport components to
|
||||||
|
add support for:
|
||||||
|
|
||||||
|
- 2D Medical Image Viewing (cornerstone ext.)
|
||||||
|
- Structured Reports as HTML (dicom html ext.)
|
||||||
|
- Encapsulated PDFs as PDFs (dicom pdf ext.)
|
||||||
|
- Whole Slide Microscopy Viewing (whole slide ext.)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
|
||||||
|
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
|
||||||
|
Component receives props containing a display set it should know how to render.
|
||||||
|
|
||||||
|
## Viewport Component Props
|
||||||
|
|
||||||
|
Each `ViewportComponent` will receive the following props:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<viewportComponent
|
||||||
|
viewportData="{viewportData}"
|
||||||
|
viewportIndex="{viewportIndex}"
|
||||||
|
children="{[children]}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --------------- | --------------- | --------------------------------- |
|
||||||
|
| `children` | React.element[] | |
|
||||||
|
| `viewportData` | object | `viewportSpecificData` (probably) |
|
||||||
|
| `viewportIndex` | number | |
|
||||||
|
|
||||||
|
### `@ohif/viewer`
|
||||||
|
|
||||||
|
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||||
|
component is used depends on:
|
||||||
|
|
||||||
|
- The Layout Configuration
|
||||||
|
- Registered SopClassHandlers
|
||||||
|
- The SopClassUID for visible/selected datasets
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>An example of three cornerstone Viewports</i></center>
|
||||||
@@ -19,10 +19,10 @@ Follow the steps outlined in the [Bug Report Template][bug-report-template].
|
|||||||
At the moment we are in the process of defining our roadmap and will do our best
|
At the moment we are in the process of defining our roadmap and will do our best
|
||||||
to communicate this to the community. If your requested feature is on the
|
to communicate this to the community. If your requested feature is on the
|
||||||
roadmap, then it will most likely be built at some point. If it is not, you are
|
roadmap, then it will most likely be built at some point. If it is not, you are
|
||||||
welcome to build it yourself and [contribute it](contributing/index.md). If you
|
welcome to build it yourself and [contribute it](development/contributing.md).
|
||||||
have resources and would like to fund the development of a feature, please
|
If you have resources and would like to fund the development of a feature,
|
||||||
[contact us](http://www.ohif.org) or work with community members that offer
|
please [contact us](http://www.ohif.org) or work with community members that
|
||||||
[consulting services][commercial-support].
|
offer [consulting services][commercial-support].
|
||||||
|
|
||||||
### Who should I contact about Academic Collaborations?
|
### Who should I contact about Academic Collaborations?
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Installation
|
# PWA vs Packaged
|
||||||
|
|
||||||
It's important to know that the OHIF Viewer project provides two different build
|
It's important to know that the OHIF Viewer project provides two different build
|
||||||
processes:
|
processes:
|
||||||
@@ -13,7 +13,7 @@ like user preferences and previous query paramaters. Because of this, the Viewer
|
|||||||
has been built to be highly configurable to work with almost any web accessible
|
has been built to be highly configurable to work with almost any web accessible
|
||||||
data source.
|
data source.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
|
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
|
||||||
These can be delivered to your end users however you would like:
|
These can be delivered to your end users however you would like:
|
||||||
@@ -36,7 +36,7 @@ many data sources. The OHIF Viewer's scope **DOES** include configuration and
|
|||||||
support for services that are protected with OpenID-Connect.
|
support for services that are protected with OpenID-Connect.
|
||||||
|
|
||||||
In an effort to aide our users and contributors, we attempt to provide several
|
In an effort to aide our users and contributors, we attempt to provide several
|
||||||
[deployment and hosting recipes](./../deployment/index.md) as potential starting
|
[deployment and hosting recipes](./deployment/index.md) as potential starting
|
||||||
points. These are not meant to be rock solid, production ready, solutions; like
|
points. These are not meant to be rock solid, production ready, solutions; like
|
||||||
most recipes, they should be augmented to best fit you and your organization's
|
most recipes, they should be augmented to best fit you and your organization's
|
||||||
taste, preferences, etc.
|
taste, preferences, etc.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Our Process
|
||||||
|
|
||||||
|
Our process is a living, breathing thing. We strive to have regular
|
||||||
|
[retrospectives][retrospective] that help us shape and adapt our process to our
|
||||||
|
team's current needs. This document attempts to capture the broad strokes of
|
||||||
|
that process in an effort to:
|
||||||
|
|
||||||
|
- Strengthen community member involvement and understanding
|
||||||
|
- Welcome feedback and helpful suggestions
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
- [Issue Triage](#issue-triage)
|
||||||
|
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
|
||||||
|
- [Contributions (Pull Requests)](#contributions-pull-requests)
|
||||||
|
- [Releases](#releases)
|
||||||
|
|
||||||
|
_Include issue lifecycle diagram_
|
||||||
|
|
||||||
|
## Issue Triage
|
||||||
|
|
||||||
|
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions,
|
||||||
|
and suggest changes to the OHIF Viewer's core team. Community issues generally
|
||||||
|
fall into one of three categories, and are marked with a `triage` label when
|
||||||
|
created.
|
||||||
|
|
||||||
|
| Issue Template Name | Description |
|
||||||
|
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
||||||
|
| Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|
||||||
|
| Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|
||||||
|
| Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
|
||||||
|
|
||||||
|
_table 1. issue template names and descriptions_
|
||||||
|
|
||||||
|
Issues that require `triage` are akin to support tickets. As this is often our
|
||||||
|
first contact with would-be adopters and contributors, it's important that we
|
||||||
|
strive for timely responses and satisfactory resolutions. We attempt to
|
||||||
|
accomplish this by:
|
||||||
|
|
||||||
|
1. Responding to issues requiring `triage` at least once a week
|
||||||
|
2. Create new "official issues" from "community issues"
|
||||||
|
3. Provide clear guidance and next steps (when applicable)
|
||||||
|
4. Regularly clean up old (stale) issues
|
||||||
|
|
||||||
|
> :pencil: Less obviously, patterns in the issues being reported can highlight
|
||||||
|
> areas that need improvement. For example, users often have difficulty
|
||||||
|
> navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce
|
||||||
|
> our ticket volume for this issue?
|
||||||
|
|
||||||
|
### Backlogged Issues
|
||||||
|
|
||||||
|
Community issues serve as vehicles of discussion that lead us to "backlogged
|
||||||
|
issues". Backlogged issues are the distilled and actionable information
|
||||||
|
extracted from community issues. They contain the scope and requirements
|
||||||
|
necessary for hand-off to a core-team (or community) contributor ^\_^
|
||||||
|
|
||||||
|
| Category | Description | Labels |
|
||||||
|
| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
|
||||||
|
| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
|
||||||
|
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
|
||||||
|
|
||||||
|
_table 2. backlogged issue types ([full list of labels][gh-labels])_
|
||||||
|
|
||||||
|
## Issue Curation (["backlog grooming"][groom-backlog])
|
||||||
|
|
||||||
|
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on
|
||||||
|
our backlog. If an issue is on our backlog, it means we are, at the very least,
|
||||||
|
committed to reviewing any community drafted Pull Requests to complete the
|
||||||
|
issue. If you're interested in seeing an issue completed but don't know where to
|
||||||
|
start, please don't hesitate to leave a comment!
|
||||||
|
|
||||||
|
While we don't yet have a long-term or quarterly road map, we do regularly add
|
||||||
|
items to our ["Active Development" GitHub Project Board][gh-board]. Items on
|
||||||
|
this project board are either in active development by Core Team members, or
|
||||||
|
queued up for development as in-progress items are completed.
|
||||||
|
|
||||||
|
> :pencil: Want to contribute but not sure where to start? Check out [Up for
|
||||||
|
> grabs][label-grabs] issues and our [Contributing
|
||||||
|
> documentation][contributing-docs]
|
||||||
|
|
||||||
|
## Contributions (Pull Requests)
|
||||||
|
|
||||||
|
Incoming Pull Requests (PRs) are triaged using the following labels. Code review
|
||||||
|
is performed on all PRs where the bug fix or added functionality is deemed
|
||||||
|
appropriate:
|
||||||
|
|
||||||
|
| Labels | Description |
|
||||||
|
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **Classification** | |
|
||||||
|
| [PR: Bug Fix][label-bug] | Filed to address a Bug. |
|
||||||
|
| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. |
|
||||||
|
| **Review Workflow** | |
|
||||||
|
| [PR: Awaiting Response 💬][awaiting-response] | The core team is waiting for additional information from the author. |
|
||||||
|
| [PR: Awaiting Review 👀][awaiting-review] | The core team has not yet performed a code review. |
|
||||||
|
| [PR: Awaiting Revisions 🖊][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. |
|
||||||
|
| **QA** | |
|
||||||
|
| [PR: Awaiting User Cases 💃][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start |
|
||||||
|
| [PR: No UX Impact 🙃][no-ux-impact] | The PR code changes do not impact the user's experience |
|
||||||
|
|
||||||
|
We rely on GitHub Checks and integrations with third party services to evaluate
|
||||||
|
changes in code quality and test coverage. Tests must pass and User cases must
|
||||||
|
be present (when applicable) before a PR can be merged to master, and code
|
||||||
|
quality and test coverage must not changed by a significant margin. For some
|
||||||
|
repositories, visual screenshot-based tests are also included, and video
|
||||||
|
recordings of end-to-end tests are stored for later review.
|
||||||
|
|
||||||
|
[You can read more about our continous integration efforts here](/development/continous-integration.md)
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Releases are made automatically based on the type of commits which have been
|
||||||
|
merged (major.minor.patch). Releases are automatically pushed to NPM. Release
|
||||||
|
notes are automatically generated. Users can subscribe to GitHub and NPM
|
||||||
|
releases.
|
||||||
|
|
||||||
|
We host development, staging, and production environments for the Progressive
|
||||||
|
Web Application version of the OHIF Viewer. [Development][ohif-dev] always
|
||||||
|
reflects the latest changes on our master branch. [Staging][ohif-stage] is used
|
||||||
|
to regression test a release before a bi-weekly deploy to our [Production
|
||||||
|
environment][ohif-prod].
|
||||||
|
|
||||||
|
Important announcements are made on GitHub, tagged as Announcement, and pinned
|
||||||
|
so that they remain at the top of the Issue page.
|
||||||
|
|
||||||
|
The Core team occasionally performs full manual testing to begin the process of
|
||||||
|
releasing a Stable version. Once testing is complete, the known issues are
|
||||||
|
addressed and a Stable version is released.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
|
||||||
|
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
|
||||||
|
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
|
||||||
|
[gh-labels]: https://github.com/OHIF/Viewers/labels
|
||||||
|
<!-- Issue Labels -->
|
||||||
|
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
|
||||||
|
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
|
||||||
|
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
|
||||||
|
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
|
||||||
|
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
|
||||||
|
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
|
||||||
|
<!-- PR Labels -->
|
||||||
|
[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft
|
||||||
|
[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A
|
||||||
|
[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A
|
||||||
|
[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A
|
||||||
|
[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A
|
||||||
|
[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A
|
||||||
|
<!-- -->
|
||||||
|
[ohif-dev]: https://viewer-dev.ohif.org
|
||||||
|
[ohif-stage]: https://viewer-stage.ohif.org
|
||||||
|
[ohif-prod]: https://viewer.ohif.org
|
||||||
|
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
||||||
|
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
||||||
|
[contributing-docs]: https://docs.ohif.org/contributing/
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This page details how to get an instance of the OHIF Viewer up and running as
|
||||||
|
fast as possible. It shows how to grab a pre-built version of the application,
|
||||||
|
point it at your data source (PACS), and plop it on a web server.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### 1. Pre-built PWA
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
### 2. Script-Tag
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
### 3. Docker
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## Security Concerns
|
||||||
|
|
||||||
|
- Secure your data
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
- Missing server rewrite rules
|
||||||
|
- CORS issues when requesting data from PACS
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Services (default)
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Services are a work in progress. As we are still in the progress of creating a
|
||||||
|
non-ui maintained service, this usage may change.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/services.png">
|
||||||
|
<img src="/assets/img/services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The simplest service return a new object that has a `name` property, and
|
||||||
|
methods/properties that give the service its functionality. The "Factory
|
||||||
|
Function" that creates the service is provided with the implementation (this is
|
||||||
|
slightly different for UI Services).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const _speak = () => {
|
||||||
|
console.warn('Speak is not implemented');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create `HelloWorldService`
|
||||||
|
*
|
||||||
|
* @param {object} implementation
|
||||||
|
* @param {function} implementation.speak - Speak's implementation
|
||||||
|
* @returns HelloWorldService
|
||||||
|
*/
|
||||||
|
export default function createHelloWorldService({ speak }) {
|
||||||
|
return {
|
||||||
|
name: 'HelloWorldService',
|
||||||
|
speak: speak || _speak,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A service, once created, can be registered with the `ServicesManager` to make it
|
||||||
|
accessible to extensions. Similarly, the application code can access named
|
||||||
|
services from the `ServicesManager`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// In the application
|
||||||
|
const speak = () => {
|
||||||
|
window.alert('HELLO WORLD');
|
||||||
|
};
|
||||||
|
const HelloWorldService = createHelloWorldService({ speak });
|
||||||
|
const servicesManager = new ServicesManager();
|
||||||
|
|
||||||
|
servicesManager.registerService(HelloWorldService);
|
||||||
|
|
||||||
|
// In an extension
|
||||||
|
const { HelloWorldService } = servicesManager.services;
|
||||||
|
|
||||||
|
if (HelloWorldService) {
|
||||||
|
HelloWorldService.speak();
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Measurements Package (ohif-measurements)
|
# Service: Measurements
|
||||||
|
|
||||||
## Package design
|
...
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Services Overview
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Kinds of Services](#kinds-of-services)
|
||||||
|
- [Services (default)](#services-default)
|
||||||
|
- [UI Services](#ui-services)
|
||||||
|
- [Related Patterns](#related-patterns)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Services are a beefier version of [commands][commands]. They provide a set of
|
||||||
|
operations, often tied to some shared state, and are made available to
|
||||||
|
extensions via the `ServicesManager`. Services are particularly well suited to
|
||||||
|
address [cross-cutting concerns][cross-cutting-concerns].
|
||||||
|
|
||||||
|
Each service should be:
|
||||||
|
|
||||||
|
- self-contained
|
||||||
|
- able to fail and/or be removed without breaking the application
|
||||||
|
- completely interchangeable with another module implementing the same interface
|
||||||
|
|
||||||
|
## Kinds of Services
|
||||||
|
|
||||||
|
Depending on the kind of service, we follow slightly different conventions. For
|
||||||
|
example, a UI service often receives its implementation from a React Context
|
||||||
|
Provider. You can read more about the different kinds of services and what makes
|
||||||
|
them different below:
|
||||||
|
|
||||||
|
### Services (default)
|
||||||
|
|
||||||
|
Services are a work in progress. As we are still in the progress of creating a
|
||||||
|
non-ui maintained service, this usage may change.
|
||||||
|
|
||||||
|
[You can read more about default services: here](./default/index.md)
|
||||||
|
|
||||||
|
### UI Services
|
||||||
|
|
||||||
|
A typical web application will have components and state for common UI like
|
||||||
|
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
||||||
|
these components from an extension.
|
||||||
|
|
||||||
|
We maintain the following UI Services:
|
||||||
|
|
||||||
|
- [UIDialogService](./ui/ui-dialog-service.md)
|
||||||
|
- [UIModalService](./ui/ui-modal-service.md)
|
||||||
|
- [UINotificationService](./ui/ui-notification-service.md)
|
||||||
|
|
||||||
|
You can read more about a specific service by selecting it in the above list,
|
||||||
|
and more about [UI services in general: here](./ui/index.md)
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
|
||||||
|
Services are "concern-specific" code modules that can be consumed across layers.
|
||||||
|
We try to minimize the coupling they introduce by authoring services that are
|
||||||
|
able to fail or be removed. Related patterns that may reduce coupling include:
|
||||||
|
|
||||||
|
- Pub/Sub
|
||||||
|
- Commands
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[commands]: #/
|
||||||
|
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
|
||||||
|
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
|
||||||
|
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# UI Services
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Tips & Tricks](#/)
|
||||||
|
- [Maintained Services](#/)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A typical web application will have components and state for common UI like
|
||||||
|
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
||||||
|
these components from an extension.
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/ui-services.png">
|
||||||
|
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
In `@ohif/core`, we have a collection of service factories. We select one we
|
||||||
|
would like our application to support, create an instance of it, and pass that
|
||||||
|
instance to our `ServicesManager` AND to a React component (in this example,
|
||||||
|
`ModalContext`'s provider).
|
||||||
|
|
||||||
|
The `ModalContext`'s provider:
|
||||||
|
|
||||||
|
- Exposes context values
|
||||||
|
- Exposes methods that leverage `useCallback` hooks
|
||||||
|
- Sets the service's implementation in a `useEffect` hook
|
||||||
|
|
||||||
|
The `ServicesManager` is:
|
||||||
|
|
||||||
|
- Passed to the `ExtensionManager`
|
||||||
|
- The `ExtensionManager` makes the `ServicesManager` available to:
|
||||||
|
- All of it's lifecycle hooks (`preRegistration`)
|
||||||
|
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.)
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
That's all fine and good, but it's still a little too abstract. What does this
|
||||||
|
translate to in practice?
|
||||||
|
|
||||||
|
```js
|
||||||
|
// In the application
|
||||||
|
import UINotificationService from '@ohif/core';
|
||||||
|
const servicesManager = new ServicesManager();
|
||||||
|
|
||||||
|
servicesManager.registerService(UINotificationService);
|
||||||
|
|
||||||
|
// UI Service Provider
|
||||||
|
useEffect(() => {
|
||||||
|
if (service) {
|
||||||
|
service.setServiceImplementation({ hide, show });
|
||||||
|
}
|
||||||
|
}, [service, hide, show]);
|
||||||
|
|
||||||
|
// In an extension
|
||||||
|
const { UINotificationService } = servicesManager.services;
|
||||||
|
|
||||||
|
if (UINotificationService) {
|
||||||
|
UINotificationService.show('Hello from the other side 👋');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/notification-example.gif">
|
||||||
|
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Tips & Tricks
|
||||||
|
|
||||||
|
It's important to remember that all we're doing is making it possible to control
|
||||||
|
bits of the application's UI from an extension. Here are a few non-obvious
|
||||||
|
takeaways worth mentioning:
|
||||||
|
|
||||||
|
- Your application code should continue to use React context
|
||||||
|
(consumers/providers) as it normally would
|
||||||
|
- You can substitute our "out of the box" UI implementations with your own
|
||||||
|
- You can create and register your own UI services
|
||||||
|
- You can choose not to register a service or provide a service implementation
|
||||||
|
- In extensions, you can provide fallback/alternative behavior if an expected
|
||||||
|
service is not registered
|
||||||
|
- No `UIModalService`? Use the `UINotificationService` to notify users.
|
||||||
|
- While we don't have an examples of this, you can technically register a
|
||||||
|
service in an extension and expose it to the core application
|
||||||
|
|
||||||
|
> Note: These are recommended patterns, not hard and fast rules. Following them
|
||||||
|
> will help reduce confusion and interoperability with the larger OHIF
|
||||||
|
> community, but they're not silver bullets. Please speak up, create an issue,
|
||||||
|
> if you would like to discuss new services or improvements to this pattern.
|
||||||
|
|
||||||
|
## Maintained Services
|
||||||
|
|
||||||
|
Our `@ohif/viewer` project is an example of how to glue together the different
|
||||||
|
parts and pieces of the OHIF Platform to create a polished and powerful product.
|
||||||
|
To accomplish that, we maintain several UI Services that you can use in your own
|
||||||
|
project, or provide alternative implementations for:
|
||||||
|
|
||||||
|
| Name | Docs |
|
||||||
|
| --------------------- | ------------------------------------ |
|
||||||
|
| UIDialogService | [Here](./ui-dialog-service.md) |
|
||||||
|
| UIModalService | [Here](./ui-modal-service.md) |
|
||||||
|
| UINotificationService | [Here](./ui-notification-service.md) |
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# UI Dialog Service
|
||||||
|
|
||||||
|
Dialogs have similar characteristics to that of Modals, but often with a
|
||||||
|
streamlined focus. They can be helpful when:
|
||||||
|
|
||||||
|
- We need to grab the user's attention
|
||||||
|
- We need user input
|
||||||
|
- We need to show additional information
|
||||||
|
|
||||||
|
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
||||||
|
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/dialog-example.gif">
|
||||||
|
<img src="/assets/img/dialog-example.gif" alt="UI Dialog Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>GIF showing successful call of UIDialogService from an extension.</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
For a more detailed look on the options and return values each of these methods
|
||||||
|
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||||
|
|
||||||
|
| API Member | Description |
|
||||||
|
| -------------- | ------------------------------------------------------ |
|
||||||
|
| `create()` | Creates a new Dialog that is displayed until dismissed |
|
||||||
|
| `dismiss()` | Dismisses the specified dialog |
|
||||||
|
| `dismissAll()` | Dismisses all dialogs |
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
| Implementation | Consumer |
|
||||||
|
| ------------------------------------ | -------------------------- |
|
||||||
|
| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider |
|
||||||
|
|
||||||
|
`*` - Denotes maintained by OHIF
|
||||||
|
|
||||||
|
> 3rd Party implementers may be added to this table via pull requests.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js
|
||||||
|
[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js
|
||||||
|
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# UI Modal Service
|
||||||
|
|
||||||
|
Modals have similar characteristics to that of Dialogs, but are often larger,
|
||||||
|
and only allow for a single instance to be viewable at once. They also tend to
|
||||||
|
be centered, and not draggable. They're commonly used when:
|
||||||
|
|
||||||
|
- We need to grab the user's attention
|
||||||
|
- We need user input
|
||||||
|
- We need to show additional information
|
||||||
|
|
||||||
|
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
||||||
|
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/modal-example.gif">
|
||||||
|
<img src="/assets/img/modal-example.gif" alt="UI Modal Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>GIF showing successful call of UIModalService from an extension.</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
For a more detailed look on the options and return values each of these methods
|
||||||
|
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||||
|
|
||||||
|
| API Member | Description |
|
||||||
|
| ---------- | ------------------------------------- |
|
||||||
|
| `hide()` | Hides the open modal |
|
||||||
|
| `show()` | Shows the provided content in a modal |
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
| Implementation | Consumer |
|
||||||
|
| ---------------------------------- | ----------------------------- |
|
||||||
|
| [Modal Provider][modal-provider]\* | [OHIFModal][modal-consumer]\* |
|
||||||
|
|
||||||
|
`*` - Denotes maintained by OHIF
|
||||||
|
|
||||||
|
> 3rd Party implementers may be added to this table via pull requests.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
|
||||||
|
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
|
||||||
|
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
|
||||||
|
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# UI Notification Service
|
||||||
|
|
||||||
|
Notifications can be annoying and disruptive. They can also deliver timely
|
||||||
|
helpful information, or expedite the user's workflow. Here is some high level
|
||||||
|
guidance on when and how to use them:
|
||||||
|
|
||||||
|
- Notifications should be non-interfering (timely, relevant, important)
|
||||||
|
- We should only show small/brief notifications
|
||||||
|
- Notifications should be contextual to current behavior/actions
|
||||||
|
- Notifications can serve warnings (acting as a confirmation)
|
||||||
|
|
||||||
|
If you're curious about the DOs and DON'Ts of notifications, check out this
|
||||||
|
article: ["How To Design Notifications For Better UX"][ux-article]
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="/assets/img/notification-example.gif">
|
||||||
|
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||||
|
</a>
|
||||||
|
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
For a more detailed look on the options and return values each of these methods
|
||||||
|
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||||
|
|
||||||
|
| API Member | Description |
|
||||||
|
| ---------- | --------------------------------------- |
|
||||||
|
| `hide()` | Hides the specified notification |
|
||||||
|
| `show()` | Creates and displays a new notification |
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
| Implementation | Consumer |
|
||||||
|
| ---------------------------------------- | ----------------------------------------- |
|
||||||
|
| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* |
|
||||||
|
|
||||||
|
`*` - Denotes maintained by OHIF
|
||||||
|
|
||||||
|
> 3rd Party implementers may be added to this table via pull requests.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LINKS
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js
|
||||||
|
[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js
|
||||||
|
[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js
|
||||||
|
[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Viewer: Configuration
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Hotkeys
|
||||||
|
|
||||||
|
...
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Viewer
|
||||||
|
|
||||||
|
The OHIF Viewing Platform strives to be highly configurable and extensible. This
|
||||||
|
makes it easier for our community members to keep their "secret sauce" private,
|
||||||
|
and incentivises contributions back to the platform. The `@ohif/viewer` project
|
||||||
|
of the platform is the lynchpin that combines everything to create our
|
||||||
|
application.
|
||||||
|
|
||||||
|
- When configuration and themeing aren't enough
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Translating
|
# Viewer: Internationalization
|
||||||
|
|
||||||
OHIF supports internationalization using [i18next](https://www.i18next.com/)
|
OHIF supports internationalization using [i18next](https://www.i18next.com/)
|
||||||
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
|
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
|
||||||
@@ -15,7 +15,7 @@ where is the main instance of i18n containing several languages and tools.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Installing
|
## Installing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn add @ohif/i18n
|
yarn add @ohif/i18n
|
||||||
@@ -25,7 +25,7 @@ yarn add @ohif/i18n
|
|||||||
npm install --save @ohif/i18n
|
npm install --save @ohif/i18n
|
||||||
```
|
```
|
||||||
|
|
||||||
### How it works
|
## How it works
|
||||||
|
|
||||||
After installing `@ohif/i18n` npm package, the translation function
|
After installing `@ohif/i18n` npm package, the translation function
|
||||||
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
|
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
|
||||||
@@ -57,13 +57,13 @@ If the translation.json file contains a key that matches the HTML content e.g.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### With React
|
### With React
|
||||||
|
|
||||||
This section will introduce you to [react-i18next](https://react.i18next.com/)
|
This section will introduce you to [react-i18next](https://react.i18next.com/)
|
||||||
basics and show how to implement the [t](https://www.i18next.com/overview/api#t)
|
basics and show how to implement the [t](https://www.i18next.com/overview/api#t)
|
||||||
function easily.
|
function easily.
|
||||||
|
|
||||||
##### Using HOCs
|
#### Using HOCs
|
||||||
|
|
||||||
In most cases we used
|
In most cases we used
|
||||||
[High Order Components](https://react.i18next.com/latest/withtranslation-hoc) to
|
[High Order Components](https://react.i18next.com/latest/withtranslation-hoc) to
|
||||||
@@ -86,13 +86,13 @@ export default withTranslation('MyNameSpace')(MyComponent);
|
|||||||
> [I18nextProvider](#using-outside-of-ohif-viewer) section, `withTranslation`
|
> [I18nextProvider](#using-outside-of-ohif-viewer) section, `withTranslation`
|
||||||
> HOC doesnt works without a I18nextProvider
|
> HOC doesnt works without a I18nextProvider
|
||||||
|
|
||||||
##### Using Hooks
|
#### Using Hooks
|
||||||
|
|
||||||
Also, it's possible to get the `t` tool using
|
Also, it's possible to get the `t` tool using
|
||||||
[React Hooks](https://react.i18next.com/latest/usetranslation-hook), but it
|
[React Hooks](https://react.i18next.com/latest/usetranslation-hook), but it
|
||||||
requires at least React > 16.8 😉
|
requires at least React > 16.8 😉
|
||||||
|
|
||||||
#### Using outside of OHIF viewer
|
### Using outside of OHIF viewer
|
||||||
|
|
||||||
OHIF Viewer already sets a main
|
OHIF Viewer already sets a main
|
||||||
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
|
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
|
||||||
@@ -119,7 +119,7 @@ usage.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Without React
|
### Without React
|
||||||
|
|
||||||
When needed, you can also use available translations _without React_.
|
When needed, you can also use available translations _without React_.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Themeing
|
# Viewer: Themeing
|
||||||
|
|
||||||
Themeing is currently accomplished with color variables that are defined within
|
Themeing is currently accomplished with color variables that are defined within
|
||||||
the [`:root`](https://css-tricks.com/almanac/selectors/r/root/) selector
|
the [`:root`](https://css-tricks.com/almanac/selectors/r/root/) selector
|
||||||
@@ -11,7 +11,11 @@ export default {
|
|||||||
* LIFECYCLE HOOKS
|
* LIFECYCLE HOOKS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
preRegistration(extensionConfiguration) {},
|
preRegistration({
|
||||||
|
servicesManager = {},
|
||||||
|
commandsManager = {},
|
||||||
|
configuration = {},
|
||||||
|
}) {},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MODULE GETTERS
|
* MODULE GETTERS
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,214 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
|
||||||
|
|
||||||
|
|
||||||
|
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.0...@ohif/extension-cornerstone@1.4.1) (2019-11-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.1...@ohif/extension-cornerstone@1.4.0) (2019-10-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes [#962](https://github.com/OHIF/Viewers/issues/962)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
* version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.1...@ohif/extension-cornerstone@1.2.2) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.0...@ohif/extension-cornerstone@1.2.1) (2019-09-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
|
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
|||||||
and is connected the redux store. This module is the most prone to change as we
|
and is connected the redux store. This module is the most prone to change as we
|
||||||
hammer out our Viewport interface.
|
hammer out our Viewport interface.
|
||||||
|
|
||||||
|
## Tool Configuration
|
||||||
|
|
||||||
|
Tools can be configured through extension configuration using the tools key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
...
|
||||||
|
cornerstoneExtensionConfig: {
|
||||||
|
tools: {
|
||||||
|
ArrowAnnotate: {
|
||||||
|
configuration: {
|
||||||
|
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
### Repositories
|
### Repositories
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "1.2.0",
|
"version": "2.1.0",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -32,9 +32,9 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "^4.0.1",
|
"cornerstone-tools": "^4.8.0",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@@ -47,7 +47,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-cornerstone-viewport": "0.1.30"
|
"query-string": "^6.8.3",
|
||||||
|
"react-cornerstone-viewport": "2.x.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import CornerstoneViewport from 'react-cornerstone-viewport';
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import throttle from 'lodash.throttle';
|
import throttle from 'lodash.throttle';
|
||||||
|
import { setEnabledElement } from './state';
|
||||||
|
|
||||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||||
const {
|
const {
|
||||||
@@ -23,36 +24,40 @@ const MEASUREMENT_ACTION_MAP = {
|
|||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
let dataFromStore;
|
let dataFromStore;
|
||||||
|
|
||||||
|
// TODO: This may not be updated anymore :thinking:
|
||||||
if (state.extensions && state.extensions.cornerstone) {
|
if (state.extensions && state.extensions.cornerstone) {
|
||||||
dataFromStore = state.extensions.cornerstone;
|
dataFromStore = state.extensions.cornerstone;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should be extension configuration
|
|
||||||
// ...dataFromStore -->
|
|
||||||
// availableTools,
|
|
||||||
// onNewImage,
|
|
||||||
// onRightClick,
|
|
||||||
// onTouchPress,
|
|
||||||
// onTouchStart,
|
|
||||||
// onMouseClick,
|
|
||||||
|
|
||||||
// If this is the active viewport, enable prefetching.
|
// If this is the active viewport, enable prefetching.
|
||||||
const { viewportIndex } = ownProps; //.viewportData;
|
const { viewportIndex } = ownProps; //.viewportData;
|
||||||
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
||||||
const viewportSpecificData =
|
const viewportSpecificData =
|
||||||
state.viewports.viewportSpecificData[viewportIndex] || {};
|
state.viewports.viewportSpecificData[viewportIndex] || {};
|
||||||
|
|
||||||
|
// CINE
|
||||||
|
let isPlaying = false;
|
||||||
|
let frameRate = 24;
|
||||||
|
|
||||||
|
if (viewportSpecificData && viewportSpecificData.cine) {
|
||||||
|
const cine = viewportSpecificData.cine;
|
||||||
|
|
||||||
|
isPlaying = cine.isPlaying === true;
|
||||||
|
frameRate = cine.cineFrameRate || frameRate;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layout: state.viewports.layout,
|
// layout: state.viewports.layout,
|
||||||
isActive,
|
isActive,
|
||||||
// TODO: Need a cleaner and more versatile way.
|
// TODO: Need a cleaner and more versatile way.
|
||||||
// Currently justing using escape hatch + commands
|
// Currently justing using escape hatch + commands
|
||||||
// activeTool: activeButton && activeButton.command,
|
// activeTool: activeButton && activeButton.command,
|
||||||
...dataFromStore,
|
...dataFromStore,
|
||||||
enableStackPrefetch: isActive,
|
isStackPrefetchEnabled: isActive,
|
||||||
|
isPlaying,
|
||||||
|
frameRate,
|
||||||
//stack: viewportSpecificData.stack,
|
//stack: viewportSpecificData.stack,
|
||||||
cineToolData: viewportSpecificData.cine,
|
// viewport: viewportSpecificData.viewport,
|
||||||
viewport: viewportSpecificData.viewport,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,11 +81,11 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
*/
|
*/
|
||||||
onElementEnabled: event => {
|
onElementEnabled: event => {
|
||||||
const enabledElement = event.detail.element;
|
const enabledElement = event.detail.element;
|
||||||
|
setEnabledElement(viewportIndex, enabledElement);
|
||||||
dispatch(
|
dispatch(
|
||||||
setViewportSpecificData(viewportIndex, {
|
setViewportSpecificData(viewportIndex, {
|
||||||
// TODO: Hack to make sure our plugin info is available from the outset
|
// TODO: Hack to make sure our plugin info is available from the outset
|
||||||
plugin: 'cornerstone',
|
plugin: 'cornerstone',
|
||||||
dom: enabledElement,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { ViewportDownloadForm } from '@ohif/ui';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import { getEnabledElement } from './state';
|
||||||
|
|
||||||
|
const MINIMUM_SIZE = 100;
|
||||||
|
const DEFAULT_SIZE = 512;
|
||||||
|
const MAX_TEXTURE_SIZE = 10000;
|
||||||
|
|
||||||
|
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
|
||||||
|
const activeEnabledElement = getEnabledElement(activeViewportIndex);
|
||||||
|
|
||||||
|
const enableViewport = viewportElement => {
|
||||||
|
if (viewportElement) {
|
||||||
|
cornerstone.enable(viewportElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableViewport = viewportElement => {
|
||||||
|
if (viewportElement) {
|
||||||
|
cornerstone.disable(viewportElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
cornerstone.fitToWindow(viewportElement);
|
||||||
|
|
||||||
|
viewportElement.addEventListener(
|
||||||
|
'cornerstoneimagerendered',
|
||||||
|
function updateViewport(event) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(event.target)
|
||||||
|
.element;
|
||||||
|
const type = 'image/' + fileType;
|
||||||
|
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||||
|
|
||||||
|
let newWidth = enabledElement.offsetHeight;
|
||||||
|
let newHeight = enabledElement.offsetWidth;
|
||||||
|
|
||||||
|
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||||
|
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||||
|
newHeight *= multiplier;
|
||||||
|
newWidth *= multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||||
|
|
||||||
|
viewportElement.removeEventListener(
|
||||||
|
'cornerstoneimagerendered',
|
||||||
|
updateViewport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadImage = (activeViewport, viewportElement, width, height) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
if (activeViewport && viewportElement) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(activeViewport);
|
||||||
|
const viewport = Object.assign({}, enabledElement.viewport);
|
||||||
|
delete viewport.scale;
|
||||||
|
viewport.translation = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
|
||||||
|
cornerstone.displayImage(viewportElement, image);
|
||||||
|
cornerstone.setViewport(viewportElement, viewport);
|
||||||
|
cornerstone.resize(viewportElement, true);
|
||||||
|
|
||||||
|
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||||
|
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||||
|
|
||||||
|
resolve({ image, width: newWidth, height: newHeight });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAnnotations = (toggle, viewportElement) => {
|
||||||
|
cornerstoneTools.store.state.tools.forEach(({ name }) => {
|
||||||
|
if (toggle) {
|
||||||
|
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
|
||||||
|
} else {
|
||||||
|
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadBlob = (
|
||||||
|
filename,
|
||||||
|
fileType,
|
||||||
|
viewportElement,
|
||||||
|
downloadCanvas
|
||||||
|
) => {
|
||||||
|
const file = `${filename}.${fileType}`;
|
||||||
|
const mimetype = `image/${fileType}`;
|
||||||
|
|
||||||
|
/* Handles JPEG images for IE11 */
|
||||||
|
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
|
||||||
|
const image = downloadCanvas.toDataURL(mimetype, 1);
|
||||||
|
const blob = utils.b64toBlob(
|
||||||
|
image.replace('data:image/jpeg;base64,', ''),
|
||||||
|
mimetype
|
||||||
|
);
|
||||||
|
return window.navigator.msSaveBlob(blob, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportElement.querySelector('canvas').toBlob(blob => {
|
||||||
|
const URLObj = window.URL || window.webkitURL;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URLObj.createObjectURL(blob);
|
||||||
|
a.download = file;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportDownloadForm
|
||||||
|
onClose={onClose}
|
||||||
|
minimumSize={MINIMUM_SIZE}
|
||||||
|
maximumSize={MAX_TEXTURE_SIZE}
|
||||||
|
defaultSize={DEFAULT_SIZE}
|
||||||
|
canvasClass={'cornerstone-canvas'}
|
||||||
|
activeViewport={activeEnabledElement}
|
||||||
|
enableViewport={enableViewport}
|
||||||
|
disableViewport={disableViewport}
|
||||||
|
updateViewportPreview={updateViewportPreview}
|
||||||
|
loadImage={loadImage}
|
||||||
|
toggleAnnotations={toggleAnnotations}
|
||||||
|
downloadBlob={downloadBlob}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CornerstoneViewportDownloadForm.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CornerstoneViewportDownloadForm;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import ConnectedCornerstoneViewport from "./ConnectedCornerstoneViewport";
|
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from "cornerstone-core";
|
import cornerstone from 'cornerstone-core';
|
||||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ cornerstone.metaData.addProvider(
|
|||||||
StackManager.setMetadataProvider(metadataProvider);
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
const SOP_CLASSES = {
|
const SOP_CLASSES = {
|
||||||
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4"
|
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
const specialCaseHandlers = {};
|
const specialCaseHandlers = {};
|
||||||
@@ -28,11 +28,11 @@ specialCaseHandlers[
|
|||||||
|
|
||||||
class OHIFCornerstoneViewport extends Component {
|
class OHIFCornerstoneViewport extends Component {
|
||||||
state = {
|
state = {
|
||||||
viewportData: null
|
viewportData: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
customProps: {}
|
customProps: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -40,17 +40,17 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
customProps: PropTypes.object
|
customProps: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = "OHIFCornerstoneViewport";
|
static id = 'OHIFCornerstoneViewport';
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log("OHIFCornerstoneViewport init()");
|
console.log('OHIFCornerstoneViewport init()');
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static destroy() {
|
||||||
console.log("OHIFCornerstoneViewport destroy()");
|
console.log('OHIFCornerstoneViewport destroy()');
|
||||||
StackManager.clearStacks();
|
StackManager.clearStacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +72,15 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
frameIndex = 0
|
frameIndex = 0
|
||||||
) {
|
) {
|
||||||
if (!studies || !studies.length) {
|
if (!studies || !studies.length) {
|
||||||
throw new Error("Studies not provided.");
|
throw new Error('Studies not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!studyInstanceUid) {
|
if (!studyInstanceUid) {
|
||||||
throw new Error("StudyInstanceUID not provided.");
|
throw new Error('StudyInstanceUID not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!displaySetInstanceUid) {
|
if (!displaySetInstanceUid) {
|
||||||
throw new Error("StudyInstanceUID not provided.");
|
throw new Error('StudyInstanceUID not provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create shortcut to displaySet
|
// Create shortcut to displaySet
|
||||||
@@ -89,7 +89,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!study) {
|
if (!study) {
|
||||||
throw new Error("Study not found.");
|
throw new Error('Study not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = study.displaySets.find(set => {
|
const displaySet = study.displaySets.find(set => {
|
||||||
@@ -97,7 +97,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!displaySet) {
|
if (!displaySet) {
|
||||||
throw new Error("Display Set not found.");
|
throw new Error('Display Set not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stack from Stack Manager
|
// Get stack from Stack Manager
|
||||||
@@ -110,7 +110,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
if (sopInstanceUid) {
|
if (sopInstanceUid) {
|
||||||
const index = stack.imageIds.findIndex(imageId => {
|
const index = stack.imageIds.findIndex(imageId => {
|
||||||
const sopCommonModule = cornerstone.metaData.get(
|
const sopCommonModule = cornerstone.metaData.get(
|
||||||
"sopCommonModule",
|
'sopCommonModule',
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
if (!sopCommonModule) {
|
if (!sopCommonModule) {
|
||||||
@@ -124,7 +124,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
stack.currentImageIdIndex = index;
|
stack.currentImageIdIndex = index;
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"SOPInstanceUID provided was not found in specified DisplaySet"
|
'SOPInstanceUID provided was not found in specified DisplaySet'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,22 +132,6 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getViewportData = (
|
|
||||||
studies,
|
|
||||||
studyInstanceUid,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
frameIndex
|
|
||||||
) => {
|
|
||||||
return OHIFCornerstoneViewport.getCornerstoneStack(
|
|
||||||
studies,
|
|
||||||
studyInstanceUid,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
sopInstanceUid,
|
|
||||||
frameIndex
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getViewportData = async (
|
getViewportData = async (
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
@@ -172,7 +156,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
const stack = OHIFCornerstoneViewport.getViewportData(
|
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
@@ -183,7 +167,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
viewportData = {
|
viewportData = {
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
stack
|
stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -199,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
sopInstanceUid,
|
sopInstanceUid,
|
||||||
frameIndex
|
frameIndex,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||||
@@ -208,7 +192,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
if (sopClassUids && sopClassUids.length > 1) {
|
if (sopClassUids && sopClassUids.length > 1) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"More than one SOPClassUid in the same series is not yet supported."
|
'More than one SOPClassUid in the same series is not yet supported.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +207,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
frameIndex
|
frameIndex
|
||||||
).then(viewportData => {
|
).then(viewportData => {
|
||||||
this.setState({
|
this.setState({
|
||||||
viewportData
|
viewportData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -249,25 +233,45 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let childrenWithProps = null;
|
let childrenWithProps = null;
|
||||||
|
|
||||||
|
if (!this.state.viewportData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { viewportIndex } = this.props;
|
||||||
|
const {
|
||||||
|
imageIds,
|
||||||
|
currentImageIdIndex,
|
||||||
|
// If this comes from the instance, would be a better default
|
||||||
|
// `FrameTime` in the instance
|
||||||
|
// frameRate = 0,
|
||||||
|
} = this.state.viewportData.stack;
|
||||||
|
|
||||||
// TODO: Does it make more sense to use Context?
|
// TODO: Does it make more sense to use Context?
|
||||||
if (this.props.children && this.props.children.length) {
|
if (this.props.children && this.props.children.length) {
|
||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return (
|
||||||
viewportIndex: this.props.viewportIndex,
|
child &&
|
||||||
key: index
|
React.cloneElement(child, {
|
||||||
});
|
viewportIndex: this.props.viewportIndex,
|
||||||
|
key: index,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.state.viewportData && (
|
<ConnectedCornerstoneViewport
|
||||||
<ConnectedCornerstoneViewport
|
viewportIndex={viewportIndex}
|
||||||
viewportData={this.state.viewportData}
|
imageIds={imageIds}
|
||||||
viewportIndex={this.props.viewportIndex}
|
imageIdIndex={currentImageIdIndex}
|
||||||
{...this.props.customProps}
|
// ~~ Connected (From REDUX)
|
||||||
/>
|
// frameRate={frameRate}
|
||||||
)}
|
// isPlaying={false}
|
||||||
|
// isStackPrefetchEnabled={true}
|
||||||
|
// onElementEnabled={() => {}}
|
||||||
|
// setViewportActive{() => {}}
|
||||||
|
{...this.props.customProps}
|
||||||
|
/>
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* We use this component to leverage "Code Splitting"
|
||||||
|
*
|
||||||
|
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default function asyncComponent(importComponent) {
|
||||||
|
class AsyncComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Add dynamically loaded component to state
|
||||||
|
const { default: component } = await importComponent();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const C = this.state.component;
|
||||||
|
|
||||||
|
// Render the loaded component, or null
|
||||||
|
return C ? <C {...this.props} /> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncComponent;
|
||||||
|
}
|
||||||
@@ -1,261 +1,360 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
|
import { getEnabledElement } from './state';
|
||||||
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
const scroll = cornerstoneTools.import('util/scroll');
|
||||||
|
|
||||||
const actions = {
|
const commandsModule = ({ servicesManager }) => {
|
||||||
rotateViewport: ({ viewports, rotation }) => {
|
const actions = {
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
rotateViewport: ({ viewports, rotation }) => {
|
||||||
viewports.viewportSpecificData,
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enabledElement) {
|
if (enabledElement) {
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.rotation += rotation;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
flipViewportHorizontal: ({ viewports }) => {
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.hflip = !viewport.hflip;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
flipViewportVertical: ({ viewports }) => {
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.vflip = !viewport.vflip;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scaleViewport: ({ viewports, direction }) => {
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
const step = direction * 0.15;
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
if (step) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
viewport.scale += step;
|
viewport.rotation += rotation;
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
} else {
|
|
||||||
cornerstone.fitToWindow(enabledElement);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
flipViewportHorizontal: ({ viewports }) => {
|
||||||
resetViewport: ({ viewports }) => {
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enabledElement) {
|
if (enabledElement) {
|
||||||
cornerstone.reset(enabledElement);
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
}
|
viewport.hflip = !viewport.hflip;
|
||||||
},
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
invertViewport: ({ viewports }) => {
|
}
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
},
|
||||||
viewports.viewportSpecificData,
|
flipViewportVertical: ({ viewports }) => {
|
||||||
viewports.activeViewportIndex
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
);
|
|
||||||
|
|
||||||
if (enabledElement) {
|
if (enabledElement) {
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
viewport.invert = !viewport.invert;
|
viewport.vflip = !viewport.vflip;
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
scaleViewport: ({ direction, viewports }) => {
|
||||||
// better mouseButtonMask sets.
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
setToolActive: ({ toolName }) => {
|
const step = direction * 0.15;
|
||||||
if (!toolName) {
|
|
||||||
console.warn('No toolname provided to setToolActive command');
|
|
||||||
}
|
|
||||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
|
||||||
},
|
|
||||||
updateViewportDisplaySet: ({ direction }) => {
|
|
||||||
// TODO
|
|
||||||
console.warn('updateDisplaySet: ', direction);
|
|
||||||
},
|
|
||||||
clearAnnotations: ({ viewports }) => {
|
|
||||||
const element = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
if (enabledElement) {
|
||||||
if (!enabledElement || !enabledElement.image) {
|
if (step) {
|
||||||
return;
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
}
|
viewport.scale += step;
|
||||||
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
|
} else {
|
||||||
|
cornerstone.fitToWindow(enabledElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetViewport: ({ viewports }) => {
|
||||||
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
|
|
||||||
const {
|
if (enabledElement) {
|
||||||
toolState,
|
cornerstone.reset(enabledElement);
|
||||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
}
|
||||||
if (
|
},
|
||||||
!toolState ||
|
invertViewport: ({ viewports }) => {
|
||||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
if (enabledElement) {
|
||||||
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
|
viewport.invert = !viewport.invert;
|
||||||
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
||||||
|
// better mouseButtonMask sets.
|
||||||
|
setToolActive: ({ toolName }) => {
|
||||||
|
if (!toolName) {
|
||||||
|
console.warn('No toolname provided to setToolActive command');
|
||||||
|
}
|
||||||
|
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||||
|
},
|
||||||
|
updateViewportDisplaySet: ({ direction }) => {
|
||||||
|
// TODO
|
||||||
|
console.warn('updateDisplaySet: ', direction);
|
||||||
|
},
|
||||||
|
clearAnnotations: ({ viewports }) => {
|
||||||
|
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const measurementsToRemove = [];
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
if (!enabledElement || !enabledElement.image) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(imageIdToolState).forEach(toolType => {
|
const {
|
||||||
const { data } = imageIdToolState[toolType];
|
toolState,
|
||||||
|
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
if (
|
||||||
|
!toolState ||
|
||||||
|
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
data.forEach(measurementData => {
|
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||||
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
|
|
||||||
if (!_id) {
|
const measurementsToRemove = [];
|
||||||
|
|
||||||
|
Object.keys(imageIdToolState).forEach(toolType => {
|
||||||
|
const { data } = imageIdToolState[toolType];
|
||||||
|
|
||||||
|
data.forEach(measurementData => {
|
||||||
|
const {
|
||||||
|
_id,
|
||||||
|
lesionNamingNumber,
|
||||||
|
measurementNumber,
|
||||||
|
} = measurementData;
|
||||||
|
if (!_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementsToRemove.push({
|
||||||
|
toolType,
|
||||||
|
_id,
|
||||||
|
lesionNamingNumber,
|
||||||
|
measurementNumber,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
measurementsToRemove.forEach(measurementData => {
|
||||||
|
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||||
|
detail: {
|
||||||
|
toolType: measurementData.toolType,
|
||||||
|
measurementData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
nextImage: ({ viewports }) => {
|
||||||
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
|
scroll(enabledElement, 1);
|
||||||
|
},
|
||||||
|
previousImage: ({ viewports }) => {
|
||||||
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
|
scroll(enabledElement, -1);
|
||||||
|
},
|
||||||
|
getActiveViewportEnabledElement: ({ viewports }) => {
|
||||||
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
|
return enabledElement;
|
||||||
|
},
|
||||||
|
showDownloadViewportModal: ({ title, viewports }) => {
|
||||||
|
const activeViewportIndex = viewports.activeViewportIndex;
|
||||||
|
const { UIModalService } = servicesManager.services;
|
||||||
|
if (UIModalService) {
|
||||||
|
UIModalService.show({
|
||||||
|
content: CornerstoneViewportDownloadForm,
|
||||||
|
title,
|
||||||
|
contentProps: {
|
||||||
|
activeViewportIndex,
|
||||||
|
onClose: UIModalService.hide,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTableWithNewMeasurementData({
|
||||||
|
toolType,
|
||||||
|
measurementNumber,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
}) {
|
||||||
|
// Update all measurements by measurement number
|
||||||
|
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||||
|
const measurements = measurementApi.tools[toolType].filter(
|
||||||
|
m => m.measurementNumber === measurementNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
measurements.forEach(measurement => {
|
||||||
|
measurement.location = location;
|
||||||
|
measurement.description = description;
|
||||||
|
|
||||||
|
measurementApi.updateMeasurement(measurement.toolType, measurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
measurementApi.syncMeasurementsAndToolData();
|
||||||
|
|
||||||
|
// Update images in all active viewports
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
||||||
|
const nearbyTool = {};
|
||||||
|
let pointNearTool = false;
|
||||||
|
|
||||||
|
availableToolTypes.forEach(toolType => {
|
||||||
|
const elementToolData = cornerstoneTools.getToolState(
|
||||||
|
element,
|
||||||
|
toolType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!elementToolData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
measurementsToRemove.push({
|
elementToolData.data.forEach((toolData, index) => {
|
||||||
toolType,
|
let elementToolInstance = cornerstoneTools.getToolForElement(
|
||||||
_id,
|
element,
|
||||||
lesionNamingNumber,
|
toolType
|
||||||
measurementNumber,
|
);
|
||||||
|
|
||||||
|
if (!elementToolInstance) {
|
||||||
|
elementToolInstance = cornerstoneTools.getToolForElement(
|
||||||
|
element,
|
||||||
|
`${toolType}Tool`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elementToolInstance) {
|
||||||
|
console.warn('Tool not found.');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
elementToolInstance.pointNearTool(
|
||||||
|
element,
|
||||||
|
toolData,
|
||||||
|
canvasCoordinates
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
pointNearTool = true;
|
||||||
|
nearbyTool.tool = toolData;
|
||||||
|
nearbyTool.index = index;
|
||||||
|
nearbyTool.toolType = toolType;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pointNearTool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
measurementsToRemove.forEach(measurementData => {
|
return pointNearTool ? nearbyTool : undefined;
|
||||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
},
|
||||||
detail: {
|
removeToolState: ({ element, toolType, tool }) => {
|
||||||
toolType: measurementData.toolType,
|
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||||
measurementData,
|
cornerstone.updateImage(element);
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
});
|
|
||||||
},
|
|
||||||
nextImage: ({ viewports }) => {
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
|
||||||
viewports.viewportSpecificData,
|
|
||||||
viewports.activeViewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
scroll(enabledElement, 1);
|
const definitions = {
|
||||||
},
|
getNearbyToolData: {
|
||||||
previousImage: ({ viewports }) => {
|
commandFn: actions.getNearbyToolData,
|
||||||
const enabledElement = _getActiveViewportEnabledElement(
|
storeContexts: [],
|
||||||
viewports.viewportSpecificData,
|
options: {},
|
||||||
viewports.activeViewportIndex
|
},
|
||||||
);
|
removeToolState: {
|
||||||
|
commandFn: actions.removeToolState,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
updateTableWithNewMeasurementData: {
|
||||||
|
commandFn: actions.updateTableWithNewMeasurementData,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
showDownloadViewportModal: {
|
||||||
|
commandFn: actions.showDownloadViewportModal,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
getActiveViewportEnabledElement: {
|
||||||
|
commandFn: actions.getActiveViewportEnabledElement,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
rotateViewportCW: {
|
||||||
|
commandFn: actions.rotateViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { rotation: 90 },
|
||||||
|
},
|
||||||
|
rotateViewportCCW: {
|
||||||
|
commandFn: actions.rotateViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { rotation: -90 },
|
||||||
|
},
|
||||||
|
invertViewport: {
|
||||||
|
commandFn: actions.invertViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
flipViewportVertical: {
|
||||||
|
commandFn: actions.flipViewportVertical,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
flipViewportHorizontal: {
|
||||||
|
commandFn: actions.flipViewportHorizontal,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
scaleUpViewport: {
|
||||||
|
commandFn: actions.scaleViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { direction: 1 },
|
||||||
|
},
|
||||||
|
scaleDownViewport: {
|
||||||
|
commandFn: actions.scaleViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { direction: -1 },
|
||||||
|
},
|
||||||
|
fitViewportToWindow: {
|
||||||
|
commandFn: actions.scaleViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: { direction: 0 },
|
||||||
|
},
|
||||||
|
resetViewport: {
|
||||||
|
commandFn: actions.resetViewport,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
clearAnnotations: {
|
||||||
|
commandFn: actions.clearAnnotations,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
nextImage: {
|
||||||
|
commandFn: actions.nextImage,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
previousImage: {
|
||||||
|
commandFn: actions.previousImage,
|
||||||
|
storeContexts: ['viewports'],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
// TODO: First/Last image
|
||||||
|
// Next/Previous series/DisplaySet
|
||||||
|
nextViewportDisplaySet: {
|
||||||
|
commandFn: actions.updateViewportDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: { direction: 1 },
|
||||||
|
},
|
||||||
|
previousViewportDisplaySet: {
|
||||||
|
commandFn: actions.updateViewportDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: { direction: -1 },
|
||||||
|
},
|
||||||
|
// TOOLS
|
||||||
|
setToolActive: {
|
||||||
|
commandFn: actions.setToolActive,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
scroll(enabledElement, -1);
|
return {
|
||||||
},
|
actions,
|
||||||
|
definitions,
|
||||||
|
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
export default commandsModule;
|
||||||
rotateViewportCW: {
|
|
||||||
commandFn: actions.rotateViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { rotation: 90 },
|
|
||||||
},
|
|
||||||
rotateViewportCCW: {
|
|
||||||
commandFn: actions.rotateViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { rotation: -90 },
|
|
||||||
},
|
|
||||||
invertViewport: {
|
|
||||||
commandFn: actions.invertViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
flipViewportVertical: {
|
|
||||||
commandFn: actions.flipViewportVertical,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
flipViewportHorizontal: {
|
|
||||||
commandFn: actions.flipViewportHorizontal,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
scaleUpViewport: {
|
|
||||||
commandFn: actions.scaleViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { direction: 1 },
|
|
||||||
},
|
|
||||||
scaleDownViewport: {
|
|
||||||
commandFn: actions.scaleViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { direction: -1 },
|
|
||||||
},
|
|
||||||
fitViewportToWindow: {
|
|
||||||
commandFn: actions.scaleViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: { direction: 0 },
|
|
||||||
},
|
|
||||||
resetViewport: {
|
|
||||||
commandFn: actions.resetViewport,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
clearAnnotations: {
|
|
||||||
commandFn: actions.clearAnnotations,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
nextImage: {
|
|
||||||
commandFn: actions.nextImage,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
previousImage: {
|
|
||||||
commandFn: actions.previousImage,
|
|
||||||
storeContexts: ['viewports'],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
// TODO: First/Last image
|
|
||||||
// Next/Previous series/DisplaySet
|
|
||||||
nextViewportDisplaySet: {
|
|
||||||
commandFn: actions.updateViewportDisplaySet,
|
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: 1 },
|
|
||||||
},
|
|
||||||
previousViewportDisplaySet: {
|
|
||||||
commandFn: actions.updateViewportDisplaySet,
|
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: -1 },
|
|
||||||
},
|
|
||||||
// TOOLS
|
|
||||||
setToolActive: {
|
|
||||||
commandFn: actions.setToolActive,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grabs `dom` reference for the enabledElement of
|
|
||||||
* the active viewport
|
|
||||||
*/
|
|
||||||
function _getActiveViewportEnabledElement(viewports, activeIndex) {
|
|
||||||
const activeViewport = viewports[activeIndex] || {};
|
|
||||||
return activeViewport.dom;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
actions,
|
|
||||||
definitions,
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@ import OHIF from '@ohif/core';
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager, DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||||
const study = studies.find(
|
const study = studies.find(
|
||||||
@@ -52,14 +52,6 @@ function addSegMetadataToCornerstoneToolState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retrieveDicomData(wadoUri) {
|
|
||||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
|
||||||
// we will need to figure out how / when to pass this information in.
|
|
||||||
return fetch(wadoUri, {
|
|
||||||
headers: OHIF.DICOMWeb.getAuthorizationHeader(),
|
|
||||||
}).then(response => response.arrayBuffer());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSegmentationStorage(
|
async function handleSegmentationStorage(
|
||||||
studies,
|
studies,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
@@ -73,8 +65,11 @@ async function handleSegmentationStorage(
|
|||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
displaySetInstanceUid
|
displaySetInstanceUid
|
||||||
);
|
);
|
||||||
const segWadoUri = displaySet.images[0].getData().wadouri;
|
|
||||||
const arrayBuffer = await retrieveDicomData(segWadoUri);
|
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||||
|
displaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
dicomData.dict
|
dicomData.dict
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import OHIFCornerstoneViewport from './OHIFCornerstoneViewport.js';
|
import init from './init.js';
|
||||||
|
import asyncComponent from './asyncComponent.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
|
|
||||||
|
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -11,13 +19,24 @@ export default {
|
|||||||
*/
|
*/
|
||||||
id: 'cornerstone',
|
id: 'cornerstone',
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
init({ servicesManager, configuration });
|
||||||
|
},
|
||||||
getViewportModule() {
|
getViewportModule() {
|
||||||
return OHIFCornerstoneViewport;
|
return OHIFCornerstoneViewport;
|
||||||
},
|
},
|
||||||
getToolbarModule() {
|
getToolbarModule() {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
},
|
},
|
||||||
getCommandsModule() {
|
getCommandsModule({ servicesManager }) {
|
||||||
return commandsModule;
|
return commandsModule({ servicesManager });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { CornerstoneViewportDownloadForm };
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
import { SimpleDialog } from '@ohif/ui';
|
||||||
|
import merge from 'lodash.merge';
|
||||||
|
|
||||||
|
function fallbackMetaDataProvider(type, imageId) {
|
||||||
|
if (!imageId.includes('wado?requestType=WADO')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you call for an WADO-URI imageId and get no
|
||||||
|
// metadata, try reformatting to WADO-RS imageId
|
||||||
|
const qs = queryString.parse(imageId);
|
||||||
|
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||||
|
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||||
|
qs.seriesUID
|
||||||
|
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||||
|
|
||||||
|
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this fallback provider with a low priority so it is handled last
|
||||||
|
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} servicesManager
|
||||||
|
* @param {Object} configuration
|
||||||
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
|
*/
|
||||||
|
export default function init({ servicesManager, configuration }) {
|
||||||
|
const callInputDialog = (data, event, callback) => {
|
||||||
|
const { UIDialogService } = servicesManager.services;
|
||||||
|
|
||||||
|
if (UIDialogService) {
|
||||||
|
let dialogId = UIDialogService.create({
|
||||||
|
centralize: true,
|
||||||
|
isDraggable: false,
|
||||||
|
content: SimpleDialog.InputDialog,
|
||||||
|
useLastPosition: false,
|
||||||
|
showOverlay: true,
|
||||||
|
contentProps: {
|
||||||
|
title: 'Enter your annotation',
|
||||||
|
label: 'New label',
|
||||||
|
measurementData: data ? { description: data.text } : {},
|
||||||
|
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||||
|
onSubmit: value => {
|
||||||
|
callback(value);
|
||||||
|
UIDialogService.dismiss({ id: dialogId });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { csToolsConfig } = configuration;
|
||||||
|
const { StackManager } = OHIF.utils;
|
||||||
|
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||||
|
|
||||||
|
// ~~ Set our MetadataProvider
|
||||||
|
cornerstone.metaData.addProvider(
|
||||||
|
metadataProvider.provider.bind(metadataProvider)
|
||||||
|
);
|
||||||
|
|
||||||
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
|
// ~~
|
||||||
|
const defaultCsToolsConfig = csToolsConfig || {
|
||||||
|
globalToolSyncEnabled: true,
|
||||||
|
showSVGCursors: true,
|
||||||
|
autoResizeViewports: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
initCornerstoneTools(defaultCsToolsConfig);
|
||||||
|
|
||||||
|
// ~~ Toooools 🙌
|
||||||
|
const tools = [
|
||||||
|
csTools.PanTool,
|
||||||
|
csTools.ZoomTool,
|
||||||
|
csTools.WwwcTool,
|
||||||
|
csTools.MagnifyTool,
|
||||||
|
csTools.StackScrollTool,
|
||||||
|
csTools.StackScrollMouseWheelTool,
|
||||||
|
// Touch
|
||||||
|
csTools.PanMultiTouchTool,
|
||||||
|
csTools.ZoomTouchPinchTool,
|
||||||
|
// Annotations
|
||||||
|
csTools.ArrowAnnotateTool,
|
||||||
|
csTools.EraserTool,
|
||||||
|
csTools.BidirectionalTool,
|
||||||
|
csTools.LengthTool,
|
||||||
|
csTools.AngleTool,
|
||||||
|
csTools.FreehandRoiTool,
|
||||||
|
csTools.EllipticalRoiTool,
|
||||||
|
csTools.DragProbeTool,
|
||||||
|
csTools.RectangleRoiTool,
|
||||||
|
// Segmentation
|
||||||
|
csTools.BrushTool,
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Add extension tools configuration here. */
|
||||||
|
const internalToolsConfig = {
|
||||||
|
ArrowAnnotate: {
|
||||||
|
configuration: {
|
||||||
|
getTextCallback: (callback, eventDetails) =>
|
||||||
|
callInputDialog(null, eventDetails, callback),
|
||||||
|
changeTextCallback: (data, eventDetails, callback) =>
|
||||||
|
callInputDialog(data, eventDetails, callback),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Add tools with its custom props through extension configuration. */
|
||||||
|
tools.forEach(tool => {
|
||||||
|
const toolName = tool.name.replace('Tool', '');
|
||||||
|
const externalToolsConfig = configuration.tools || {};
|
||||||
|
const externalToolProps = externalToolsConfig[toolName] || {};
|
||||||
|
const internalToolProps = internalToolsConfig[toolName] || {};
|
||||||
|
const props = merge(internalToolProps, externalToolProps);
|
||||||
|
csTools.addTool(tool, props);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
|
||||||
|
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
|
||||||
|
tools.forEach(tool => {
|
||||||
|
if (tool.prototype instanceof BaseAnnotationTool) {
|
||||||
|
// BaseAnnotationTool would likely come from csTools lib exports
|
||||||
|
const toolName = new tool().name;
|
||||||
|
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||||
|
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||||
|
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||||
|
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
||||||
|
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||||
|
csTools.setToolActive('ZoomTouchPinch', {});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const state = {
|
||||||
|
enabledElements: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the enabled element `dom` reference for an active viewport.
|
||||||
|
* @param {HTMLElement} dom Active viewport element.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
const setEnabledElement = (viewportIndex, element) =>
|
||||||
|
(state.enabledElements[viewportIndex] = element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grabs the enabled element `dom` reference of an active viewport.
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} Active viewport element.
|
||||||
|
*/
|
||||||
|
const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex];
|
||||||
|
|
||||||
|
export { setEnabledElement, getEnabledElement };
|
||||||
@@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
|
|||||||
BUILT_IN: 'builtIn',
|
BUILT_IN: 'builtIn',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TOOLBAR_BUTTON_BEHAVIORS = {
|
||||||
|
CINE: 'CINE',
|
||||||
|
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* TODO: Export enums through a extension manager. */
|
||||||
|
const enums = {
|
||||||
|
TOOLBAR_BUTTON_TYPES,
|
||||||
|
TOOLBAR_BUTTON_BEHAVIORS,
|
||||||
|
};
|
||||||
|
|
||||||
const definitions = [
|
const definitions = [
|
||||||
{
|
{
|
||||||
id: 'StackScroll',
|
id: 'StackScroll',
|
||||||
@@ -74,7 +85,7 @@ const definitions = [
|
|||||||
label: 'Annotate',
|
label: 'Annotate',
|
||||||
icon: 'measure-non-target',
|
icon: 'measure-non-target',
|
||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||||
},
|
},
|
||||||
@@ -102,7 +113,7 @@ const definitions = [
|
|||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||||
options: {
|
options: {
|
||||||
behavior: 'CINE',
|
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,7 +211,7 @@ const definitions = [
|
|||||||
label: 'Bidirectional',
|
label: 'Bidirectional',
|
||||||
icon: 'measure-target',
|
icon: 'measure-target',
|
||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Bidirectional' },
|
commandOptions: { toolName: 'Bidirectional' },
|
||||||
},
|
},
|
||||||
@@ -209,10 +220,21 @@ const definitions = [
|
|||||||
label: 'Eraser',
|
label: 'Eraser',
|
||||||
icon: 'eraser',
|
icon: 'eraser',
|
||||||
//
|
//
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Eraser' },
|
commandOptions: { toolName: 'Eraser' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'Download',
|
||||||
|
label: 'Download',
|
||||||
|
icon: 'create-screen-capture',
|
||||||
|
//
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||||
|
options: {
|
||||||
|
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
|
||||||
|
togglable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
@@ -34,5 +35,10 @@ module.exports = (env, argv) => {
|
|||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
filename: pkg.main,
|
filename: pkg.main,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,63 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.0...@ohif/extension-dicom-html@1.0.1) (2019-10-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Set SR viewport as active by interaction ([#1118](https://github.com/OHIF/Viewers/issues/1118)) ([5b33417](https://github.com/OHIF/Viewers/commit/5b334175c370afb930b4b6dbd307ddece8f850e3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.8...@ohif/extension-dicom-html@1.0.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* DICOM Seg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.7...@ohif/extension-dicom-html@0.50.8) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.6...@ohif/extension-dicom-html@0.50.7) (2019-09-12)
|
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@0.50.6...@ohif/extension-dicom-html@0.50.7) (2019-09-12)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-html",
|
"name": "@ohif/extension-dicom-html",
|
||||||
"version": "0.50.7",
|
"version": "1.1.0",
|
||||||
"description": "OHIF extension for rendering structured reports to HTML",
|
"description": "OHIF extension for rendering structured reports to HTML",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import DicomHtmlViewport from './DicomHtmlViewport';
|
||||||
|
|
||||||
|
const { setViewportActive } = OHIF.redux.actions;
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const { viewportIndex, byteArray } = ownProps;
|
||||||
|
const { activeViewportIndex } = state.viewports;
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewportIndex,
|
||||||
|
activeViewportIndex,
|
||||||
|
byteArray,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||||
|
const { viewportIndex } = ownProps;
|
||||||
|
|
||||||
|
return {
|
||||||
|
setViewportActive: () => {
|
||||||
|
dispatch(setViewportActive(viewportIndex));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectedDicomHtmlViewport = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DicomHtmlViewport);
|
||||||
|
|
||||||
|
export default ConnectedDicomHtmlViewport;
|
||||||