versioned docs v2
No files matched your search
@@ -0,0 +1,151 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
Looking to extend your instance of the OHIF Viewer? Want learn how to reuse _a
|
||||
portion_ of the Viewer in your own application? Or maybe you want to get
|
||||
involved and draft or suggest a new feature? Regardless, you're in the right
|
||||
place!
|
||||
|
||||
The OHIF Viewer aims to be decoupled, configurable, and extensible; while this
|
||||
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
|
||||
architected, and the role each of it's dependent libraries plays.
|
||||
|
||||
|
||||
|
||||
The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
|
||||
[`monorepo`][monorepo]. This means that this repository, instead of containing a
|
||||
single project, contains many projects. If you explore our project structure,
|
||||
you'll see the following:
|
||||
|
||||
```bash
|
||||
.
|
||||
├── extensions
|
||||
│ ├── _example # Skeleton of example extension
|
||||
│ ├── cornerstone # 2D images w/ Cornerstone.js
|
||||
│ ├── dicom-html # Structured Reports as HTML in viewport
|
||||
│ ├── dicom-microscopy # Whole slide microscopy viewing
|
||||
│ ├── dicom-pdf # View DICOM wrapped PDFs in viewport
|
||||
│ └── vtk # MPR and Volume support w/ VTK.js
|
||||
│
|
||||
├── platform
|
||||
│ ├── core # Business Logic
|
||||
│ ├── i18n # Internationalization Support
|
||||
│ ├── ui # React component library
|
||||
│ └── viewer # Connects platform and extension projects
|
||||
│
|
||||
├── ... # misc. shared configuration
|
||||
├── lerna.json # MonoRepo (Lerna) settings
|
||||
├── package.json # Shared devDependencies and commands
|
||||
└── README.md
|
||||
```
|
||||
|
||||
The `platform` directory contains the business logic library, component library,
|
||||
and the application library that combines them to create a powerful medical
|
||||
imaging viewer.
|
||||
|
||||
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
|
||||
features common to Web-based medical imaging viewers. For example:
|
||||
|
||||
- Hotkeys
|
||||
- DICOM Web requests
|
||||
- Hanging Protocols
|
||||
- Managing a study's measurements
|
||||
- Managing a study's DICOM metadata
|
||||
- [A flexible pattern for extensions](./extensions/index.md)
|
||||
- And many others
|
||||
|
||||
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
|
||||
Vue, React, Vanilla JS, or any number of other frameworks.
|
||||
|
||||
## React Component Library
|
||||
|
||||
[`@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 in isolation; easing the development process by reducing the need to
|
||||
stand-up a local PACS with test case data.
|
||||
|
||||
Extension authors can also use these same components when building their
|
||||
extension's UI; allowing for a consistent look and feel with the rest of the
|
||||
application.
|
||||
|
||||
[Check out our component library!](https://react.ohif.org/)
|
||||
|
||||
## Extensions & Configuration
|
||||
|
||||
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
|
||||
the viewer. Some common use cases include:
|
||||
|
||||
- Adding AI/ML tools and insights
|
||||
- Custom workflows for guided diagnosis
|
||||
- Collecting specific annotations for training data or reports
|
||||
- Authentication and granular permissions
|
||||
- Teleconsultation workflow, image comments, and tracking
|
||||
- Adding surgical templating tools and reports
|
||||
- and many others
|
||||
|
||||
We expose common integration points via [extensions](./extensions/index.md) to
|
||||
make this possible. The viewer and many of our own extensions also offer
|
||||
[configuration][configuration]. For a list of extensions maintained by OHIF,
|
||||
[check out this helpful table](./extensions/index.md#maintained-extensions).
|
||||
|
||||
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
|
||||
looking for ways to improve our extensibility ^\_^
|
||||
|
||||
[Click here to read more about extensions!](./extensions/index.md)
|
||||
|
||||
## Common Questions
|
||||
|
||||
> When should I use the packaged source `@ohif/viewer` versus building a PWA
|
||||
> from the source?
|
||||
|
||||
...
|
||||
|
||||
> Can I create my own Viewer using Vue.js or Angular.js?
|
||||
|
||||
You can, but you will not be able to leverage as much of the existing code and
|
||||
components. `@ohif/core` could still be used for business logic, and to provide
|
||||
a model for extensions. `@ohif/ui` would then become a guide for the components
|
||||
you would need to recreate.
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[monorepo]: https://github.com/OHIF/Viewers/issues/768
|
||||
[viewers-project]: https://github.com/OHIF/Viewers
|
||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
||||
[configuration]: ../configuring/index.md
|
||||
[extensions]: ../extensions/index.md
|
||||
[core-github]: https://github.com/OHIF/viewers/platform/core
|
||||
[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: Introduction
|
||||
slug: /
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
|
||||
<!-- <div class='row'>
|
||||
<div class='column' style='text-align: right; padding: 0 20px'>
|
||||
<strong>Looking for a Live Demo?</strong>
|
||||
<a href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
||||
</div>
|
||||
<div class='column' style='text-align: left; padding: 0 20px'>
|
||||
<a href="https://www.netlify.com">
|
||||
<img src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
||||
</a>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div>
|
||||
<strong>Looking for a Live Demo?</strong>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <a href="https://www.netlify.com"> -->
|
||||
|
||||
<div className="text--center">
|
||||
<a style={{marginRight:'10px'}} href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
||||
<a href="https://www.netlify.com"/>
|
||||
<img style={{width:'70px'}} src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<img className={{width:'50px'}} src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
||||
</a> -->
|
||||
|
||||
|
||||
> ATTENTION! You are looking at the docs for the `React` version of the OHIF
|
||||
> Viewer. If you're looking for the `Meteor` version's documentation (now
|
||||
> deprecated), select it's version from the dropdown box in the top right corner
|
||||
> of this page.
|
||||
|
||||
The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
|
||||
web-based, medical imaging viewer. It can be configured to connect to Image
|
||||
Archives that support [DicomWeb][dicom-web], and offers support for mapping to
|
||||
proprietary API formats. OHIF maintained extensions add support for viewing,
|
||||
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
|
||||
|
||||

|
||||
|
||||
<center>The <strong>OHIF Viewer</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
||||
|
||||
The Open Health Imaging Foundation intends to provide a simple general purpose
|
||||
DICOM Viewer which can be easily extended for specific uses. If you find
|
||||
yourself unable to extend the viewer for your purposes, please reach out via our
|
||||
[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
|
||||
our integration and extension points.
|
||||
|
||||
## Where to next?
|
||||
|
||||
Check out these helpful links:
|
||||
|
||||
- Ready to dive into some code? Check out our
|
||||
[Getting Started Guide](./development/getting-started.md).
|
||||
- We're an active, vibrant community.
|
||||
[Learn how you can be more involved.](./development/contributing.md)
|
||||
- Feeling lost? Read our [help page](./help.md).
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[ohif-org]: http://www.ohif.org
|
||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 7.5 2 A 4.5 4.5 0 0 0 3 6.5 A 4.5 4.5 0 0 0 3.0527344 7.15625 A 3 3 0 0 0 1 10 A 3 3 0 0 0 4 13 L 11.5 13 A 3.5 3.5 0 0 0 15 9.5 A 3.5 3.5 0 0 0 11.970703 6.0351562 A 4.5 4.5 0 0 0 7.5 2 z M 7.5 3 A 3.5 3.5 0 0 1 11 6.5 A 3.5 3.5 0 0 1 10.949219 7.0644531 A 2.5 2.5 0 0 1 11.5 7 A 2.5 2.5 0 0 1 14 9.5 A 2.5 2.5 0 0 1 11.5 12 L 4 12 A 2 2 0 0 1 2 10 A 2 2 0 0 1 4 8 A 2 2 0 0 1 4.3574219 8.0332031 A 3.5 3.5 0 0 1 4 6.5 A 3.5 3.5 0 0 1 7.5 3 z "
|
||||
id="path4139"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="npm" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 780 250" style="enable-background:new 0 0 780 250;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#C12127;}
|
||||
</style>
|
||||
<path class="st0" d="M240,250h100v-50h100V0H240V250z M340,50h50v100h-50V50z M480,0v200h100V50h50v150h50V50h50v150h50V0H480z
|
||||
M0,200h100V50h50v150h50V0H0V200z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
|
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 |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 884 KiB |
|
After Width: | Height: | Size: 440 KiB |
@@ -0,0 +1,187 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="203px" height="39px" viewBox="0 0 203 39" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
|
||||
<title>Custom Preset 2 Copy</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter x="-22.4%" y="-22.4%" width="144.8%" height="144.8%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4707441 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-2"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-4"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-6"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-8"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-10"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-12"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-14"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-16"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-18"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-20"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-22"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-24"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-26"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-28"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-30"></path>
|
||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-32"></path>
|
||||
<polyline id="path-34" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polyline>
|
||||
<polyline id="path-36" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polyline>
|
||||
<polygon id="path-38" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polygon>
|
||||
</defs>
|
||||
<g id="Custom-Preset-2-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect fill="#FFFFFF" x="0" y="0" width="203" height="39"></rect>
|
||||
<g id="Group-65" filter="url(#filter-1)" transform="translate(6.000000, 5.000000)">
|
||||
<g id="Group-49" transform="translate(1.000000, 1.000000)">
|
||||
<g id="Group-3">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"></use>
|
||||
</mask>
|
||||
<g id="Clip-2"></g>
|
||||
<path d="M1.85138074,6.7158 C1.69250667,7.85927059 2.08948444,8.45077059 1.85138074,9.00253529 C1.61306963,9.55450588 1.3349363,10.5402706 1.57324741,11.2104176 C1.81155852,11.8805647 2.16892148,13.3392412 2.48666963,13.3789765 C2.80441778,13.4180941 2.24856593,11.8019176 2.56610667,12.3143588 C2.88406222,12.8270059 2.88406222,13.5893882 3.44012148,14.1150059 C3.99597333,14.6406235 4.75072889,14.5617706 5.34661037,15.0740059 C5.94228444,15.5866529 6.53795852,16.3358588 7.09401778,16.2570059 C7.09401778,16.2570059 7.05419556,16.6510647 6.77626963,17.3214176 C6.4983437,17.9915647 6.45852148,18.7798882 6.81609185,19.2532118 C7.17345481,19.7263294 7.53081778,20.4362118 7.84877333,20.7909471 C8.16652148,21.1456824 8.40483259,23.0381529 8.32518815,23.9841824 C8.24595852,24.9304176 8.68275852,27.0594471 9.67561778,27.5327706 C10.668477,28.0058882 9.67561778,27.2960059 9.47712889,26.7047118 C9.27843259,26.1134176 9.63040296,24.7333882 10.1101363,24.5361529 C10.58904,24.3389176 11.0260474,23.5505941 11.3834104,22.9984176 C11.7409807,22.4466529 12.654403,22.2098882 12.9325363,21.5399471 C13.2106696,20.8698 13.0914104,20.0810647 13.2502844,19.7263294 C13.4091585,19.3713882 13.8461659,18.5828588 12.8927141,18.1097412 C11.939677,17.6366235 11.5424919,17.8733882 11.5026696,17.4395941 C11.4628474,17.0058 11.2245363,16.5328882 10.5496326,16.3358588 C9.87431407,16.1384176 9.27843259,15.7560941 8.52367704,15.6714765 C7.76912889,15.5866529 7.61025481,16.1781529 7.21306963,15.8625353 C6.81609185,15.5473294 6.4983437,15.2317118 6.33926222,14.8376529 C6.18059556,14.4433882 5.70376593,14.6799471 5.74379556,14.4433882 C5.78320296,14.2066235 6.41869926,13.5366824 5.70376593,13.7732412 C4.98904,14.0095941 4.3535437,13.9307412 4.39315852,13.3001235 C4.43318815,12.6693 5.30678815,12.4325353 5.66435852,12.3536824 C6.02151407,12.2748294 6.29964741,12.7084176 6.73665481,12.9847118 C7.17345481,13.2605941 6.77626963,12.5904471 7.01458074,12.1171235 C7.25289185,11.6440059 8.16652148,11.6835353 8.28557333,11.0133882 C8.40483259,10.3432412 8.24595852,9.79106471 8.96089185,9.71221176 C9.67561778,9.63335882 10.4301659,9.75153529 10.5098104,9.39659412 C10.58904,9.04206471 9.67561778,9.51518235 9.63600296,9.08138824 C9.59618074,8.6478 10.4301659,8.05650588 10.668477,8.49009412 C10.9067881,8.92368235 11.5026696,8.96321176 11.5821067,8.8053 C11.6615437,8.6478 11.5821067,8.37191765 11.3039733,7.70156471 C11.0260474,7.03121176 10.7479141,7.22844706 10.5098104,6.47944706 C10.2714993,5.73024118 9.87431407,6.51877059 9.47712889,6.12450588 C9.0799437,5.73024118 8.60332148,4.90218235 8.52367704,5.61206471 C8.44444741,6.32153529 8.44444741,6.7158 8.08708444,7.26777059 C7.72951407,7.81994706 7.57084741,7.38615294 7.25289185,7.03121176 C6.9351437,6.67647647 6.22021037,6.32153529 6.45852148,6.00632941 C6.69683259,5.69071176 6.9351437,4.86285882 7.29250667,5.09962353 C7.65007704,5.33618235 8.2061363,5.33618235 8.12669926,4.94171176 C8.04726222,4.54765294 8.12669926,4.38974118 8.28557333,4.19271176 C8.44444741,3.99547647 9.00050667,3.64053529 9.00050667,4.35041765 C9.00050667,5.06009412 8.92086222,5.25732941 9.47712889,5.33618235 C10.0331881,5.41503529 10.668477,5.45435882 10.6290696,5.09962353 C10.58904,4.74468235 10.7879437,4.82353529 10.6290696,4.23203529 C10.4699881,3.64053529 9.99336593,3.60141765 9.47712889,3.32512353 C8.96089185,3.04924118 8.68275852,2.61565294 8.24595852,2.77335882 C7.80915852,2.93106471 7.76912889,3.08856471 7.72951407,3.52235882 C7.68969185,3.95615294 7.45138074,4.11385882 7.29250667,3.91662353 C7.13384,3.71959412 7.33232889,3.08856471 6.73665481,3.48303529 C6.14077333,3.87709412 5.90266963,4.54765294 5.58492148,4.07432941 C5.26696593,3.60141765 6.0613363,2.77335882 5.22735111,3.1283 C4.39315852,3.48303529 3.79727704,3.87709412 4.23428444,4.03500588 C4.67129185,4.19271176 5.10829926,4.4688 4.67129185,4.62650588 C4.23428444,4.78400588 3.36047704,4.50812353 3.04272889,4.82353529 C2.72498074,5.13874118 1.85138074,6.7158 1.85138074,6.7158" id="Fill-1" fill="#2196F3" mask="url(#mask-3)"></path>
|
||||
</g>
|
||||
<g id="Group-6">
|
||||
<mask id="mask-5" fill="white">
|
||||
<use xlink:href="#path-4"></use>
|
||||
</mask>
|
||||
<g id="Clip-5"></g>
|
||||
<path d="M1.85138074,6.7158 C1.69250667,7.85927059 2.08948444,8.45077059 1.85138074,9.00253529 C1.61306963,9.55450588 1.3349363,10.5402706 1.57324741,11.2104176 C1.81155852,11.8805647 2.16892148,13.3392412 2.48666963,13.3789765 C2.80441778,13.4180941 2.24856593,11.8019176 2.56610667,12.3143588 C2.88406222,12.8270059 2.88406222,13.5893882 3.44012148,14.1150059 C3.99597333,14.6406235 4.75072889,14.5617706 5.34661037,15.0740059 C5.94228444,15.5866529 6.53795852,16.3358588 7.09401778,16.2570059 C7.09401778,16.2570059 7.05419556,16.6510647 6.77626963,17.3214176 C6.4983437,17.9915647 6.45852148,18.7798882 6.81609185,19.2532118 C7.17345481,19.7263294 7.53081778,20.4362118 7.84877333,20.7909471 C8.16652148,21.1456824 8.40483259,23.0381529 8.32518815,23.9841824 C8.24595852,24.9304176 8.68275852,27.0594471 9.67561778,27.5327706 C10.668477,28.0058882 9.67561778,27.2960059 9.47712889,26.7047118 C9.27843259,26.1134176 9.63040296,24.7333882 10.1101363,24.5361529 C10.58904,24.3389176 11.0260474,23.5505941 11.3834104,22.9984176 C11.7409807,22.4466529 12.654403,22.2098882 12.9325363,21.5399471 C13.2106696,20.8698 13.0914104,20.0810647 13.2502844,19.7263294 C13.4091585,19.3713882 13.8461659,18.5828588 12.8927141,18.1097412 C11.939677,17.6366235 11.5424919,17.8733882 11.5026696,17.4395941 C11.4628474,17.0058 11.2245363,16.5328882 10.5496326,16.3358588 C9.87431407,16.1384176 9.27843259,15.7560941 8.52367704,15.6714765 C7.76912889,15.5866529 7.61025481,16.1781529 7.21306963,15.8625353 C6.81609185,15.5473294 6.4983437,15.2317118 6.33926222,14.8376529 C6.18059556,14.4433882 5.70376593,14.6799471 5.74379556,14.4433882 C5.78320296,14.2066235 6.41869926,13.5366824 5.70376593,13.7732412 C4.98904,14.0095941 4.3535437,13.9307412 4.39315852,13.3001235 C4.43318815,12.6693 5.30678815,12.4325353 5.66435852,12.3536824 C6.02151407,12.2748294 6.29964741,12.7084176 6.73665481,12.9847118 C7.17345481,13.2605941 6.77626963,12.5904471 7.01458074,12.1171235 C7.25289185,11.6440059 8.16652148,11.6835353 8.28557333,11.0133882 C8.40483259,10.3432412 8.24595852,9.79106471 8.96089185,9.71221176 C9.67561778,9.63335882 10.4301659,9.75153529 10.5098104,9.39659412 C10.58904,9.04206471 9.67561778,9.51518235 9.63600296,9.08138824 C9.59618074,8.6478 10.4301659,8.05650588 10.668477,8.49009412 C10.9067881,8.92368235 11.5026696,8.96321176 11.5821067,8.8053 C11.6615437,8.6478 11.5821067,8.37191765 11.3039733,7.70156471 C11.0260474,7.03121176 10.7479141,7.22844706 10.5098104,6.47944706 C10.2714993,5.73024118 9.87431407,6.51877059 9.47712889,6.12450588 C9.0799437,5.73024118 8.60332148,4.90218235 8.52367704,5.61206471 C8.44444741,6.32153529 8.44444741,6.7158 8.08708444,7.26777059 C7.72951407,7.81994706 7.57084741,7.38615294 7.25289185,7.03121176 C6.9351437,6.67647647 6.22021037,6.32153529 6.45852148,6.00632941 C6.69683259,5.69071176 6.9351437,4.86285882 7.29250667,5.09962353 C7.65007704,5.33618235 8.2061363,5.33618235 8.12669926,4.94171176 C8.04726222,4.54765294 8.12669926,4.38974118 8.28557333,4.19271176 C8.44444741,3.99547647 9.00050667,3.64053529 9.00050667,4.35041765 C9.00050667,5.06009412 8.92086222,5.25732941 9.47712889,5.33618235 C10.0331881,5.41503529 10.668477,5.45435882 10.6290696,5.09962353 C10.58904,4.74468235 10.7879437,4.82353529 10.6290696,4.23203529 C10.4699881,3.64053529 9.99336593,3.60141765 9.47712889,3.32512353 C8.96089185,3.04924118 8.68275852,2.61565294 8.24595852,2.77335882 C7.80915852,2.93106471 7.76912889,3.08856471 7.72951407,3.52235882 C7.68969185,3.95615294 7.45138074,4.11385882 7.29250667,3.91662353 C7.13384,3.71959412 7.33232889,3.08856471 6.73665481,3.48303529 C6.14077333,3.87709412 5.90266963,4.54765294 5.58492148,4.07432941 C5.26696593,3.60141765 6.0613363,2.77335882 5.22735111,3.1283 C4.39315852,3.48303529 3.79727704,3.87709412 4.23428444,4.03500588 C4.67129185,4.19271176 5.10829926,4.4688 4.67129185,4.62650588 C4.23428444,4.78400588 3.36047704,4.50812353 3.04272889,4.82353529 C2.72498074,5.13874118 1.85138074,6.7158 1.85138074,6.7158 Z" id="Stroke-4" stroke="#2196F3" mask="url(#mask-5)"></path>
|
||||
</g>
|
||||
<g id="Group-9">
|
||||
<mask id="mask-7" fill="white">
|
||||
<use xlink:href="#path-6"></use>
|
||||
</mask>
|
||||
<g id="Clip-8"></g>
|
||||
<path d="M6.5378963,2.45802941 C6.53582222,2.89882353 6.65715556,3.24635294 7.05434074,2.85229412 C7.45152593,2.45802941 7.68962963,1.98491176 7.96797037,2.18194118 C8.2458963,2.37917647 8.72251852,2.32667647 8.9208,2.3135 C9.1197037,2.30032353 9.15931852,1.70882353 9.71537778,1.78767647 C10.271437,1.86652941 10.0729481,1.6695 10.6686222,1.35388235 C11.2645037,1.03847059 12.5354963,0.486705882 11.2246815,0.644411765 C9.91386667,0.802117647 9.83442963,0.447176471 8.9208,1.03847059 C8.00758519,1.62997059 7.8090963,1.62997059 7.57078519,1.78767647 C7.33247407,1.94538235 7.05434074,1.31455882 6.73659259,1.78767647 C6.41863704,2.26079412 6.54017778,2.00879412 6.5378963,2.45802941" id="Fill-7" fill="#2196F3" mask="url(#mask-7)"></path>
|
||||
</g>
|
||||
<g id="Group-12">
|
||||
<mask id="mask-9" fill="white">
|
||||
<use xlink:href="#path-8"></use>
|
||||
</mask>
|
||||
<g id="Clip-11"></g>
|
||||
<path d="M6.5378963,2.45802941 C6.53582222,2.89882353 6.65715556,3.24635294 7.05434074,2.85229412 C7.45152593,2.45802941 7.68962963,1.98491176 7.96797037,2.18194118 C8.2458963,2.37917647 8.72251852,2.32667647 8.9208,2.3135 C9.1197037,2.30032353 9.15931852,1.70882353 9.71537778,1.78767647 C10.271437,1.86652941 10.0729481,1.6695 10.6686222,1.35388235 C11.2645037,1.03847059 12.5354963,0.486705882 11.2246815,0.644411765 C9.91386667,0.802117647 9.83442963,0.447176471 8.9208,1.03847059 C8.00758519,1.62997059 7.8090963,1.62997059 7.57078519,1.78767647 C7.33247407,1.94538235 7.05434074,1.31455882 6.73659259,1.78767647 C6.41863704,2.26079412 6.54017778,2.00879412 6.5378963,2.45802941 Z" id="Stroke-10" stroke="#2196F3" mask="url(#mask-9)"></path>
|
||||
</g>
|
||||
<g id="Group-15">
|
||||
<mask id="mask-11" fill="white">
|
||||
<use xlink:href="#path-10"></use>
|
||||
</mask>
|
||||
<g id="Clip-14"></g>
|
||||
<path d="M11.3040148,1.23570588 C10.8346519,1.63841176 10.1126667,2.22147059 10.6685185,2.22147059 C11.2245778,2.22147059 11.4946222,1.93241176 11.7966074,2.3135 C12.0985926,2.69438235 11.9791259,3.28588235 11.9791259,3.56176471 C11.9791259,3.83785294 11.8202519,4.46888235 12.2176444,5.25720588 C12.6148296,6.04552941 12.6544444,6.55817647 13.051837,6.04552941 C13.4488148,5.53329412 14.4020593,4.54752941 14.8786815,4.46888235 C15.3553037,4.38982353 15.8323407,4.19258824 16.0704444,3.79832353 C16.3085481,3.40405882 16.0704444,3.02152941 16.4674222,2.73988235 C16.8644,2.45802941 16.784963,2.10308824 16.9044296,1.6695 C17.0232741,1.23570588 18.0959852,1.23570588 17.6191556,0.999352941 C17.1427407,0.762588235 16.2689333,0.604882353 15.2360444,0.723264706 C14.203363,0.841441176 12.1778222,0.486705882 11.3040148,1.23570588" id="Fill-13" fill="#2196F3" mask="url(#mask-11)"></path>
|
||||
</g>
|
||||
<g id="Group-18">
|
||||
<mask id="mask-13" fill="white">
|
||||
<use xlink:href="#path-12"></use>
|
||||
</mask>
|
||||
<g id="Clip-17"></g>
|
||||
<path d="M11.3040148,1.23570588 C10.8346519,1.63841176 10.1126667,2.22147059 10.6685185,2.22147059 C11.2245778,2.22147059 11.4946222,1.93241176 11.7966074,2.3135 C12.0985926,2.69438235 11.9791259,3.28588235 11.9791259,3.56176471 C11.9791259,3.83785294 11.8202519,4.46888235 12.2176444,5.25720588 C12.6148296,6.04552941 12.6544444,6.55817647 13.051837,6.04552941 C13.4488148,5.53329412 14.4020593,4.54752941 14.8786815,4.46888235 C15.3553037,4.38982353 15.8323407,4.19258824 16.0704444,3.79832353 C16.3085481,3.40405882 16.0704444,3.02152941 16.4674222,2.73988235 C16.8644,2.45802941 16.784963,2.10308824 16.9044296,1.6695 C17.0232741,1.23570588 18.0959852,1.23570588 17.6191556,0.999352941 C17.1427407,0.762588235 16.2689333,0.604882353 15.2360444,0.723264706 C14.203363,0.841441176 12.1778222,0.486705882 11.3040148,1.23570588 Z" id="Stroke-16" stroke="#2196F3" mask="url(#mask-13)"></path>
|
||||
</g>
|
||||
<g id="Group-21">
|
||||
<mask id="mask-15" fill="white">
|
||||
<use xlink:href="#path-14"></use>
|
||||
</mask>
|
||||
<g id="Clip-20"></g>
|
||||
<path d="M6.93518519,13.4181353 C6.68256296,13.8270176 6.81592593,13.8519294 7.09405926,13.9307824 C7.37198519,14.0096353 6.9748,14.4825471 7.29254815,14.5220765 C7.6102963,14.5618118 8.32522963,14.7588412 8.52371852,14.5220765 C8.72262222,14.2857235 8.20617778,14.3779588 7.84860741,14.1150471 C7.49145185,13.8519294 7.11459259,13.1278412 6.93518519,13.4181353" id="Fill-19" fill="#2196F3" mask="url(#mask-15)"></path>
|
||||
</g>
|
||||
<g id="Group-24">
|
||||
<mask id="mask-17" fill="white">
|
||||
<use xlink:href="#path-16"></use>
|
||||
</mask>
|
||||
<g id="Clip-23"></g>
|
||||
<path d="M6.93518519,13.4181353 C6.68256296,13.8270176 6.81592593,13.8519294 7.09405926,13.9307824 C7.37198519,14.0096353 6.9748,14.4825471 7.29254815,14.5220765 C7.6102963,14.5618118 8.32522963,14.7588412 8.52371852,14.5220765 C8.72262222,14.2857235 8.20617778,14.3779588 7.84860741,14.1150471 C7.49145185,13.8519294 7.11459259,13.1278412 6.93518519,13.4181353 Z" id="Stroke-22" stroke="#2196F3" mask="url(#mask-17)"></path>
|
||||
</g>
|
||||
<g id="Group-27">
|
||||
<mask id="mask-19" fill="white">
|
||||
<use xlink:href="#path-18"></use>
|
||||
</mask>
|
||||
<g id="Clip-26"></g>
|
||||
<path d="M26.1584504,19.8445265 C25.7305689,20.0572029 25.9203467,20.3967029 25.5229541,20.6332618 C25.1257689,20.8696147 25.284643,20.9484676 25.1655911,21.5006441 C25.0463319,22.0524088 25.2052059,22.3678206 25.4833393,21.9342324 C25.7614726,21.5006441 26.6350726,19.6081735 26.1584504,19.8445265" id="Fill-25" fill="#2196F3" mask="url(#mask-19)"></path>
|
||||
</g>
|
||||
<g id="Group-30">
|
||||
<mask id="mask-21" fill="white">
|
||||
<use xlink:href="#path-20"></use>
|
||||
</mask>
|
||||
<g id="Clip-29"></g>
|
||||
<path d="M26.1584504,19.8445265 C25.7305689,20.0572029 25.9203467,20.3967029 25.5229541,20.6332618 C25.1257689,20.8696147 25.284643,20.9484676 25.1655911,21.5006441 C25.0463319,22.0524088 25.2052059,22.3678206 25.4833393,21.9342324 C25.7614726,21.5006441 26.6350726,19.6081735 26.1584504,19.8445265 Z" id="Stroke-28" stroke="#2196F3" mask="url(#mask-21)"></path>
|
||||
</g>
|
||||
<g id="Group-33">
|
||||
<mask id="mask-23" fill="white">
|
||||
<use xlink:href="#path-22"></use>
|
||||
</mask>
|
||||
<g id="Clip-32"></g>
|
||||
<path d="M26.5159378,11.4075706 C26.3966785,11.5259529 26.1981896,10.8556 26.0791378,10.5401882 C25.9598785,10.2247765 25.9627822,9.67651176 26.29816,9.37180588 C26.7158785,8.99174706 26.5159378,9.86983529 26.5159378,10.3431588 C26.5159378,10.8160706 26.6349896,11.2891882 26.5159378,11.4075706 M25.1254785,10.1854529 C24.8473452,10.0277471 24.331523,10.1854529 23.6960267,10.3824824 C23.060323,10.5797176 23.5688859,9.94374706 23.8549007,9.5151 C24.1106341,9.13195294 23.9739526,9.63327647 24.5302193,9.67280588 C25.0860711,9.71233529 25.4040267,10.3431588 25.1254785,10.1854529 M24.2124711,11.5259529 C24.1726489,11.8411588 24.331523,12.3538059 23.4577156,12.1567765 C22.5839081,11.9595412 22.5839081,11.4864235 22.2265452,11.8411588 C21.8691822,12.1961 21.4321748,12.5113059 20.9157304,11.9595412 C20.3994933,11.4075706 20.4789304,11.2498647 20.2010044,11.2103353 C20.0029304,11.1825412 19.3427526,11.0361588 18.7922933,11.1951 C18.8350193,11.1697765 18.8806489,11.1364235 18.9298044,11.0921588 C19.3269896,10.7374235 19.2475526,10.6190412 19.6843526,10.2247765 C20.1215674,9.83030588 20.4391081,9.47577647 20.7568563,9.83030588 C21.0746044,10.1854529 21.749923,10.2643059 21.5116119,10.6585706 C21.2733007,11.0526294 21.4321748,11.2891882 21.7103081,11.0526294 C21.9880267,10.8160706 22.3854193,10.6585706 22.1074933,10.3431588 C21.8291526,10.0277471 21.5510193,9.19968824 21.9486193,9.75165882 C22.3458044,10.3036294 22.4648563,10.5797176 22.70296,10.8949235 C22.9414785,11.2103353 23.1797896,11.4075706 23.2592267,11.2103353 C23.3386637,11.0133059 23.2194044,10.5797176 23.3386637,10.5008647 C23.4577156,10.4220118 23.3386637,11.1710118 23.6960267,11.2103353 C24.0533896,11.2498647 24.2520859,11.2103353 24.2124711,11.5259529 M28.4224267,13.2213941 C28.3033748,12.5508353 27.4295674,6.47936471 27.4295674,6.47936471 C27.4295674,6.47936471 25.0860711,4.98115882 24.6890933,5.29636471 C24.2917007,5.61198235 23.6165896,5.56442353 23.6960267,5.25312941 C23.7754637,4.94183529 24.2917007,5.02089412 23.4176859,4.42918824 C22.5442933,3.83768824 22.3854193,3.32524706 21.7103081,4.07424706 C21.0347822,4.82345294 20.8761156,5.45427647 20.4789304,5.65151176 C20.0817452,5.84854118 19.763997,6.47936471 20.2010044,6.63707059 C20.6378044,6.79457059 20.6776267,6.63707059 21.0347822,6.95227647 C21.39256,7.26789412 21.8691822,7.18883529 21.9486193,6.99201176 C22.0280563,6.79457059 22.1074933,6.16395294 22.1471081,5.92739412 C22.1869304,5.69083529 22.5905452,4.90230588 22.5474044,5.53312941 C22.5044711,6.16395294 23.1001452,6.16395294 22.70296,6.59754118 C22.3059822,7.03112941 22.5442933,7.38627647 21.749923,7.46512941 C20.9553452,7.54377647 20.9157304,7.07086471 20.4789304,7.30742353 C20.041923,7.54377647 20.0817452,7.70168824 19.9228711,7.93804118 C19.763997,8.1746 18.9298044,8.64771765 18.9298044,8.64771765 C18.9298044,8.64771765 18.2546933,8.41115882 18.5722341,8.84495294 C18.8901896,9.27854118 19.1681156,9.79098235 18.8901896,9.83030588 C18.6120563,9.86983529 17.8573007,9.43624706 17.8573007,9.90936471 C17.8573007,10.3824824 17.7384563,10.8949235 18.0562044,11.0133059 C18.2804119,11.0966882 18.4260119,11.2776588 18.63176,11.2523353 C18.4722637,11.3206882 18.3272859,11.4197176 18.2150785,11.5652765 C17.9058341,11.9661294 18.0958193,12.6692176 17.460323,12.9057765 C16.8250341,13.1425412 16.8250341,13.4182176 16.7055674,13.9308647 C16.586723,14.4433059 16.7055674,15.7442765 16.8644415,15.8626588 C17.0231081,15.9808353 17.3014489,16.9666 17.9767674,16.8875412 C18.6520859,16.8086882 19.3668119,16.7784235 19.605123,16.8922765 C19.8434341,17.0057176 20.2404119,16.6511882 20.3596711,17.5578941 C20.4789304,18.4646 20.7568563,18.5829824 20.9157304,19.0561 C21.0746044,19.5290118 20.8362933,20.5149824 20.9157304,21.1062765 C20.9951674,21.6975706 21.5510193,22.7228647 21.5510193,23.2353059 C21.5510193,23.7477471 22.1471081,23.9843059 22.4648563,23.7872765 C22.7826044,23.5900412 23.4577156,23.1957765 23.7356415,22.5649529 C24.0139822,21.9343353 24.0139822,21.6189235 24.4505748,20.7908647 C24.8873748,19.9630118 24.7685304,19.3715118 24.9668119,18.7406882 C25.1655081,18.1098647 25.6433748,17.6760706 26.29816,16.9666 C26.9527378,16.2571294 26.8349304,15.8626588 26.29816,15.9019824 C25.7613896,15.9413059 25.4034044,15.4290706 25.1254785,14.7982471 C24.8479674,14.1672176 24.5302193,12.5903647 24.7685304,13.1028059 C25.0068415,13.6156588 25.4832563,15.0348059 25.9598785,15.2713647 C26.4365007,15.5079235 27.3105156,14.8692765 27.5090044,14.6366294 C27.7074933,14.4037765 27.6280563,13.7338353 27.1116119,13.4577471 C26.5953748,13.1816588 26.913123,12.9453059 27.4295674,13.2605118 C27.9458044,13.5759235 28.5416859,13.8913353 28.4224267,13.2213941" id="Fill-31" fill="#2196F3" mask="url(#mask-23)"></path>
|
||||
</g>
|
||||
<g id="Group-36">
|
||||
<mask id="mask-25" fill="white">
|
||||
<use xlink:href="#path-24"></use>
|
||||
</mask>
|
||||
<g id="Clip-35"></g>
|
||||
<path d="M26.5159378,11.4075706 C26.3966785,11.5259529 26.1981896,10.8556 26.0791378,10.5401882 C25.9598785,10.2247765 25.9627822,9.67651176 26.29816,9.37180588 C26.7158785,8.99174706 26.5159378,9.86983529 26.5159378,10.3431588 C26.5159378,10.8160706 26.6349896,11.2891882 26.5159378,11.4075706 Z M25.1254785,10.1854529 C24.8473452,10.0277471 24.331523,10.1854529 23.6960267,10.3824824 C23.060323,10.5797176 23.5688859,9.94374706 23.8549007,9.5151 C24.1106341,9.13195294 23.9739526,9.63327647 24.5302193,9.67280588 C25.0860711,9.71233529 25.4040267,10.3431588 25.1254785,10.1854529 Z M24.2124711,11.5259529 C24.1726489,11.8411588 24.331523,12.3538059 23.4577156,12.1567765 C22.5839081,11.9595412 22.5839081,11.4864235 22.2265452,11.8411588 C21.8691822,12.1961 21.4321748,12.5113059 20.9157304,11.9595412 C20.3994933,11.4075706 20.4789304,11.2498647 20.2010044,11.2103353 C20.0029304,11.1825412 19.3427526,11.0361588 18.7922933,11.1951 C18.8350193,11.1697765 18.8806489,11.1364235 18.9298044,11.0921588 C19.3269896,10.7374235 19.2475526,10.6190412 19.6843526,10.2247765 C20.1215674,9.83030588 20.4391081,9.47577647 20.7568563,9.83030588 C21.0746044,10.1854529 21.749923,10.2643059 21.5116119,10.6585706 C21.2733007,11.0526294 21.4321748,11.2891882 21.7103081,11.0526294 C21.9880267,10.8160706 22.3854193,10.6585706 22.1074933,10.3431588 C21.8291526,10.0277471 21.5510193,9.19968824 21.9486193,9.75165882 C22.3458044,10.3036294 22.4648563,10.5797176 22.70296,10.8949235 C22.9414785,11.2103353 23.1797896,11.4075706 23.2592267,11.2103353 C23.3386637,11.0133059 23.2194044,10.5797176 23.3386637,10.5008647 C23.4577156,10.4220118 23.3386637,11.1710118 23.6960267,11.2103353 C24.0533896,11.2498647 24.2520859,11.2103353 24.2124711,11.5259529 Z M28.4224267,13.2213941 C28.3033748,12.5508353 27.4295674,6.47936471 27.4295674,6.47936471 C27.4295674,6.47936471 25.0860711,4.98115882 24.6890933,5.29636471 C24.2917007,5.61198235 23.6165896,5.56442353 23.6960267,5.25312941 C23.7754637,4.94183529 24.2917007,5.02089412 23.4176859,4.42918824 C22.5442933,3.83768824 22.3854193,3.32524706 21.7103081,4.07424706 C21.0347822,4.82345294 20.8761156,5.45427647 20.4789304,5.65151176 C20.0817452,5.84854118 19.763997,6.47936471 20.2010044,6.63707059 C20.6378044,6.79457059 20.6776267,6.63707059 21.0347822,6.95227647 C21.39256,7.26789412 21.8691822,7.18883529 21.9486193,6.99201176 C22.0280563,6.79457059 22.1074933,6.16395294 22.1471081,5.92739412 C22.1869304,5.69083529 22.5905452,4.90230588 22.5474044,5.53312941 C22.5044711,6.16395294 23.1001452,6.16395294 22.70296,6.59754118 C22.3059822,7.03112941 22.5442933,7.38627647 21.749923,7.46512941 C20.9553452,7.54377647 20.9157304,7.07086471 20.4789304,7.30742353 C20.041923,7.54377647 20.0817452,7.70168824 19.9228711,7.93804118 C19.763997,8.1746 18.9298044,8.64771765 18.9298044,8.64771765 C18.9298044,8.64771765 18.2546933,8.41115882 18.5722341,8.84495294 C18.8901896,9.27854118 19.1681156,9.79098235 18.8901896,9.83030588 C18.6120563,9.86983529 17.8573007,9.43624706 17.8573007,9.90936471 C17.8573007,10.3824824 17.7384563,10.8949235 18.0562044,11.0133059 C18.2804119,11.0966882 18.4260119,11.2776588 18.63176,11.2523353 C18.4722637,11.3206882 18.3272859,11.4197176 18.2150785,11.5652765 C17.9058341,11.9661294 18.0958193,12.6692176 17.460323,12.9057765 C16.8250341,13.1425412 16.8250341,13.4182176 16.7055674,13.9308647 C16.586723,14.4433059 16.7055674,15.7442765 16.8644415,15.8626588 C17.0231081,15.9808353 17.3014489,16.9666 17.9767674,16.8875412 C18.6520859,16.8086882 19.3668119,16.7784235 19.605123,16.8922765 C19.8434341,17.0057176 20.2404119,16.6511882 20.3596711,17.5578941 C20.4789304,18.4646 20.7568563,18.5829824 20.9157304,19.0561 C21.0746044,19.5290118 20.8362933,20.5149824 20.9157304,21.1062765 C20.9951674,21.6975706 21.5510193,22.7228647 21.5510193,23.2353059 C21.5510193,23.7477471 22.1471081,23.9843059 22.4648563,23.7872765 C22.7826044,23.5900412 23.4577156,23.1957765 23.7356415,22.5649529 C24.0139822,21.9343353 24.0139822,21.6189235 24.4505748,20.7908647 C24.8873748,19.9630118 24.7685304,19.3715118 24.9668119,18.7406882 C25.1655081,18.1098647 25.6433748,17.6760706 26.29816,16.9666 C26.9527378,16.2571294 26.8349304,15.8626588 26.29816,15.9019824 C25.7613896,15.9413059 25.4034044,15.4290706 25.1254785,14.7982471 C24.8479674,14.1672176 24.5302193,12.5903647 24.7685304,13.1028059 C25.0068415,13.6156588 25.4832563,15.0348059 25.9598785,15.2713647 C26.4365007,15.5079235 27.3105156,14.8692765 27.5090044,14.6366294 C27.7074933,14.4037765 27.6280563,13.7338353 27.1116119,13.4577471 C26.5953748,13.1816588 26.913123,12.9453059 27.4295674,13.2605118 C27.9458044,13.5759235 28.5416859,13.8913353 28.4224267,13.2213941 Z" id="Stroke-34" stroke="#2196F3" mask="url(#mask-25)"></path>
|
||||
</g>
|
||||
<g id="Group-39">
|
||||
<mask id="mask-27" fill="white">
|
||||
<use xlink:href="#path-26"></use>
|
||||
</mask>
|
||||
<g id="Clip-38"></g>
|
||||
<path d="M18.8503674,6.59758235 C18.6070785,6.67025882 18.4135674,7.14955294 18.5326193,7.38631765 C18.6520859,7.62267059 18.4135674,7.26772941 18.0958193,7.46496471 C17.7778637,7.66199412 17.3060119,8.21417059 17.7405304,8.25328824 C18.1752563,8.29302353 18.0958193,7.93808235 18.4930044,8.09578824 C18.8901896,8.25328824 19.3269896,8.21417059 19.3269896,7.81990588 C19.3269896,7.42543529 19.2475526,6.47940588 18.8503674,6.59758235" id="Fill-37" fill="#2196F3" mask="url(#mask-27)"></path>
|
||||
</g>
|
||||
<g id="Group-42">
|
||||
<mask id="mask-29" fill="white">
|
||||
<use xlink:href="#path-28"></use>
|
||||
</mask>
|
||||
<g id="Clip-41"></g>
|
||||
<path d="M18.8503674,6.59758235 C18.6070785,6.67025882 18.4135674,7.14955294 18.5326193,7.38631765 C18.6520859,7.62267059 18.4135674,7.26772941 18.0958193,7.46496471 C17.7778637,7.66199412 17.3060119,8.21417059 17.7405304,8.25328824 C18.1752563,8.29302353 18.0958193,7.93808235 18.4930044,8.09578824 C18.8901896,8.25328824 19.3269896,8.21417059 19.3269896,7.81990588 C19.3269896,7.42543529 19.2475526,6.47940588 18.8503674,6.59758235 Z" id="Stroke-40" stroke="#2196F3" mask="url(#mask-29)"></path>
|
||||
</g>
|
||||
<g id="Group-45">
|
||||
<mask id="mask-31" fill="white">
|
||||
<use xlink:href="#path-30"></use>
|
||||
</mask>
|
||||
<g id="Clip-44"></g>
|
||||
<path d="M16.2689333,4.78404706 C16.0295852,4.81801765 15.9509778,5.41487059 16.4674222,5.49372353 C16.9838667,5.57257647 17.9082815,5.37534118 17.5652296,5.05992941 C17.2221778,4.74472353 16.8252,4.70519412 16.2689333,4.78404706" id="Fill-43" fill="#2196F3" mask="url(#mask-31)"></path>
|
||||
</g>
|
||||
<g id="Group-48">
|
||||
<mask id="mask-33" fill="white">
|
||||
<use xlink:href="#path-32"></use>
|
||||
</mask>
|
||||
<g id="Clip-47"></g>
|
||||
<path d="M16.2689333,4.78404706 C16.0295852,4.81801765 15.9509778,5.41487059 16.4674222,5.49372353 C16.9838667,5.57257647 17.9082815,5.37534118 17.5652296,5.05992941 C17.2221778,4.74472353 16.8252,4.70519412 16.2689333,4.78404706 Z" id="Stroke-46" stroke="#2196F3" mask="url(#mask-33)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-54">
|
||||
<path d="M28.1296528,14.2858085 C28.1296528,21.8560338 21.992681,27.9932099 14.4222514,27.9932099 C6.85182183,27.9932099 0.71485,21.8560338 0.71485,14.2858085 C0.71485,6.71537887 6.85182183,0.578407042 14.4222514,0.578407042 C21.992681,0.578407042 28.1296528,6.71537887 28.1296528,14.2858085 Z" id="Stroke-50" stroke="#2196F3"></path>
|
||||
<path d="M14.4854387,1.69603028 C7.4461993,1.69603028 1.71931197,7.42291761 1.71931197,14.462157 C1.71931197,21.5013965 7.4461993,27.2280796 14.4854387,27.2280796 C21.5246782,27.2280796 27.2513613,21.5013965 27.2513613,14.462157 C27.2513613,7.42291761 21.5246782,1.69603028 14.4854387,1.69603028 M14.4854387,28.8618824 C6.54536127,28.8618824 0.0855091549,22.4022345 0.0855091549,14.462157 C0.0855091549,6.52187535 6.54536127,0.0622274648 14.4854387,0.0622274648 C22.4255162,0.0622274648 28.8851641,6.52187535 28.8851641,14.462157 C28.8851641,22.4022345 22.4255162,28.8618824 14.4854387,28.8618824" id="Fill-52" fill="#2196F3"></path>
|
||||
</g>
|
||||
<g id="Group-64" transform="translate(16.000000, 0.000000)">
|
||||
<g id="Group-57">
|
||||
<mask id="mask-35" fill="white">
|
||||
<use xlink:href="#path-34"></use>
|
||||
</mask>
|
||||
<g id="Clip-56"></g>
|
||||
<path d="M7.92792,7.13868 C7.61952,7.44688 7.25072,7.60088 6.82132,7.60088 C6.39172,7.60088 6.02312,7.44688 5.71492,7.13868 C5.40672,6.83048 5.25232,6.46128 5.25232,6.03208 C5.25232,5.60268 5.40672,5.23948 5.71492,4.94208 C6.02312,4.64488 6.39172,4.49608 6.82132,4.49608 C7.25072,4.49608 7.61952,4.64488 7.92792,4.94208 C8.23592,5.23948 8.39032,5.60268 8.39032,6.03208 C8.39032,6.46128 8.23592,6.83048 7.92792,7.13868 M10.43812,2.53088 C9.44732,1.55088 8.24732,1.06088 6.83792,1.06088 C5.42852,1.06088 4.22852,1.55088 3.23752,2.53088 C2.24652,3.51048 1.75112,4.69428 1.75112,6.08168 C1.75112,6.91828 2.17492,8.31688 3.02292,10.27648 C3.87052,12.23628 4.71832,14.01988 5.56592,15.62728 C6.41412,17.23468 6.83792,18.01648 6.83792,17.97268 C6.96992,17.73048 7.15152,17.40008 7.38272,16.98168 C7.61412,16.56328 8.02152,15.77608 8.60512,14.61988 C9.18852,13.46368 9.70052,12.40108 10.14052,11.43288 C10.58132,10.46368 10.98872,9.46168 11.36292,8.42648 C11.73752,7.39188 11.92432,6.61008 11.92432,6.08168 C11.92432,4.69428 11.42892,3.51048 10.43812,2.53088" id="Fill-55" fill="#FFFFFF" mask="url(#mask-35)"></path>
|
||||
</g>
|
||||
<g id="Group-60">
|
||||
<mask id="mask-37" fill="white">
|
||||
<use xlink:href="#path-36"></use>
|
||||
</mask>
|
||||
<g id="Clip-59"></g>
|
||||
<path d="M7.92792,7.13868 C7.61952,7.44688 7.25072,7.60088 6.82132,7.60088 C6.39172,7.60088 6.02312,7.44688 5.71492,7.13868 C5.40672,6.83048 5.25232,6.46128 5.25232,6.03208 C5.25232,5.60268 5.40672,5.23948 5.71492,4.94208 C6.02312,4.64488 6.39172,4.49608 6.82132,4.49608 C7.25072,4.49608 7.61952,4.64488 7.92792,4.94208 C8.23592,5.23948 8.39032,5.60268 8.39032,6.03208 C8.39032,6.46128 8.23592,6.83048 7.92792,7.13868 M10.43812,2.53088 C9.44732,1.55088 8.24732,1.06088 6.83792,1.06088 C5.42852,1.06088 4.22852,1.55088 3.23752,2.53088 C2.24652,3.51048 1.75112,4.69428 1.75112,6.08168 C1.75112,6.91828 2.17492,8.31688 3.02292,10.27648 C3.87052,12.23628 4.71832,14.01988 5.56592,15.62728 C6.41412,17.23468 6.83792,18.01648 6.83792,17.97268 C6.96992,17.73048 7.15152,17.40008 7.38272,16.98168 C7.61412,16.56328 8.02152,15.77608 8.60512,14.61988 C9.18852,13.46368 9.70052,12.40108 10.14052,11.43288 C10.58132,10.46368 10.98872,9.46168 11.36292,8.42648 C11.73752,7.39188 11.92432,6.61008 11.92432,6.08168 C11.92432,4.69428 11.42892,3.51048 10.43812,2.53088" id="Fill-58" fill="#FFFFFF" mask="url(#mask-37)"></path>
|
||||
</g>
|
||||
<g id="Group-63">
|
||||
<mask id="mask-39" fill="white">
|
||||
<use xlink:href="#path-38"></use>
|
||||
</mask>
|
||||
<g id="Clip-62"></g>
|
||||
<path d="M6.82128,4.9961 C6.51968,4.9961 6.27828,5.0933 6.06188,5.3021 C5.85068,5.5057 5.75228,5.7377 5.75228,6.0321 C5.75228,6.3301 5.85268,6.5695 6.06828,6.7851 C6.28368,7.0005 6.52308,7.1009 6.82128,7.1009 C7.11948,7.1009 7.35888,7.0005 7.57448,6.7849 C7.78988,6.5695 7.89028,6.3301 7.89028,6.0321 C7.89028,5.7377 7.79188,5.5057 7.58068,5.3017 C7.36428,5.0933 7.12288,4.9961 6.82128,4.9961 L6.82128,4.9961 Z M6.82128,8.1009 C6.25648,8.1009 5.76528,7.8961 5.36128,7.4921 C4.95728,7.0881 4.75228,6.5969 4.75228,6.0321 C4.75228,5.4639 4.95928,4.9761 5.36748,4.5823 C5.77108,4.1933 6.26008,3.9961 6.82128,3.9961 C7.38228,3.9961 7.87128,4.1933 8.27488,4.5821 C8.68348,4.9763 8.89028,5.4641 8.89028,6.0321 C8.89028,6.5967 8.68548,7.0879 8.28168,7.4921 C7.87728,7.8961 7.38608,8.1009 6.82128,8.1009 L6.82128,8.1009 Z M6.83788,1.5609 C5.55368,1.5609 4.49108,1.9945 3.58908,2.8865 C2.68888,3.7763 2.25108,4.8215 2.25108,6.0817 C2.25108,6.6073 2.46468,7.7273 3.48188,10.0779 C4.31968,12.0155 5.16968,13.8041 6.00828,15.3941 C6.38528,16.1089 6.64868,16.6035 6.83208,16.9447 C6.86828,16.8789 6.90608,16.8105 6.94528,16.7399 C7.17208,16.3295 7.58028,15.5405 8.15888,14.3947 C8.73768,13.2469 9.25128,12.1809 9.68528,11.2259 C10.11888,10.2727 10.52508,9.2735 10.89248,8.2565 C11.33228,7.0427 11.42448,6.4039 11.42448,6.0817 C11.42448,4.8215 10.98668,3.7765 10.08648,2.8865 C9.18468,1.9945 8.12208,1.5609 6.83788,1.5609 L6.83788,1.5609 Z M7.04328,18.4751 L6.83508,18.4745 C6.52208,18.4733 6.46988,18.3767 6.06328,17.6257 C5.85068,17.2335 5.53768,16.6451 5.12388,15.8607 C4.27328,14.2479 3.41208,12.4359 2.56388,10.4749 C1.68068,8.4333 1.25108,6.9963 1.25108,6.0817 C1.25108,4.5619 1.80108,3.2475 2.88588,2.1753 C3.96928,1.1041 5.29908,0.5609 6.83788,0.5609 C8.37688,0.5609 9.70648,1.1041 10.78968,2.1753 C11.87448,3.2477 12.42448,4.5621 12.42448,6.0817 C12.42448,6.6751 12.23108,7.4977 11.83308,8.5967 C11.45648,9.6383 11.04028,10.6623 10.59568,11.6399 C10.15628,12.6071 9.63668,13.6855 9.05148,14.8451 C8.45908,16.0185 8.05648,16.7965 7.82048,17.2235 C7.62348,17.5797 7.46268,17.8721 7.33788,18.1003 L7.33788,18.1803 L7.04328,18.4751 Z" id="Fill-61" fill="#2196F3" mask="url(#mask-39)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Translation-Manageme" transform="translate(40.000000, 6.000000)" fill-rule="nonzero">
|
||||
<path d="M2.688,9.94 L2.688,1.946 L1.42108547e-14,1.946 L1.42108547e-14,0.504 L7.126,0.504 L7.126,1.946 L4.41,1.946 L4.41,9.94 L2.688,9.94 Z M7.462,9.94 L7.462,5.32 C7.462,4.396 7.448,3.724 7.406,3.122 L8.89,3.122 L8.96,4.41 L9.002,4.41 C9.338,3.458 10.136,2.968 10.878,2.968 C11.046,2.968 11.144,2.982 11.284,3.01 L11.284,4.62 C11.13,4.592 10.976,4.578 10.766,4.578 C9.954,4.578 9.38,5.096 9.226,5.88 C9.198,6.034 9.184,6.216 9.184,6.398 L9.184,9.94 L7.462,9.94 Z M17.612,5.852 L17.612,8.302 C17.612,8.918 17.64,9.52 17.724,9.94 L16.17,9.94 L16.044,9.184 L16.002,9.184 C15.596,9.716 14.882,10.094 13.986,10.094 C12.614,10.094 11.844,9.1 11.844,8.064 C11.844,6.356 13.37,5.474 15.89,5.488 L15.89,5.376 C15.89,4.928 15.708,4.186 14.504,4.186 C13.832,4.186 13.132,4.396 12.67,4.69 L12.334,3.57 C12.838,3.262 13.72,2.968 14.798,2.968 C16.982,2.968 17.612,4.354 17.612,5.852 Z M15.932,7.546 L15.932,6.608 C14.714,6.594 13.552,6.846 13.552,7.882 C13.552,8.554 13.986,8.862 14.532,8.862 C15.232,8.862 15.722,8.414 15.89,7.924 C15.932,7.798 15.932,7.672 15.932,7.546 Z M19.432,9.94 L19.432,5.152 C19.432,4.368 19.404,3.71 19.376,3.122 L20.874,3.122 L20.958,4.144 L21,4.144 C21.308,3.612 22.05,2.968 23.184,2.968 C24.374,2.968 25.606,3.738 25.606,5.894 L25.606,9.94 L23.884,9.94 L23.884,6.09 C23.884,5.11 23.52,4.368 22.582,4.368 C21.896,4.368 21.42,4.858 21.238,5.376 C21.182,5.516 21.154,5.726 21.154,5.922 L21.154,9.94 L19.432,9.94 Z M27.02,9.604 L27.37,8.344 C27.748,8.582 28.49,8.848 29.106,8.848 C29.862,8.848 30.198,8.526 30.198,8.078 C30.198,7.616 29.918,7.392 29.078,7.084 C27.748,6.636 27.202,5.908 27.202,5.096 C27.202,3.892 28.196,2.968 29.778,2.968 C30.534,2.968 31.192,3.15 31.598,3.374 L31.248,4.578 C30.968,4.41 30.408,4.186 29.806,4.186 C29.19,4.186 28.854,4.494 28.854,4.914 C28.854,5.348 29.176,5.544 30.044,5.866 C31.29,6.3 31.85,6.93 31.864,7.938 C31.864,9.184 30.898,10.08 29.092,10.08 C28.266,10.08 27.524,9.884 27.02,9.604 Z M33.278,9.94 L33.278,-7.10542736e-15 L35,-7.10542736e-15 L35,9.94 L33.278,9.94 Z M42.168,5.852 L42.168,8.302 C42.168,8.918 42.196,9.52 42.28,9.94 L40.726,9.94 L40.6,9.184 L40.558,9.184 C40.152,9.716 39.438,10.094 38.542,10.094 C37.17,10.094 36.4,9.1 36.4,8.064 C36.4,6.356 37.926,5.474 40.446,5.488 L40.446,5.376 C40.446,4.928 40.264,4.186 39.06,4.186 C38.388,4.186 37.688,4.396 37.226,4.69 L36.89,3.57 C37.394,3.262 38.276,2.968 39.354,2.968 C41.538,2.968 42.168,4.354 42.168,5.852 Z M40.488,7.546 L40.488,6.608 C39.27,6.594 38.108,6.846 38.108,7.882 C38.108,8.554 38.542,8.862 39.088,8.862 C39.788,8.862 40.278,8.414 40.446,7.924 C40.488,7.798 40.488,7.672 40.488,7.546 Z M44.184,1.848 L45.878,1.372 L45.878,3.122 L47.516,3.122 L47.516,4.41 L45.878,4.41 L45.878,7.42 C45.878,8.26 46.102,8.68 46.76,8.68 C47.068,8.68 47.236,8.666 47.432,8.61 L47.46,9.912 C47.208,10.01 46.746,10.094 46.214,10.094 C45.57,10.094 45.052,9.884 44.73,9.534 C44.352,9.142 44.184,8.512 44.184,7.616 L44.184,4.41 L43.218,4.41 L43.218,3.122 L44.184,3.122 L44.184,1.848 Z M50.54,9.94 L48.818,9.94 L48.818,3.122 L50.54,3.122 L50.54,9.94 Z M49.686,0.28 C50.274,0.28 50.638,0.686 50.652,1.218 C50.652,1.722 50.274,2.142 49.672,2.142 C49.098,2.142 48.72,1.722 48.72,1.218 C48.72,0.686 49.112,0.28 49.686,0.28 Z M55.496,2.968 C57.526,2.968 58.884,4.396 58.884,6.468 C58.884,8.974 57.12,10.094 55.384,10.094 C53.466,10.094 51.968,8.778 51.968,6.58 C51.968,4.34 53.438,2.968 55.496,2.968 Z M55.454,4.214 C54.236,4.214 53.746,5.404 53.746,6.538 C53.746,7.854 54.404,8.848 55.44,8.848 C56.42,8.848 57.106,7.896 57.106,6.51 C57.106,5.446 56.63,4.214 55.454,4.214 Z M60.298,9.94 L60.298,5.152 C60.298,4.368 60.27,3.71 60.242,3.122 L61.74,3.122 L61.824,4.144 L61.866,4.144 C62.174,3.612 62.916,2.968 64.05,2.968 C65.24,2.968 66.472,3.738 66.472,5.894 L66.472,9.94 L64.75,9.94 L64.75,6.09 C64.75,5.11 64.386,4.368 63.448,4.368 C62.762,4.368 62.286,4.858 62.104,5.376 C62.048,5.516 62.02,5.726 62.02,5.922 L62.02,9.94 L60.298,9.94 Z M79.422,9.94 L79.24,6.09 C79.184,4.872 79.114,3.402 79.128,2.128 L79.086,2.128 C78.778,3.276 78.386,4.55 77.966,5.74 L76.58,9.828 L75.264,9.828 L74.004,5.796 C73.64,4.592 73.304,3.304 73.052,2.128 L73.024,2.128 C72.982,3.36 72.926,4.858 72.856,6.146 L72.646,9.94 L71.036,9.94 L71.666,0.504 L73.934,0.504 L75.166,4.298 C75.502,5.404 75.796,6.524 76.034,7.574 L76.076,7.574 C76.328,6.552 76.65,5.39 77.014,4.284 L78.316,0.504 L80.556,0.504 L81.102,9.94 L79.422,9.94 Z M88.13,5.852 L88.13,8.302 C88.13,8.918 88.158,9.52 88.242,9.94 L86.688,9.94 L86.562,9.184 L86.52,9.184 C86.114,9.716 85.4,10.094 84.504,10.094 C83.132,10.094 82.362,9.1 82.362,8.064 C82.362,6.356 83.888,5.474 86.408,5.488 L86.408,5.376 C86.408,4.928 86.226,4.186 85.022,4.186 C84.35,4.186 83.65,4.396 83.188,4.69 L82.852,3.57 C83.356,3.262 84.238,2.968 85.316,2.968 C87.5,2.968 88.13,4.354 88.13,5.852 Z M86.45,7.546 L86.45,6.608 C85.232,6.594 84.07,6.846 84.07,7.882 C84.07,8.554 84.504,8.862 85.05,8.862 C85.75,8.862 86.24,8.414 86.408,7.924 C86.45,7.798 86.45,7.672 86.45,7.546 Z M89Line truncated
|
||||
<path d="M0.91,27.712 L0.91,20.376 C0.91,19.508 0.882,18.808 0.854,18.164 L1.946,18.164 L2.016,19.326 L2.044,19.326 C2.534,18.5 3.346,18.024 4.452,18.024 C6.104,18.024 7.336,19.41 7.336,21.454 C7.336,23.89 5.838,25.094 4.242,25.094 C3.346,25.094 2.562,24.702 2.156,24.03 L2.128,24.03 L2.128,27.712 L0.91,27.712 Z M2.128,21.006 L2.128,22.182 C2.128,22.364 2.142,22.532 2.184,22.686 C2.408,23.54 3.15,24.128 4.032,24.128 C5.334,24.128 6.09,23.064 6.09,21.51 C6.09,20.166 5.376,19.004 4.074,19.004 C3.234,19.004 2.436,19.592 2.212,20.516 C2.17,20.67 2.128,20.852 2.128,21.006 Z M11.746,18.024 C13.692,18.024 15.008,19.438 15.008,21.496 C15.008,24.002 13.258,25.094 11.634,25.094 C9.814,25.094 8.386,23.75 8.386,21.608 C8.386,19.354 9.884,18.024 11.746,18.024 Z M11.718,18.948 C10.276,18.948 9.646,20.292 9.646,21.566 C9.646,23.05 10.486,24.17 11.69,24.17 C12.866,24.17 13.748,23.064 13.748,21.538 C13.748,20.39 13.174,18.948 11.718,18.948 Z M15.694,18.164 L16.954,18.164 L17.864,21.608 C18.046,22.364 18.228,23.064 18.354,23.764 L18.396,23.764 C18.55,23.078 18.774,22.35 18.998,21.622 L20.104,18.164 L21.14,18.164 L22.19,21.552 C22.442,22.364 22.638,23.078 22.792,23.764 L22.834,23.764 C22.946,23.078 23.128,22.364 23.352,21.566 L24.318,18.164 L25.536,18.164 L23.352,24.94 L22.232,24.94 L21.196,21.706 C20.958,20.95 20.762,20.278 20.594,19.48 L20.566,19.48 C20.398,20.292 20.188,20.992 19.95,21.72 L18.858,24.94 L17.738,24.94 L15.694,18.164 Z M32.074,21.776 L27.314,21.776 C27.342,23.442 28.392,24.128 29.638,24.128 C30.52,24.128 31.066,23.974 31.514,23.778 L31.738,24.66 C31.304,24.856 30.548,25.094 29.47,25.094 C27.384,25.094 26.138,23.708 26.138,21.664 C26.138,19.62 27.342,18.024 29.316,18.024 C31.542,18.024 32.116,19.956 32.116,21.202 C32.116,21.454 32.102,21.636 32.074,21.776 Z M27.328,20.894 L30.926,20.894 C30.94,20.124 30.604,18.906 29.218,18.906 C27.958,18.906 27.426,20.04 27.328,20.894 Z M33.642,24.94 L33.642,20.278 C33.642,19.48 33.628,18.794 33.586,18.164 L34.664,18.164 L34.72,19.508 L34.762,19.508 C35.07,18.598 35.826,18.024 36.652,18.024 C36.778,18.024 36.876,18.038 36.988,18.052 L36.988,19.214 C36.862,19.186 36.736,19.186 36.568,19.186 C35.7,19.186 35.084,19.83 34.916,20.754 C34.888,20.922 34.874,21.132 34.874,21.328 L34.874,24.94 L33.642,24.94 Z M43.526,21.776 L38.766,21.776 C38.794,23.442 39.844,24.128 41.09,24.128 C41.972,24.128 42.518,23.974 42.966,23.778 L43.19,24.66 C42.756,24.856 42,25.094 40.922,25.094 C38.836,25.094 37.59,23.708 37.59,21.664 C37.59,19.62 38.794,18.024 40.768,18.024 C42.994,18.024 43.568,19.956 43.568,21.202 C43.568,21.454 43.554,21.636 43.526,21.776 Z M38.78,20.894 L42.378,20.894 C42.392,20.124 42.056,18.906 40.67,18.906 C39.41,18.906 38.878,20.04 38.78,20.894 Z M49.714,15 L50.932,15 L50.932,23.19 C50.932,23.792 50.96,24.478 50.988,24.94 L49.896,24.94 L49.84,23.764 L49.798,23.764 C49.434,24.52 48.622,25.094 47.516,25.094 C45.878,25.094 44.604,23.708 44.604,21.65 C44.59,19.396 46.004,18.024 47.642,18.024 C48.692,18.024 49.378,18.514 49.686,19.046 L49.714,19.046 L49.714,15 Z M49.714,22.098 L49.714,20.922 C49.714,20.768 49.7,20.558 49.658,20.404 C49.476,19.634 48.804,18.99 47.88,18.99 C46.606,18.99 45.85,20.11 45.85,21.594 C45.85,22.966 46.536,24.1 47.852,24.1 C48.678,24.1 49.434,23.54 49.658,22.63 C49.7,22.462 49.714,22.294 49.714,22.098 Z M55.958,23.19 L55.958,15 L57.176,15 L57.176,19.256 L57.204,19.256 C57.638,18.5 58.422,18.024 59.514,18.024 C61.208,18.024 62.384,19.424 62.384,21.468 C62.384,23.89 60.844,25.094 59.332,25.094 C58.352,25.094 57.568,24.716 57.05,23.834 L57.022,23.834 L56.952,24.94 L55.902,24.94 C55.93,24.478 55.958,23.792 55.958,23.19 Z M57.176,21.02 L57.176,22.224 C57.176,22.378 57.19,22.532 57.232,22.672 C57.456,23.526 58.184,24.114 59.08,24.114 C60.382,24.114 61.138,23.064 61.138,21.51 C61.138,20.152 60.438,18.99 59.108,18.99 C58.282,18.99 57.498,19.578 57.246,20.502 C57.204,20.656 57.176,20.824 57.176,21.02 Z M62.93,18.164 L64.274,18.164 L65.744,22.168 C65.912,22.616 66.08,23.148 66.192,23.554 L66.22,23.554 C66.346,23.148 66.486,22.63 66.654,22.14 L67.998,18.164 L69.3,18.164 L67.452,22.994 C66.57,25.318 65.968,26.494 65.128,27.236 C64.512,27.754 63.924,27.964 63.616,28.02 L63.308,26.998 C63.616,26.9 64.022,26.704 64.386,26.396 C64.722,26.13 65.128,25.654 65.422,25.024 C65.478,24.898 65.52,24.8 65.52,24.73 C65.52,24.66 65.492,24.562 65.422,24.408 L62.93,18.164 Z" id="poweredby" fill="#AAAAAA"></path>
|
||||
<path d="M73.29,24.94 L73.29,15 L75.012,15 L75.012,24.94 L73.29,24.94 Z M79.982,17.968 C82.012,17.968 83.37,19.396 83.37,21.468 C83.37,23.974 81.606,25.094 79.87,25.094 C77.952,25.094 76.454,23.778 76.454,21.58 C76.454,19.34 77.924,17.968 79.982,17.968 Z M79.94,19.214 C78.722,19.214 78.232,20.404 78.232,21.538 C78.232,22.854 78.89,23.848 79.926,23.848 C80.906,23.848 81.592,22.896 81.592,21.51 C81.592,20.446 81.116,19.214 79.94,19.214 Z M89.558,23.456 L89.796,24.73 C89.432,24.898 88.718,25.08 87.864,25.08 C85.736,25.08 84.35,23.708 84.35,21.594 C84.35,19.55 85.75,17.968 88.144,17.968 C88.774,17.968 89.418,18.108 89.81,18.276 L89.502,19.578 C89.222,19.438 88.816,19.312 88.2,19.312 C86.884,19.312 86.1,20.278 86.114,21.524 C86.114,22.924 87.024,23.722 88.2,23.722 C88.788,23.722 89.222,23.596 89.558,23.456 Z M92.792,24.94 L91.07,24.94 L91.07,18.122 L92.792,18.122 L92.792,24.94 Z M91.938,15.28 C92.526,15.28 92.89,15.686 92.904,16.218 C92.904,16.722 92.526,17.142 91.924,17.142 C91.35,17.142 90.972,16.722 90.972,16.218 C90.972,15.686 91.364,15.28 91.938,15.28 Z M94.01,24.94 L94.01,23.946 L96.572,20.67 C96.922,20.25 97.216,19.914 97.566,19.522 L97.566,19.494 L94.262,19.494 L94.262,18.122 L99.666,18.122 L99.666,19.172 L97.174,22.378 C96.838,22.784 96.516,23.176 96.166,23.54 L96.166,23.568 L99.75,23.568 L99.75,24.94 L94.01,24.94 Z M106.638,22.014 L102.074,22.014 C102.116,23.246 103.068,23.778 104.174,23.778 C104.972,23.778 105.532,23.652 106.064,23.47 L106.316,24.646 C105.728,24.898 104.916,25.08 103.936,25.08 C101.724,25.08 100.422,23.722 100.422,21.636 C100.422,19.746 101.57,17.968 103.754,17.968 C105.966,17.968 106.694,19.788 106.694,21.286 C106.694,21.608 106.666,21.86 106.638,22.014 Z M102.074,20.81 L105.07,20.81 C105.084,20.18 104.804,19.144 103.656,19.144 C102.592,19.144 102.144,20.11 102.074,20.81 Z M108.724,25.094 C108.108,25.094 107.674,24.618 107.674,23.988 C107.674,23.344 108.122,22.882 108.738,22.882 C109.368,22.882 109.802,23.33 109.802,23.988 C109.802,24.618 109.382,25.094 108.724,25.094 Z M115.962,23.456 L116.2,24.73 C115.836,24.898 115.122,25.08 114.268,25.08 C112.14,25.08 110.754,23.708 110.754,21.594 C110.754,19.55 112.154,17.968 114.548,17.968 C115.178,17.968 115.822,18.108 116.214,18.276 L115.906,19.578 C115.626,19.438 115.22,19.312 114.604,19.312 C113.288,19.312 112.504,20.278 112.518,21.524 C112.518,22.924 113.428,23.722 114.604,23.722 C115.192,23.722 115.626,23.596 115.962,23.456 Z M120.442,17.968 C122.472,17.968 123.83,19.396 123.83,21.468 C123.83,23.974 122.066,25.094 120.33,25.094 C118.412,25.094 116.914,23.778 116.914,21.58 C116.914,19.34 118.384,17.968 120.442,17.968 Z M120.4,19.214 C119.182,19.214 118.692,20.404 118.692,21.538 C118.692,22.854 119.35,23.848 120.386,23.848 C121.366,23.848 122.052,22.896 122.052,21.51 C122.052,20.446 121.576,19.214 120.4,19.214 Z M125.244,24.94 L125.244,20.152 C125.244,19.368 125.216,18.71 125.188,18.122 L126.658,18.122 L126.742,19.13 L126.784,19.13 C127.106,18.598 127.722,17.968 128.884,17.968 C129.78,17.968 130.48,18.472 130.774,19.228 L130.802,19.228 C131.04,18.85 131.32,18.57 131.628,18.374 C132.02,18.108 132.454,17.968 133.028,17.968 C134.148,17.968 135.296,18.738 135.296,20.922 L135.296,24.94 L133.616,24.94 L133.616,21.174 C133.616,20.04 133.224,19.368 132.398,19.368 C131.81,19.368 131.376,19.788 131.194,20.278 C131.138,20.432 131.11,20.656 131.11,20.852 L131.11,24.94 L129.43,24.94 L129.43,20.992 C129.43,20.04 129.052,19.368 128.254,19.368 C127.61,19.368 127.176,19.872 127.008,20.334 C126.952,20.516 126.924,20.712 126.924,20.908 L126.924,24.94 L125.244,24.94 Z" id="locize.com" fill="#888888"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 994 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Configuring",
|
||||
"position": 4
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
# Data Source
|
||||
|
||||
After following the steps outlined in
|
||||
[Getting Started](./../development/getting-started.md), you'll notice that the
|
||||
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
|
||||
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
|
||||
the time may come when you want to develop using your own data either locally or
|
||||
remotely.
|
||||
|
||||
## Set up a local DICOM server
|
||||
|
||||
> ATTENTION! Already have a remote or local server? Skip to the
|
||||
> [configuration section](#configuration-learn-more) below.
|
||||
|
||||
While the OHIF Viewer can work with any data source, the easiest to configure
|
||||
are the ones that follow the [DICOMWeb][dicom-web] spec.
|
||||
|
||||
1. Choose and install an Image Archive
|
||||
2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your
|
||||
archive's web interface)
|
||||
3. Keep the server running
|
||||
|
||||
For our purposes, we will be using `Orthanc`, but you can see a list of
|
||||
[other Open Source options](#open-source-dicom-image-archives) below.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker
|
||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
||||
- [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/)
|
||||
- [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/)
|
||||
|
||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
||||
in command prompt or terminal_
|
||||
|
||||
> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_
|
||||
> parameter in _platform/viewer/package.json_ to http://192.168.99.100:8042 or
|
||||
> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy]
|
||||
> uses to proxy requests
|
||||
|
||||
### Running Orthanc
|
||||
|
||||
_Start Orthanc:_
|
||||
|
||||
```bash
|
||||
# Runs orthanc so long as window remains open
|
||||
yarn run orthanc:up
|
||||
```
|
||||
|
||||
_Upload your first Study:_
|
||||
|
||||
1. Navigate to
|
||||
[Orthanc's web interface](http://localhost:8042/app/explorer.html) at
|
||||
`http://localhost:8042/app/explorer.html` in a web browser.
|
||||
2. In the top right corner, click "Upload"
|
||||
3. Click "Select files to upload..." and select one or more DICOM files
|
||||
4. Click "Start the upload"
|
||||
|
||||
#### Orthanc: Learn More
|
||||
|
||||
You can see the `docker-compose.yml` file this command runs at
|
||||
[`<project-root>/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on
|
||||
Orthanc for Docker in [Orthanc's documentation][orthanc-docker].
|
||||
|
||||
### Connecting to Orthanc
|
||||
|
||||
Now that we have a local Orthanc instance up and running, we need to configure
|
||||
our web application to connect to it. Open a new terminal window, navigate to
|
||||
this repository's root directory, and run:
|
||||
|
||||
```bash
|
||||
# If you haven't already, enable yarn workspaces
|
||||
yarn config set workspaces-experimental true
|
||||
|
||||
# Restore dependencies
|
||||
yarn install
|
||||
|
||||
# Run our dev command, but with the local orthanc config
|
||||
yarn run dev:orthanc
|
||||
```
|
||||
|
||||
#### Configuration: Learn More
|
||||
|
||||
> For more configuration fun, check out the
|
||||
> [Essentials Configuration](./index.md) guide.
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w
|
||||
```
|
||||
|
||||
- `cross-env` sets three environment variables
|
||||
- PROXY_TARGET: `/dicom-web`
|
||||
- PROXY_DOMAIN: `http://localhost:8042`
|
||||
- APP_CONFIG: `config/docker_nginx-orthanc.js`
|
||||
- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration
|
||||
file. It will watch for changes and update as we develop.
|
||||
|
||||
`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests
|
||||
to `Orthanc`. This allows us to bypass CORS issues that normally occur when
|
||||
requesting resources that live at a different domain.
|
||||
|
||||
The `APP_CONFIG` value tells our app which file to load on to `window.config`.
|
||||
Here is what that
|
||||
configuration looks like:
|
||||
|
||||
```js title="<project-root>/platform/viewer/public/config/default.js"
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'Orthanc',
|
||||
wadoUriRoot: 'http://localhost:8899/wado',
|
||||
qidoRoot: 'http://localhost:8899/dicom-web',
|
||||
wadoRoot: 'http://localhost:8899/dicom-web',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](./index.md).
|
||||
|
||||
## Open Source DICOM Image Archives
|
||||
|
||||
Our example uses `Orthanc`, but there are a lot of options available to you.
|
||||
Here are some of the more popular ones:
|
||||
|
||||
| Archive | Installation |
|
||||
| --------------------------------------------- | ---------------------------------- |
|
||||
| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] |
|
||||
| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] |
|
||||
| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] |
|
||||
| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client |
|
||||
| [Horos][horos] (**Mac OSX only**) | Desktop Client |
|
||||
|
||||
_Feel free to make a Pull Request if you want to add to this list._
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado
|
||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||
[storescu]: http://support.dcmtk.org/docs/storescu.html
|
||||
[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy
|
||||
[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc
|
||||
<!-- Archives -->
|
||||
[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
|
||||
[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
|
||||
[orthanc]: https://www.orthanc-server.com/
|
||||
[orthanc-docker]: http://book.orthanc-server.com/users/docker.html
|
||||
[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud
|
||||
[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
|
||||
[osirix]: http://www.osirix-viewer.com/
|
||||
[horos]: https://www.horosproject.org/
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
> 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
|
||||
> recipe that contains an open Image Archive
|
||||
|
||||
|
||||
|
||||
### Configuration Files
|
||||
|
||||
The configuration for our viewer is in the `<root>platform/viewer/public/config`
|
||||
directory. Our build process knows which configuration file to use based on the
|
||||
`APP_CONFIG` environment variable. By default, its value is
|
||||
[`config/default.js`][default-config]. The majority of the viewer's features,
|
||||
and registered extension's features, are configured using this file.
|
||||
|
||||
**Embedded Use Note:**
|
||||
|
||||
Alternatively, when using the `umd` bundle for embedded use cases, these same
|
||||
values are what you'll pass to `installViewer` method:
|
||||
|
||||
`OHIFStandaloneViewer.installViewer(window.config)`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
We use environment variables at build and dev time to change the Viewer's
|
||||
behavior. We can update the `HTML_TEMPLATE` to easily change which extensions
|
||||
are registered, and specify a different `APP_CONFIG` to connect to an
|
||||
alternative data source (or even specify different default hotkeys).
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
|
||||
| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` |
|
||||
| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` |
|
||||
| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` |
|
||||
| `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` |
|
||||
|
||||
## How do I configure my project?
|
||||
|
||||
The simplest way is to update the existing default config:
|
||||
|
||||
|
||||
```js title="/platform/viewer/public/config/default.js"
|
||||
window.config = {
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
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
|
||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
||||
value of this environment variable a few different ways:
|
||||
|
||||
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
|
||||
- Previous `react-scripts` functionality that we need to duplicate with
|
||||
`dotenv-webpack`
|
||||
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
|
||||
- Previous `react-scripts` functionality that we need to duplicate with
|
||||
`dotenv-webpack`
|
||||
- Using the `cross-env` package in an npm script:
|
||||
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
|
||||
|
||||
After updating the configuration, `yarn run build` to generate updated build
|
||||
output.
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
|
||||
[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
|
||||
[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Deployment",
|
||||
"position": 8
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Overview
|
||||
---
|
||||
# Deployment
|
||||
|
||||
The OHIF Viewer can be embedded in other web applications via it's [packaged
|
||||
script source][viewer-npm], or served up as a stand-alone PWA ([progressive web
|
||||
application][pwa-url]) by building and hosting a collection of static assets. In
|
||||
either case, you will need to configure your instance of the Viewer so that it
|
||||
can connect to your data source (the database or PACS that provides the data
|
||||
your Viewer will display).
|
||||
|
||||
|
||||
Our goal is to make deployment as simple and painless as possible; however,
|
||||
there is an inherent amount of complexity in configuring and deploying web
|
||||
applications. If you find yourself a little lost, please don't hesitate to
|
||||
[reach out for help](/help.md)
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
### Embedded Viewer
|
||||
|
||||
The quickest and easiest way to get the OHIF Viewer up and running is to embed
|
||||
it into an existing web application. It allows us to forego a "build step", and
|
||||
add a powerful medical imaging viewer to an existing web page using only a few
|
||||
include tags.
|
||||
|
||||
- Read more about it here: [Embedded Viewer](./recipes/embedded-viewer.md)
|
||||
- And check out our [live demo on CodeSandbox][code-sandbox]
|
||||
|
||||

|
||||
|
||||
|
||||
### Stand-alone Viewer
|
||||
|
||||
Deploying the OHIF Viewer as a stand-alone web application provides many
|
||||
benefits, but comes at the cost of time and complexity. Some benefits include:
|
||||
|
||||
_Today:_
|
||||
|
||||
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
|
||||
- Add routes and customize the viewer's workflow
|
||||
- Finer control over styling and whitelabeling
|
||||
|
||||
_In the future:_
|
||||
|
||||
- The ability to package the viewer for [App Store distribution][app-store]
|
||||
- Leverage `service-workers` for offline support and speed benefits from caching
|
||||
|
||||
#### Hosted Static Assets
|
||||
|
||||
At the end of the day, a production OHIF Viewer instance is a collection of
|
||||
HTML, CSS, JS, Font Files, and Images. We "build" those files from our
|
||||
`source code` with configuration specific to our project. We then make those
|
||||
files publicly accessible by hosting them on a Web Server.
|
||||
|
||||
If you have not deployed a web application before, this may be a good time to
|
||||
[reach out for help](/help.md), as these steps assume prior web development and
|
||||
deployment experience.
|
||||
|
||||
##### Part 1 - Build Production Assets
|
||||
|
||||
"Building", or creating, the files you will need is the same regardless of the
|
||||
web host you choose. You can find detailed instructions on how to configure and
|
||||
build the OHIF Viewer in our
|
||||
["Build for Production" guide](./recipes/build-for-production.md).
|
||||
|
||||
##### Part 2 - Host Your App
|
||||
|
||||
There are a lot of [benefits to hosting static assets][host-static-assets] over
|
||||
dynamic content. You can find instructions on how to host your build's output
|
||||
via one of these guides:
|
||||
|
||||
_Drag-n-drop_
|
||||
|
||||
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
|
||||
|
||||
_Easy_
|
||||
|
||||
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
|
||||
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
|
||||
|
||||
_Advanced_
|
||||
|
||||
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
|
||||
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
|
||||
- [Azure](/deployment/recipes/static-assets.md#azure)
|
||||
|
||||
## Data
|
||||
|
||||
The OHIF Viewer is able to connect to any data source that implements the [DICOM
|
||||
Web Standard][dicom-web-standard]. [DICOM Web][dicom-web] refers to RESTful
|
||||
DICOM Services -- a recently standardized set of guidelines for exchanging
|
||||
medical images and imaging metadata over the internet. Not all archives fully
|
||||
support it yet, but it is gaining wider adoption.
|
||||
|
||||
### Configure Connection
|
||||
|
||||
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
|
||||
the steps layed out in our
|
||||
[Configuration Essentials Guide](./../configuring/index.md).
|
||||
|
||||
#### What if I don't have an imaging archive?
|
||||
|
||||
We provide some guidance on configuring a local image archive in our
|
||||
[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an
|
||||
archive remotely is a little trickier. You can check out some of our
|
||||
[advanced recipes](#recipes) for modeled setups that may work for you.
|
||||
|
||||
#### What if I intend to host the OHIF Viewer at a different domain?
|
||||
|
||||
There are two important steps to making sure this setup works:
|
||||
|
||||
1. Your Image Archive needs to be exposed, in some way, to the open web. This
|
||||
can be directly, or through a `reverse proxy`, but the Viewer needs _some
|
||||
way_ to request it's data.
|
||||
2. \* Your Image Archive needs to have appropriate CORS (Cross-Origin Resource
|
||||
Sharing) Headers
|
||||
|
||||
> \* Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional
|
||||
> HTTP headers to tell a browser to let a web application running at one origin
|
||||
> (domain) have permission to access selected resources from a server at a
|
||||
> different origin. - [MDN Web Docs: Web - Http - CORS][cors]
|
||||
|
||||
Most image archives do not provide either of these features "out of the box".
|
||||
It's common to use IIS, Nginx, or Apache to route incoming requests and append
|
||||
appropriate headers. You can find an example of this setup in our
|
||||
[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
|
||||
|
||||
#### What if my archive doesn't support DicomWeb?
|
||||
|
||||
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
|
||||
You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
|
||||
|
||||
This JSON is supplied via the '?url=' query parameter.
|
||||
It should reference an endpoint that returns **application/json** formatted text.
|
||||
|
||||
If you do not have an API, you can simply return a text file containing the JSON from any web server.
|
||||
|
||||
|
||||
You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route:
|
||||
|
||||
eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
|
||||
|
||||
The returned JSON object must contain a single root object with a 'studies' array.
|
||||
|
||||
|
||||
*Sample JSON format:*
|
||||
```json
|
||||
{
|
||||
"studies": [
|
||||
{
|
||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
|
||||
"StudyDescription": "BRAIN SELLA",
|
||||
"StudyDate": "20010108",
|
||||
"StudyTime": "120022",
|
||||
"PatientName": "MISTER^MR",
|
||||
"PatientId": "832040",
|
||||
"series": [
|
||||
{
|
||||
"SeriesDescription": "SAG T-1",
|
||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
||||
"SeriesNumber": 2,
|
||||
"SeriesDate": "20010108",
|
||||
"SeriesTime": "120318",
|
||||
"Modality": "MR",
|
||||
"instances": [
|
||||
{
|
||||
"metadata": {
|
||||
"Columns": 512,
|
||||
"Rows": 512,
|
||||
"InstanceNumber": 3,
|
||||
"AcquisitionNumber": 0,
|
||||
"PhotometricInterpretation": "MONOCHROME2",
|
||||
"BitsAllocated": 16,
|
||||
"BitsStored": 16,
|
||||
"PixelRepresentation": 1,
|
||||
"SamplesPerPixel": 1,
|
||||
"PixelSpacing": [0.390625, 0.390625],
|
||||
"HighBit": 15,
|
||||
"ImageOrientationPatient": [0,1,0,0,0,-1],
|
||||
"ImagePositionPatient": [11.600000,-92.500000, 98.099998],
|
||||
"FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
|
||||
"ImageType": ["ORIGINAL","PRIMARY","OTHER"],
|
||||
"Modality": "MR",
|
||||
"SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
|
||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
|
||||
},
|
||||
"url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
|
||||
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route.
|
||||
2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly.
|
||||
eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm`
|
||||
3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
|
||||
4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
|
||||
|
||||
*CORS Issues (Cross-Origin Resource Sharing)*
|
||||
|
||||
If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
|
||||
Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
|
||||
You will to need add a configuration on each server hosting these assets to allow your App server origin.
|
||||
|
||||
For example:
|
||||
|
||||
Lets assume your application is hosted on `https://my-ohif-server.com`.
|
||||
|
||||
Your JSON API is hosted on `https://my-json-api.aws.com`
|
||||
|
||||
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
|
||||
|
||||
When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
|
||||
|
||||
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
|
||||
|
||||
Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
|
||||
You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
|
||||
|
||||
Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
|
||||
|
||||
|
||||
### Securing Your Data
|
||||
|
||||
> Feeling lost? Securing your data is important, and it can be hard to tell if
|
||||
> you've gotten it right. Don't hesitate to work with professional auditors, or
|
||||
> [enlist help from experts](./../help.md).
|
||||
|
||||
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
|
||||
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
|
||||
values following the instructions laid out in the
|
||||
[Configuration Essentials Guide](./../configuring/index.md).
|
||||
|
||||
_Example OpenID-Connect Settings:_
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
...
|
||||
oidc: [
|
||||
{
|
||||
// ~ REQUIRED
|
||||
// Authorization Server URL
|
||||
authority: 'http://127.0.0.1/auth/realms/ohif',
|
||||
client_id: 'ohif-viewer',
|
||||
redirect_uri: 'http://127.0.0.1/callback', // `OHIFStandaloneViewer.js`
|
||||
response_type: 'code', // "Authorization Code Flow"
|
||||
scope: 'openid', // email profile openid
|
||||
// ~ OPTIONAL
|
||||
post_logout_redirect_uri: '/logout-redirect.html',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
You can find an example of this setup in our
|
||||
[User Account Control Deployment Recipe](./recipes/user-account-control.md).
|
||||
|
||||
#### Choosing a Flow for the Viewer
|
||||
|
||||
In general, we recommend using the "Authorization Code Flow" ( [see
|
||||
`response_type=code` here][code-flows]); however, the "Implicit Flow" ( [see
|
||||
`response_type=token` here][code-flows]) can work if additonal precautions are
|
||||
taken. If the flow you've chosen produces a JWT Token, it's validity can be used
|
||||
to secure access to your Image Archive as well.
|
||||
|
||||
### Recipes
|
||||
|
||||
We've included a few recipes for common deployment scenarios. There are many,
|
||||
many possible configurations, so please don't feel limited to these setups.
|
||||
Please feel free to suggest or contribute your own recipes.
|
||||
|
||||
- Script Include
|
||||
- [Embedding the Viewer](./recipes/embedded-viewer.md)
|
||||
- Stand-Alone
|
||||
- [Build for Production](./recipes/build-for-production.md)
|
||||
- [Static](./recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
|
||||
- [User Account Control](./recipes/user-account-control.md)
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
|
||||
[static-assets-url]: https://www.maxcdn.com/one/visual-glossary/static-content/
|
||||
[app-store]: https://medium.freecodecamp.org/i-built-a-pwa-and-published-it-in-3-app-stores-heres-what-i-learned-7cb3f56daf9b
|
||||
[dicom-web-standard]: https://www.dicomstandard.org/dicomweb/
|
||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
|
||||
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
||||
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Recipes",
|
||||
"position": 2
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
# Build for Production
|
||||
|
||||
> If you've already followed the
|
||||
> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead
|
||||
> to [Configuration](#configuration)
|
||||
|
||||
## Overview
|
||||
|
||||
### Build Machine Requirements
|
||||
|
||||
- [Node.js & NPM](https://nodejs.org/en/download/)
|
||||
- [Yarn](https://yarnpkg.com/lang/en/docs/install/)
|
||||
- [Git](https://www.atlassian.com/git/tutorials/install-git)
|
||||
|
||||
### Getting the Code
|
||||
|
||||
_With Git:_
|
||||
|
||||
```bash
|
||||
# Clone the remote repository to your local machine
|
||||
git clone https://github.com/OHIF/Viewers.git
|
||||
```
|
||||
|
||||
More on: _[`git clone`](https://git-scm.com/docs/git-clone),
|
||||
[`git checkout`](https://git-scm.com/docs/git-checkout)_
|
||||
|
||||
_From .zip:_
|
||||
|
||||
[OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/master.zip)
|
||||
|
||||
### Restore Dependencies & Build
|
||||
|
||||
Open your terminal, and navigate to the directory containing the source files.
|
||||
Next run these commands:
|
||||
|
||||
```bash
|
||||
// If you haven't already, enable yarn workspaces
|
||||
yarn config set workspaces-experimental true
|
||||
|
||||
// Restore dependencies
|
||||
yarn install
|
||||
|
||||
// Build source code for production
|
||||
yarn run build
|
||||
```
|
||||
|
||||
If everything worked as expected, you should have a new `dist/` directory in the
|
||||
project's folder. It should roughly resemble the following:
|
||||
|
||||
```bash
|
||||
<root>platform/viewer/dist/
|
||||
├── app-config.js
|
||||
├── app.bundle.js
|
||||
├── app.css
|
||||
├── index.html
|
||||
├── manifest.json
|
||||
├── service-worker.js
|
||||
└── ...
|
||||
```
|
||||
|
||||
By default, the build output will connect to OHIF's publicly accessible PACS. If
|
||||
this is your first time setting up the OHIF Viewer, it is recommended that you
|
||||
test with these default settings. After testing, you can find instructions on
|
||||
how to configure the project for your own imaging archive below.
|
||||
|
||||
### Configuration
|
||||
|
||||
The configuration for our viewer is in the `<root>platform/viewer/public/config`
|
||||
directory. Our build process knows which configuration file to use based on the
|
||||
`APP_CONFIG` environment variable. By default, its value is
|
||||
[`config/default.js`][default-config]. The majority of the viewer's features,
|
||||
and registered extension's features, are configured using this file.
|
||||
|
||||
The easiest way to apply your own configuration is to modify the `default.js`
|
||||
file. For more advanced cofiguration options, check out our
|
||||
[configuration essentials guide](/configuring/index.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Deploying Build Output
|
||||
|
||||
_Drag-n-drop_
|
||||
|
||||
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
|
||||
|
||||
_Easy_
|
||||
|
||||
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
|
||||
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
|
||||
|
||||
_Advanced_
|
||||
|
||||
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
|
||||
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
|
||||
- [Azure](/deployment/recipes/static-assets.md#azure)
|
||||
|
||||
### Testing Build Output Locally
|
||||
|
||||
A quick way to test your build output locally is to spin up a small webserver.
|
||||
You can do this by running the following commands in the `dist/` output
|
||||
directory:
|
||||
|
||||
```bash
|
||||
// Install http-server as a globally available package
|
||||
yarn global add http-server
|
||||
|
||||
// Serve the files in our current directory
|
||||
// Accessible at: `http://localhost:8080`
|
||||
http-server
|
||||
```
|
||||
|
||||
### Automating Builds and Deployments
|
||||
|
||||
If you found setting up your environmnent and running all of these steps to be a
|
||||
bit tedious, then you are in good company. Thankfully, there are a large number
|
||||
of tools available to assist with automating tasks like building and deploying
|
||||
web application. For a starting point, check out this repository's own use of:
|
||||
|
||||
- [CircleCI][circleci]: [config.yaml][circleci-config]
|
||||
- [Netlify][netlify]: [netlify.toml][netlify.toml] |
|
||||
[build-deploy-preview.sh][build-deploy-preview.sh]
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
> Issues and resolutions for common GitHub issues will be summarized here
|
||||
|
||||
...
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[circleci]: https://circleci.com/gh/OHIF/Viewers
|
||||
[circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml
|
||||
[netlify]: https://app.netlify.com/sites/ohif/deploys
|
||||
[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/netlify.toml
|
||||
[build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
# Embedded Viewer
|
||||
|
||||
The quickest and easiest way to get the OHIF Viewer up and running is to embed
|
||||
it into an existing web application. It allows us to forego a "build step", and
|
||||
add a powerful medical imaging viewer to an existing web page using only a few
|
||||
include tags. Here's how it works:
|
||||
|
||||
{% include "./../_embedded-viewer-diagram.md" %}
|
||||
|
||||
1. Create a new web page or template that includes the following external
|
||||
dependencies:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap">
|
||||
<code>Google Font: Roboto</code>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://unpkg.com/@ohif/viewer">
|
||||
<code>@ohif/viewer@latest</code>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ol start="2">
|
||||
<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>
|
||||
</ol>
|
||||
|
||||
```js
|
||||
// Set before importing `ohif-viewer` (JS Object)
|
||||
window.config = {
|
||||
// default: '/'
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](../../configuring/index.md).
|
||||
|
||||
<ol start="3"><li>
|
||||
Render the viewer in the web page's target <code>div</code>
|
||||
</li></ol>
|
||||
|
||||
```js
|
||||
// Made available by the `@ohif/viewer` script included in step 1
|
||||
var containerId = 'id-of-div-to-render-component-to';
|
||||
var componentRenderedOrUpdatedCallback = function() {
|
||||
console.log('OHIF Viewer rendered/updated');
|
||||
};
|
||||
window.OHIFViewer.installViewer(
|
||||
window.config,
|
||||
containerId,
|
||||
componentRenderedOrUpdatedCallback
|
||||
);
|
||||
```
|
||||
|
||||
You can see a live example of this recipe in [this CodeSandbox][code-sandbox].
|
||||
|
||||
## Add Extensions
|
||||
|
||||
The UMD build of the OHIF Viewer is a "light weight" build that only contains
|
||||
the core extensions required for basic 2D image viewing. It's possible to add
|
||||
other extensions at runtime.
|
||||
|
||||
This only requires us to include a single script tag, and add it using the
|
||||
`extensions` key to our config. In this practical example, we register our
|
||||
popular whole slide microscopy extension:
|
||||
|
||||
```html
|
||||
<script
|
||||
src="https://unpkg.com/@ohif/extension-dicom-microscopy@0.50.5/dist/index.umd.js"
|
||||
crossorigin
|
||||
></script>
|
||||
|
||||
<!-- --->
|
||||
<script>
|
||||
window.config = {
|
||||
// ...
|
||||
extensions: [OHIFExtDicomMicroscopy],
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
You can see an example of a slide microscopy study in the viewer [with the
|
||||
extension enabled here][whole-slide-ext-demo] ([source code][ext-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
|
||||
[extensions guide](/extensions/index.md).
|
||||
|
||||
#### FAQ
|
||||
|
||||
> I'm having trouble getting this to work. Where can I go for help?
|
||||
|
||||
First, check out this fully functional [CodeSandbox][code-sandbox] example. If
|
||||
you're still having trouble, feel free to search or GitHub issues. Can't find
|
||||
anything related your problem? Create a new one.
|
||||
|
||||
> My application's styles are impacting the OHIF Viewer's look and feel. What
|
||||
> can I do?
|
||||
|
||||
When you include stylesheets and scripts, they are added globally. This has the
|
||||
potential of causing conflicts with other scripts and styles on the page. To
|
||||
prevent this, `embed` the viewer in a new/empty web page. Have that working?
|
||||
Good. Now `embed` that new page using an
|
||||
[`<iframe>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe).
|
||||
|
||||
This should produce the expected result while also protecting your page from any
|
||||
globally defined styles/scripts.
|
||||
|
||||
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
|
||||
> react-dom and react conflicts. What can we do?
|
||||
|
||||
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
|
||||
```jsx
|
||||
import { installViewer } from '@ohif/viewer'
|
||||
|
||||
const ohifViewerConfig = window.config // or set it here
|
||||
const containerId = 'ohif'
|
||||
const componentRenderedOrUpdatedCallback = function() {
|
||||
console.log('OHIF Viewer rendered/updated');
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
installViewer(
|
||||
ohifViewerConfig,
|
||||
containerId,
|
||||
componentRenderedOrUpdatedCallback
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
...
|
||||
//you can render in any element you wish
|
||||
<AnyTag id={containerId}/>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`installViewer` is a convenience method that pulls in some dependencies that may
|
||||
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
|
||||
which is a react component that takes the `configuration` outlined above as
|
||||
props. You can use it as a reusable component, and to avoid `react` version
|
||||
conflict issues.
|
||||
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
|
||||
[whole-slide-base-demo]: https://tprch.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
|
||||
[ext-code-sandbox]: https://codesandbox.io/s/viewer-script-tag-microscopy-extension-44unk
|
||||
[whole-slide-ext-demo]: https://44unk.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Google Cloud Healthcare
|
||||
|
||||
> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a
|
||||
> powerful option for storing medical imaging data in the cloud.
|
||||
|
||||
An alternative to deploying your own PACS is to use a software-as-a-service
|
||||
provider such as Google Cloud. The Cloud Healthcare API promises to be a
|
||||
scalable, secure, cost effective image storage solution for those willing to
|
||||
store their data in the cloud. It offers an
|
||||
[almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom)
|
||||
which requires tokens generated via the
|
||||
[OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in).
|
||||
Images can even be transcoded on the fly if this is desired.
|
||||
|
||||
## Setup a Google Cloud Healthcare Project
|
||||
|
||||
- Create a Google Cloud account
|
||||
- Create a project in Google Cloud
|
||||
- Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for
|
||||
your project.
|
||||
- (Optional): Create a Dataset and Data Store for storing your DICOM data
|
||||
- Enable the
|
||||
[Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for
|
||||
your project.
|
||||
- _Note:_ If you are having trouble finding the APIs, use the search box at
|
||||
the top of the Cloud console.
|
||||
- Go to APIs & Services > Credentials to create an OAuth Consent screen and fill
|
||||
in your application details.
|
||||
- Under Scopes for Google APIs, click "manually paste scopes".
|
||||
- Add the following scopes:
|
||||
- `https://www.googleapis.com/auth/cloudplatformprojects.readonly`
|
||||
- `https://www.googleapis.com/auth/cloud-healthcare`
|
||||
- Go to APIs & Services > Credentials to create a new set of credentials:
|
||||
|
||||
- Choose the "Web Application" type
|
||||
- Set up an
|
||||
[OAuth 2.0 Client ID](https://support.google.com/cloud/answer/6158849?hl=en)
|
||||
- Add your domain (e.g. `http://localhost:3000`) to Authorized JavaScript
|
||||
origins.
|
||||
- Add your domain, plus `callback` (e.g. `http://localhost:3000/callback`) to
|
||||
Authorized Redirect URIs.
|
||||
- Save your Client ID for later.
|
||||
|
||||
- (Optional): Enable Public Datasets that are being hosted by Google:
|
||||
https://cloud.google.com/healthcare/docs/resources/public-datasets/
|
||||
|
||||
## Run the viewer with your OAuth Client ID
|
||||
|
||||
1. Open the `config/google.js` file and change `YOURCLIENTID` to your Client ID
|
||||
value.
|
||||
1. Run the OHIF Viewer using the config/google.js configuration file
|
||||
|
||||
```bash
|
||||
cd OHIFViewer
|
||||
yarn install
|
||||
APP_CONFIG=config/google.js yarn run dev
|
||||
```
|
||||
|
||||
## Running via Docker
|
||||
|
||||
The OHIF Viewer Docker container can be connected to Google Cloud Healthcare by
|
||||
providing a Client ID at runtime. This is a very simple method to get up and
|
||||
running.
|
||||
|
||||
1. Install Docker (https://www.docker.com/)
|
||||
1. Run the Docker container, providing a Client ID as an environment variable.
|
||||
Client IDs look like `xyz.apps.googleusercontent.com`.
|
||||
|
||||
```bash
|
||||
docker run --env CLIENT_ID=$CLIENT_ID --publish 5000:80 ohif/viewer:latest
|
||||
```
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
# Nginx + Image Archive
|
||||
|
||||
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
|
||||
> doubt, enlist the help of an expert and conduct proper audits.
|
||||
|
||||
At a certain point, you may want others to have access to your instance of the
|
||||
OHIF Viewer and its medical imaging data. This post covers one of many potential
|
||||
setups that accomplish that. Please note, noticably absent is user account
|
||||
control.
|
||||
|
||||
Do not use this recipe to host sensitive medical data on the open web. Depending
|
||||
on your company's policies, this may be an appropriate setup on an internal
|
||||
network when protected with a server's basic authentication. For a more robust
|
||||
setup, check out our [user account control recipe](./user-account-control.md)
|
||||
that builds on the lessons learned here.
|
||||
|
||||
## Overview
|
||||
|
||||
Our two biggest hurdles when hosting our image archive and web client are:
|
||||
|
||||
- Risks related to exposing our PACS to the netowrk
|
||||
- Cross-Origin Resource Sharing (CORS) requests
|
||||
|
||||
### Handling Web Requests
|
||||
|
||||
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
||||
requests. Nginx is open source software for web serving, reverse proxying,
|
||||
caching, and more. It's designed for maximum performance and stability --
|
||||
allowing us to more reliably serve content than Orthanc's built-in server can.
|
||||
|
||||
More specifically, we accomplish this by using a
|
||||
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
||||
resources from our image archive (Orthanc), and when accessing its web admin.
|
||||
|
||||
> A reverse proxy is a type of proxy server that retrieves resources on behalf
|
||||
> of a client from one or more servers. These resources are then returned to the
|
||||
> client, appearing as if they originated from the proxy server itself.
|
||||
|
||||
### CORS Issues
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
|
||||
tell a browser which web applications have permission to access selected
|
||||
resources from a server at a different origin (domain, protocol, port). IE. By
|
||||
default, a Web App located at `http://my-website.com` can't access resources
|
||||
hosted at `http://not-my-website.com`
|
||||
|
||||
We can solve this one of two ways:
|
||||
|
||||
1. Have our Image Archive located at the same domain as our Web App
|
||||
2. Add appropriate `Access-Control-Allow-*` HTTP headers
|
||||
|
||||
**This solution uses the first approach.**
|
||||
|
||||
You can read more about CORS in this Medium article: [Understanding
|
||||
CORS][understanding-cors]
|
||||
|
||||
### Diagram
|
||||
|
||||
This setup allows us to create a setup similar to the one pictured below:
|
||||
|
||||
<!-- {% include "./../_nginx-image-archive-diagram.md" %} -->
|
||||

|
||||
|
||||
|
||||
- All web requests are routed through `nginx` on our `OpenResty` image
|
||||
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
|
||||
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
|
||||
- All static resources for OHIF Viewer are served up by `nginx` when a matching
|
||||
route for that resource is requested
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker
|
||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
||||
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
||||
|
||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
||||
in command prompt or terminal_
|
||||
|
||||
### Setup
|
||||
|
||||
_Spin Things Up_
|
||||
|
||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
|
||||
- Run `docker-compose up`
|
||||
|
||||
_Upload Your First Study_
|
||||
|
||||
- Navigate to `http://127.0.0.1/pacs-admin`
|
||||
- From the top right, select "Upload"
|
||||
- Click "Select files to upload..." (DICOM)
|
||||
- Click "Start the upload"
|
||||
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
_Exit code 137_
|
||||
|
||||
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
||||
tab, and increase the amount of Memory available.
|
||||
|
||||
_Cannot create container for service X_
|
||||
|
||||
Use this one with caution: `docker system prune`
|
||||
|
||||
_X is already running_
|
||||
|
||||
Stop running all containers:
|
||||
|
||||
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
|
||||
- Linux: `docker stop $(docker ps -a -q)`
|
||||
|
||||
### Configuration
|
||||
|
||||
After verifying that everything runs with default configuration values, you will
|
||||
likely want to update:
|
||||
|
||||
- The domain: `http://127.0.0.1`
|
||||
|
||||
#### OHIF Viewer
|
||||
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||
configuration we use is set to a specific file when we build the viewer, and
|
||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||
in the `dockerfile` for this solution:
|
||||
|
||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
||||
|
||||
You can find the configuration we're using here:
|
||||
`/public/config/docker_openresty-orthanc.js`
|
||||
|
||||
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
||||
Viewer's configuration, you can run:
|
||||
|
||||
- `docker-compose build` OR
|
||||
- `docker-compose up --build`
|
||||
|
||||
#### Other
|
||||
|
||||
All other files are found in: `/docker/OpenResty-Orthanc/`
|
||||
|
||||
| Service | Configuration | Docs |
|
||||
| ----------------- | --------------------------------- | ------------------------------------------- |
|
||||
| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! |
|
||||
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
|
||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Deploying to Production
|
||||
|
||||
While these configuration and docker-compose files model an environment suitable
|
||||
for production, they are not easy to deploy "as is". You can either:
|
||||
|
||||
- Manually recreate this environment and deploy built application files **OR**
|
||||
- Deploy to a cloud kubernetes provider like
|
||||
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
|
||||
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
|
||||
- Find and follow your preferred provider's guide on setting up
|
||||
[swarms and stacks](https://docs.docker.com/get-started/)
|
||||
|
||||
### Adding SSL
|
||||
|
||||
Adding SSL registration and renewal for your domain with Let's Encrypt that
|
||||
terminates at Nginx is an incredibly important step toward securing your data.
|
||||
Here are some resources, specific to this setup, that may be helpful:
|
||||
|
||||
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
|
||||
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
|
||||
|
||||
While we terminate SSL at Nginx, it may be worth using self signed certificates
|
||||
for communication between services.
|
||||
|
||||
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
|
||||
|
||||
### Use PostgresSQL w/ Orthanc
|
||||
|
||||
Orthanc can handle a large amount of data and requests, but if you find that
|
||||
requests start to slow as you add more and more studies, you may want to
|
||||
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
|
||||
that can be found in the
|
||||
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
|
||||
"PostgreSQL and Orthanc inside Docker"
|
||||
|
||||
### Improving This Guide
|
||||
|
||||
Here are some improvements this guide would benefit from, and that we would be
|
||||
more than happy to accept Pull Requests for:
|
||||
|
||||
- SSL Support
|
||||
- Complete configuration with `.env` file (or something similar)
|
||||
- Any security issues
|
||||
- One-click deploy to a cloud provider
|
||||
|
||||
## Resources
|
||||
|
||||
### Misc. Helpful Commands
|
||||
|
||||
_Check if `nginx.conf` is valid:_
|
||||
|
||||
```bash
|
||||
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
|
||||
```
|
||||
|
||||
_Interact w/ running container:_
|
||||
|
||||
`docker exec -it CONTAINER_NAME bash`
|
||||
|
||||
_List running containers:_
|
||||
|
||||
`docker ps`
|
||||
|
||||
### Referenced Articles
|
||||
|
||||
For more documentation on the software we've chosen to use, you may find the
|
||||
following resources helpful:
|
||||
|
||||
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
|
||||
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
|
||||
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
|
||||
|
||||
For a different take on this setup, check out the repositories our community
|
||||
members put together:
|
||||
|
||||
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
|
||||
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- DOCS -->
|
||||
[nginx]: https://www.nginx.com/resources/glossary/nginx/
|
||||
[understanding-cors]: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b
|
||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||
<!-- SRC -->
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_label: Assets
|
||||
---
|
||||
# Deploy Static Assets
|
||||
|
||||
> WARNING! All of these solutions stand-up a publicly accessible web viewer. Do
|
||||
> not hook your hosted viewer up to a sensitive source of data without
|
||||
> implementing authentication.
|
||||
|
||||
There are a lot of options for deploying static assets. Some services, like
|
||||
`netlify` and `surge.sh`, specialize in static websites. You'll notice that
|
||||
deploying with them requires much less time and effort, but comes at the cost of
|
||||
less product offerings.
|
||||
|
||||
While not required, it can simplify things to host your Web Viewer alongside
|
||||
your image archive. Services with more robust product offerings, like
|
||||
`Google Cloud`, `Microsoft's Azure`, and `Amazon Web Services (AWS)`, are able
|
||||
to accomodate this setup.
|
||||
|
||||
_Drag-n-drop_
|
||||
|
||||
- [Netlify: Drop](#netlify-drop)
|
||||
|
||||
_Easy_
|
||||
|
||||
- [Surge.sh](#surgesh)
|
||||
- [GitHub Pages](#github-pages)
|
||||
|
||||
_Advanced_
|
||||
|
||||
- [Deploy Static Assets](#deploy-static-assets)
|
||||
- [Drag-n-drop](#drag-n-drop)
|
||||
- [Netlify: Drop](#netlify-drop)
|
||||
- [Easy](#easy)
|
||||
- [Surge.sh](#surgesh)
|
||||
- [GitHub Pages](#github-pages)
|
||||
- [Advanced](#advanced)
|
||||
- [AWS S3 + Cloudfront](#aws-s3--cloudfront)
|
||||
- [GCP + Cloudflare](#gcp--cloudflare)
|
||||
- [Azure](#azure)
|
||||
|
||||
## Drag-n-drop
|
||||
|
||||
### Netlify: Drop
|
||||
|
||||
> Build, deploy, and manage modern web projects. An all-in-one workflow that
|
||||
> combines global deployment, continuous integration, and automatic HTTPS. And
|
||||
> that’s just the beginning.
|
||||
|
||||

|
||||
|
||||
_GIF demonstrating deployment with Netlify Drop_
|
||||
|
||||
1. https://app.netlify.com/drop
|
||||
2. Drag your `build/` folder on to the drop target
|
||||
3. ...
|
||||
4. _annnd you're done_
|
||||
|
||||
**Features:**
|
||||
|
||||
- Custom domains & HTTPS
|
||||
- Instant Git integration
|
||||
- Continuous deployment
|
||||
- Deploy previews
|
||||
- Access to add-ons
|
||||
|
||||
(Non-free tiers include identity, FaaS, Forms, etc.)
|
||||
|
||||
Learn more about [Netlify on their website](https://www.netlify.com/)
|
||||
|
||||
## Easy
|
||||
|
||||
### Surge.sh
|
||||
|
||||
> Static web publishing for Front-End Developers. Simple, single-command web
|
||||
> publishing. Publish HTML, CSS, and JS for free, without leaving the command
|
||||
> line.
|
||||
|
||||

|
||||
|
||||
_GIF demonstrating deployment with surge_
|
||||
|
||||
```shell
|
||||
# Add surge command
|
||||
yarn global add surge
|
||||
|
||||
# In the build directory
|
||||
surge
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Free custom domain support
|
||||
- Free SSL for surge.sh subdomains
|
||||
- pushState support for single page apps
|
||||
- Custom 404.html pages
|
||||
- Barrier-free deployment through the CLI
|
||||
- Easy integration into your Grunt toolchain
|
||||
- Cross-origin resource support
|
||||
- And more…
|
||||
|
||||
Learn more about [surge.sh on their website](https://surge.sh/)
|
||||
|
||||
### GitHub Pages
|
||||
|
||||
> WARNING! While great for project sites and light use, it is not advised to use
|
||||
> GitHub Pages for production workloads. Please consider using a different
|
||||
> service for mission critical applications.
|
||||
|
||||
> Websites for you and your projects. Hosted directly from your GitHub
|
||||
> repository. Just edit, push, and your changes are live.
|
||||
|
||||
This deploy strategy makes more sense if you intend to maintain your project in
|
||||
a GitHub repository. It allows you to specify a `branch` or `folder` as the
|
||||
target for a GitHub Page's website. As you push code changes, the hosted content
|
||||
updates to reflect those changes.
|
||||
|
||||
1. Head over to GitHub.com and create a new repository, or go to an existing
|
||||
one. Click on the Settings tab.
|
||||
2. Scroll down to the GitHub Pages section. Choose the `branch` or `folder` you
|
||||
would like as the "root" of your website.
|
||||
3. Fire up a browser and go to `http://username.github.io/repository`
|
||||
|
||||
Configuring Your Site:
|
||||
|
||||
- [Setting up a custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages)
|
||||
- [Setting up SSL](https://help.github.com/en/articles/securing-your-github-pages-site-with-https)
|
||||
|
||||
Learn more about [GitHub Pages on its website](https://pages.github.com/)
|
||||
|
||||
## Advanced
|
||||
|
||||
All of these options, while using providers with more service offerings,
|
||||
demonstrate how to host the viewer with their respective file storage and CDN
|
||||
offerings. While you can serve your static assets this way, if you're going
|
||||
through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to
|
||||
avoid using a proxy or to simplify authentication.
|
||||
|
||||
If that is the case, check out some of our more advanced `docker` deployments
|
||||
that target these providers from the left-hand sidepanel.
|
||||
|
||||
These guides can be a bit longer and a update more frequently. To provide
|
||||
accurate documentation, we will link to each provider's own recommended steps:
|
||||
|
||||
### AWS S3 + Cloudfront
|
||||
|
||||
- [Host a Static Website](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
|
||||
- [Speed Up Your Website with Cloudfront](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html)
|
||||
|
||||
### GCP + Cloudflare
|
||||
|
||||
- [Things to Know Before Getting Started](https://code.luasoftware.com/tutorials/google-cloud-storage/things-to-know-before-hosting-static-website-on-google-cloud-storage/)
|
||||
- [Hosting a Static Website on GCP](https://cloud.google.com/storage/docs/hosting-static-website)
|
||||
|
||||
### Azure
|
||||
|
||||
- [Host a Static Website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)
|
||||
- [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn)
|
||||
- [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name)
|
||||
@@ -0,0 +1,293 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
# User Account Control
|
||||
|
||||
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
|
||||
> doubt, enlist the help of an expert and conduct proper audits.
|
||||
|
||||
Making a viewer and its medical imaging data accessible on the open web can
|
||||
provide a lot of benefits, but requires additional security to make sure
|
||||
sensitive information can only be viewed by authorized individuals. Most image
|
||||
archives are equipped with basic security measures, but they are not
|
||||
robust/secure enough for the open web.
|
||||
|
||||
This guide covers one of many potential production setups that secure our
|
||||
sensitive data.
|
||||
|
||||
## Overview
|
||||
|
||||
This guide builds on top of our
|
||||
[Nginx + Image Archive guide](/deployment/recipes/nginx--image-archive.md),
|
||||
wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy)
|
||||
to retrieve resources from our image archive (Orthanc).
|
||||
|
||||
To add support for "User Account Control" we introduce
|
||||
[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source
|
||||
Identity and Access Management solution that makes it easy to secure
|
||||
applications and services with little to no code. We improve upon our
|
||||
`reverse proxy` setup by integrating Keycloak and Nginx to create an
|
||||
`authenticating reverse proxy`.
|
||||
|
||||
> An authenticating reverse proxy is a reverse proxy that only retrieves the
|
||||
> resources on behalf of a client if the client has been authenticated. If a
|
||||
> client is not authenticated they can be redirected to a login page.
|
||||
|
||||
This setup allows us to create a setup similar to the one pictured below:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
- All web requests are routed through `nginx` on our `OpenResty` image
|
||||
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
|
||||
- Requires valid `Authorization: Bearer <token>` header
|
||||
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
|
||||
- `/auth` is a reverse proxy for `keycloak`
|
||||
- All static resources for OHIF Viewer are unprotected and accessible. We have
|
||||
application logic that will redirect unauthenticated users to the appropriate
|
||||
`keycloak` login screen.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker
|
||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
||||
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
||||
|
||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
||||
in command prompt or terminal_
|
||||
|
||||
### Setup
|
||||
|
||||
_Spin Things Up_
|
||||
|
||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc-Keycloak` in your shell
|
||||
- Run `docker-compose up`
|
||||
|
||||
_Create Your First User_
|
||||
|
||||
- Navigate to: `http://127.0.0.1/auth/admin`
|
||||
- Sign in with: `admin`/`password`
|
||||
- From the top left dropdown, select the `Ohif` realm
|
||||
- From the left sidebar, under `Manage`, select `Users`
|
||||
- Click `Add User`
|
||||
- Username: `test`
|
||||
- Email Verified: `ON`
|
||||
- Click `Save`
|
||||
- Click the `Credentials` Tab
|
||||
- New Pasword: `test`
|
||||
- Password Confirmation: `test`
|
||||
- Temporary: `OFF`
|
||||
- Click: `Reset Password`
|
||||
- From the top right dropdown, select `Admin`, then `Sign Out`
|
||||
|
||||
_Sign In_
|
||||
|
||||
- Navigate to `http://127.0.0.1/`
|
||||
- Username: `test`, Password: `test`
|
||||
- Click `Log In`
|
||||
|
||||
_Upload Your First Study_
|
||||
|
||||
- Navigate to `http://127.0.0.1/pacs-admin`
|
||||
- If you're not already logged in, use `test`/`test`
|
||||
- From the top right, select "Upload"
|
||||
- Click "Select files to upload..." (DICOM)
|
||||
- Click "Start the upload"
|
||||
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
_Exit code 137_
|
||||
|
||||
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
||||
tab, and increase the amount of Memory available.
|
||||
|
||||
_Cannot create container for service X_
|
||||
|
||||
Use this one with caution: `docker system prune`
|
||||
|
||||
_X is already running_
|
||||
|
||||
Stop running all containers:
|
||||
|
||||
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
|
||||
- Linux: `docker stop $(docker ps -a -q)`
|
||||
|
||||
### Configuration
|
||||
|
||||
After verifying that everything runs with default configuration values, you will
|
||||
likely want to update:
|
||||
|
||||
- The domain: `http://127.0.0.1`
|
||||
- Set secure, non-default passwords
|
||||
- Regenerate Keycloak Client Secrets
|
||||
|
||||
#### OHIF Viewer
|
||||
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||
configuration we use is set to a specific file when we build the viewer, and
|
||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||
in the `dockerfile` for this solution:
|
||||
|
||||
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
|
||||
|
||||
You can find the configuration we're using here:
|
||||
`/public/config/docker_openresty-orthanc-keycloak.js`
|
||||
|
||||
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
||||
Viewer's configuration, you can run:
|
||||
|
||||
- `docker-compose build` OR
|
||||
- `docker-compose up --build`
|
||||
|
||||
#### Other
|
||||
|
||||
All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/`
|
||||
|
||||
| Service | Configuration | Docs |
|
||||
| ----------------- | ------------------------------------------------ | ------------------------------------------- |
|
||||
| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! |
|
||||
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
|
||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
||||
| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | |
|
||||
|
||||
\* These are the seed values for Keycloak. They can be manually updated at
|
||||
`http://127.0.0.1/auth/admin`
|
||||
|
||||
#### Keycloak Themeing
|
||||
|
||||
The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak
|
||||
theme. You can find the source files for it in
|
||||
`/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/`. You can see how
|
||||
we add it to Keycloak in the `docker-compose` file, and you can read up on how
|
||||
to leverage custom themes in
|
||||
[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes).
|
||||
|
||||
| Default Theme | OHIF Theme |
|
||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Deploying to Production
|
||||
|
||||
While these configuration and docker-compose files model an environment suitable
|
||||
for production, they are not easy to deploy "as is". You can either:
|
||||
|
||||
- Manually recreate this environment and deploy built application files **OR**
|
||||
- Deploy to a cloud kubernetes provider like
|
||||
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
|
||||
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
|
||||
- Find and follow your preferred provider's guide on setting up
|
||||
[swarms and stacks](https://docs.docker.com/get-started/)
|
||||
|
||||
### Adding SSL
|
||||
|
||||
Adding SSL registration and renewal for your domain with Let's Encrypt that
|
||||
terminates at Nginx is an incredibly important step toward securing your data.
|
||||
Here are some resources, specific to this setup, that may be helpful:
|
||||
|
||||
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
|
||||
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
|
||||
|
||||
While we terminate SSL at Nginx, it may be worth using self signed certificates
|
||||
for communication between services.
|
||||
|
||||
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
|
||||
|
||||
### Use PostgresSQL w/ Orthanc
|
||||
|
||||
Orthanc can handle a large amount of data and requests, but if you find that
|
||||
requests start to slow as you add more and more studies, you may want to
|
||||
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
|
||||
that can be found in the
|
||||
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
|
||||
"PostgreSQL and Orthanc inside Docker"
|
||||
|
||||
### Improving This Guide
|
||||
|
||||
Here are some improvements this guide would benefit from, and that we would be
|
||||
more than happy to accept Pull Requests for:
|
||||
|
||||
- SSL Support
|
||||
- Complete configuration with `.env` file (or something similar)
|
||||
- Keycloak Theme improvements
|
||||
- Any security issues
|
||||
- One-click deploy to a cloud provider
|
||||
|
||||
## Resources
|
||||
|
||||
### Misc. Helpful Commands
|
||||
|
||||
_Check if `nginx.conf` is valid:_
|
||||
|
||||
```bash
|
||||
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
|
||||
```
|
||||
|
||||
_Interact w/ running container:_
|
||||
|
||||
`docker exec -it CONTAINER_NAME bash`
|
||||
|
||||
_List running containers:_
|
||||
|
||||
`docker ps`
|
||||
|
||||
_Clear Keycloak DB so you can re-seed values:_
|
||||
|
||||
- `docker volume prune` OR
|
||||
- `docker volume ls` and `docker volume rm VOLUME_NAME VOLUME_NAME`
|
||||
|
||||
### Referenced Articles
|
||||
|
||||
The inspiration for our setup was driven largely by these articles:
|
||||
|
||||
- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx)
|
||||
- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/)
|
||||
- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/)
|
||||
|
||||
For more documentation on the software we've chosen to use, you may find the
|
||||
following resources helpful:
|
||||
|
||||
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
|
||||
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
|
||||
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
|
||||
- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use)
|
||||
|
||||
We chose to use a generic OpenID Connect library on the client, but it's worth
|
||||
noting that Keycloak comes packaged with its own:
|
||||
|
||||
- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps
|
||||
[oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki))
|
||||
- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
|
||||
|
||||
If you're not already drowning in links, here are some good security resources
|
||||
for OAuth:
|
||||
|
||||
- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660)
|
||||
- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code)
|
||||
|
||||
For a different take on this setup, check out the repositories our community
|
||||
members put together:
|
||||
|
||||
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
|
||||
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- DOCS -->
|
||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||
<!-- SRC -->
|
||||
[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json
|
||||
[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Development",
|
||||
"position": 3
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Continous Integration
|
||||
---
|
||||
# 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 -->
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Contributing
|
||||
---
|
||||
# Contributing
|
||||
|
||||
## How can I help?
|
||||
|
||||
Fork the repository, make your change and submit a pull request. If you would
|
||||
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
|
||||
minimum, you may want to read the following documentation:
|
||||
|
||||
- [Getting Started](./getting-started.md)
|
||||
- [Architecture](../architecture/index.md)
|
||||
|
||||
Pull requests that are:
|
||||
|
||||
- Small
|
||||
- [Well tested](./testing.md)
|
||||
- Decoupled
|
||||
|
||||
Are much more likely to get reviewed and merged in a timely manner.
|
||||
|
||||
## When changes impact multiple repositories
|
||||
|
||||
While this can be tricky, we've tried to reduce how often this situation crops
|
||||
up this with our [recent switch to a monorepo][monorepo]. Our maintained
|
||||
extensions, ui components, internationalization library, and business logic can
|
||||
all be developed by simply running `yarn run dev` from the repository root.
|
||||
|
||||
Testing the viewer with locally developed, unpublished package changes from a
|
||||
package outside of the monorepo is most common with extension development. Let's
|
||||
demonstrate how to accomplish this with two commonly forked extension
|
||||
dependencies:
|
||||
|
||||
### `cornerstone-tools`
|
||||
|
||||
On your local file system:
|
||||
|
||||
```bash title="/my-projects/"
|
||||
.
|
||||
├── cornerstonejs/cornerstone-tools
|
||||
└── ohif/viewers
|
||||
```
|
||||
|
||||
- Open a terminal/shell
|
||||
- Navigate to `cornerstonejs/cornerstone-tools`
|
||||
- `npm install`
|
||||
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `npm run dev`
|
||||
- Open a new terminal/shell
|
||||
- Navigate to `ohif/viewers`.
|
||||
- `yarn install`
|
||||
- [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `yarn run dev`
|
||||
|
||||
As you make changed to `cornerstone-tools`, and it's output is rebuilt, you
|
||||
should see the following behavior:
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
If you wish to stop using your local package, run the following commands in the
|
||||
`ohif/viewers` repository root:
|
||||
|
||||
- `yarn unlink cornerstone-tools`
|
||||
- `yarn install --force`
|
||||
|
||||
### `react-vtkjs-viewport`
|
||||
|
||||
On your local file system:
|
||||
|
||||
```bash title="/my-projects/"
|
||||
.
|
||||
├── ohif/react-vtkjs-viewport
|
||||
└── ohif/viewers
|
||||
```
|
||||
|
||||
- Open a terminal/shell
|
||||
- Navigate to `ohif/react-vtkjs-viewport`
|
||||
- `yarn install`
|
||||
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `yarn run start`
|
||||
- Open a new terminal/shell
|
||||
- Navigate to `ohif/viewers`.
|
||||
- `yarn install`
|
||||
- [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `yarn run dev`
|
||||
|
||||
#### Other linkage notes
|
||||
|
||||
We're still working out some of the kinks with local package development as
|
||||
there are a lot of factors that can influence the behavior of our development
|
||||
server and bundler. If you encounter issues not addressed here, please don't
|
||||
hesitate to reach out on GitHub.
|
||||
|
||||
## Any guidance on submitting changes?
|
||||
|
||||
While we do appreciate code contributions, triaging and integrating contributed
|
||||
code changes can be very time consuming. Please consider the following tips when
|
||||
working on your pull requests:
|
||||
|
||||
- Functionality is appropriate for the repository. Consider creating a GitHub
|
||||
issue to discuss your suggested changes.
|
||||
- The scope of the pull request is not too large. Please consider separate pull
|
||||
requests for each feature as big pull requests are very time consuming to
|
||||
understand.
|
||||
|
||||
We will provide feedback on your pull requests as soon as possible. Following
|
||||
the tips above will help ensure your changes are reviewed.
|
||||
|
||||
## Testing contribution pull requests
|
||||
|
||||
OHIF uses [netlify](https://www.netlify.com/) so that pull requests are
|
||||
autogenerated and available for testing.
|
||||
|
||||
For example, [this url][example-url] allows you to test [pull request 237, the
|
||||
request that created this FAQ entry,][pr-237] using data pulled from Amazon S3.
|
||||
|
||||
Replacing the number 237 in the link below with your pull request number should
|
||||
let you test it as well and you can use this link for discussions on github
|
||||
without requiring reviewers to download and build your branch.
|
||||
|
||||
```bash
|
||||
https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
|
||||
```
|
||||
|
||||
If you have made a documentation change, a link like this will let you preview
|
||||
the gitbook generated by the pull request:
|
||||
|
||||
```bash
|
||||
https://deploy-preview-237--ohif.netlify.com/contributing.html
|
||||
```
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
|
||||
[pr-237]: https://github.com/OHIF/Viewers/pull/237
|
||||
[monorepo]: https://github.com/OHIF/Viewers/issues/768
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: Getting Started
|
||||
---
|
||||
# Getting Started
|
||||
|
||||
## Setup
|
||||
|
||||
### Fork & Clone
|
||||
|
||||
If you intend to contribute back changes, or if you would like to pull updates
|
||||
we make to the OHIF Viewer, then follow these steps:
|
||||
|
||||
- [Fork][fork-a-repo] the [OHIF/Viewers][ohif-viewers-repo] repository
|
||||
- [Create a local clone][clone-a-repo] of your fork
|
||||
- `git clone https://github.com/YOUR-USERNAME/Viewers`
|
||||
- Add OHIF/Viewers as a [remote repository][add-remote-repo] labled `upstream`
|
||||
- Navigate to the cloned project's directory
|
||||
- `git remote add upstream https://github.com/OHIF/Viewers.git`
|
||||
|
||||
With this setup, you can now [sync your fork][sync-changes] to keep it
|
||||
up-to-date with the upstream (original) repository. This is called a "Triangular
|
||||
Workflow" and is common for Open Source projects. The GitHub blog has a [good
|
||||
graphic that illustrates this setup][triangular-workflow].
|
||||
|
||||
### Private
|
||||
|
||||
Alternatively, if you intend to use the OHIF Viewer as a starting point, and you
|
||||
aren't as concerned with syncing updates, then follow these steps:
|
||||
|
||||
1. Navigate to the [OHIF/Viewers][ohif-viewers] repository
|
||||
2. Click `Clone or download`, and then `Download ZIP`
|
||||
3. Use the contents of the `.zip` file as a starting point for your viewer
|
||||
|
||||
> NOTE: It is still possible to sync changes using this approach. However,
|
||||
> submitting pull requests for fixes and features are best done with the
|
||||
> separate, forked repository setup described in "Fork & Clone"
|
||||
|
||||
## Developing
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Node.js & NPM](https://nodejs.org/en/)
|
||||
- [Yarn](https://yarnpkg.com/en/)
|
||||
- Yarn workspaces should be enabled:
|
||||
- `yarn config set workspaces-experimental true`
|
||||
|
||||
### Kick the tires
|
||||
|
||||
Navigate to the root of the project's directory in your terminal and run the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
# Restore dependencies
|
||||
yarn install
|
||||
|
||||
# Start local development server
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
You should see the following output:
|
||||
|
||||
```bash
|
||||
@ohif/viewer: i 「wds」: Project is running at http://localhost:3000/
|
||||
@ohif/viewer: i 「wds」: webpack output is served from /
|
||||
@ohif/viewer: i 「wds」: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer
|
||||
@ohif/viewer: i 「wds」: 404s will fallback to /index.html
|
||||
|
||||
# And a list of all generated files
|
||||
```
|
||||
|
||||
### 🎉 Celebrate 🎉
|
||||
|
||||
|
||||
|
||||
|
||||
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
|
||||
<iframe src="https://player.vimeo.com/video/547952862?badge=0&autopause=0&player_id=0&app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Building for Production
|
||||
|
||||
> More comprehensive guides for building and publishing can be found in our
|
||||
> [deployment docs](./../deployment/index.md)
|
||||
|
||||
```bash
|
||||
# Build static assets to host a PWA
|
||||
yarn run build
|
||||
|
||||
# Build packaged output (script-tag use)
|
||||
yarn run build:package
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you receive a _"No Studies Found"_ message and do not see your studies, try
|
||||
changing the Study Date filters to a wider range.
|
||||
- If you see a 'Loading' message which never resolves, check your browser
|
||||
JavaScript console inside the Developer Tools to identify any errors.
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[fork-a-repo]: https://help.github.com/en/articles/fork-a-repo
|
||||
[clone-a-repo]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||
[add-remote-repo]: https://help.github.com/en/articles/fork-a-repo#step-3-configure-git-to-sync-your-fork-with-the-original-spoon-knife-repository
|
||||
[sync-changes]: https://help.github.com/en/articles/syncing-a-fork
|
||||
[triangular-workflow]: https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/#improved-support-for-triangular-workflows
|
||||
[ohif-viewers-repo]: https://github.com/OHIF/Viewers
|
||||
[ohif-viewers]: https://github.com/OHIF/Viewers
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Testing
|
||||
---
|
||||
# Tests
|
||||
|
||||
> Testing is an opinionated topic. Here is a rough overview of our testing
|
||||
> philosiphy. See something you want to discuss or think should be changed? Open
|
||||
> a PR and let's discuss.
|
||||
|
||||
You're an engineer. You know how to write code, and writing tests isn't all that
|
||||
different. But do you know why we write tests? Do you know when to write one, or
|
||||
what kind of test to write? How do you know if a test is a _"good"_ test? This
|
||||
document's goal is to give you the tools you need to make those determinations.
|
||||
|
||||
Okay. So why do we write tests? To increase our... **CONFIDENCE**
|
||||
|
||||
- If I do a large refactor, does everything still work?
|
||||
- If I changed some critical piece of code, is it safe to push to production?
|
||||
|
||||
Gaining the confidence we need to answer these questions after every change is
|
||||
costly. Good tests allow us to answer them without manual regression testing.
|
||||
What and how we choose to test to increase that confidence is nuanced.
|
||||
|
||||
## Kinds of Tests
|
||||
|
||||
Test's buy us confidence, but not all tests are created equal. Each kind of test
|
||||
has a different cost to write and maintain. An expensive test is worth it if it
|
||||
gives us confidence that a payment is processed, but it may not be the best
|
||||
choice for asserting an element's border color.
|
||||
|
||||
| Test Type | Example | Speed | Cost |
|
||||
| ----------- | ------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------ |
|
||||
| Static | `addNums(1, '2')` called with `string`, expected `int`. | :rocket: Instant | :money_with_wings: |
|
||||
| Unit | `addNums(1, 2)` returns expected result `3` | :airplane: Fast | :money_with_wings::money_with_wings: |
|
||||
| Integration | Clicking "Sign In", navigates to the dashboard (mocked network requests) | :running: Okay | :money_with_wings::money_with_wings::money_with_wings: |
|
||||
| End-to-end | Clicking "Sign In", navigates to the dashboard (no mocks) | :turtle: Slow | :money_with_wings::money_with_wings::money_with_wings::money_with_wings: |
|
||||
|
||||
- :rocket: Speed: How quickly tests run
|
||||
- :money_with_wings: Cost: Time to write, and to debug when broken (more points
|
||||
of failure)
|
||||
|
||||
### Static Code Analysis
|
||||
|
||||
Modern tooling gives us this "for free". It can catch invalid regular
|
||||
expressions, unused variables, and guarantee we're calling methods/functions
|
||||
with the expected paramater types.
|
||||
|
||||
Example Tooling:
|
||||
|
||||
- [ESLint][eslint-rules]
|
||||
- [TypeScript][typescript-docs] or [Flow][flow-org]
|
||||
|
||||
### Unit Tests
|
||||
|
||||
The building blocks of our libraries and applications. For these, you'll often
|
||||
be testing a single function or method. Conceptually, this equates to:
|
||||
|
||||
_Pure Function Test:_
|
||||
|
||||
- If I call `sum(2, 2)`, I expect the output to be `4`
|
||||
|
||||
_Side Effect Test:_
|
||||
|
||||
- If I call `resetViewport(viewport)`, I expect `cornerstone.reset` to be called
|
||||
with `viewport`
|
||||
|
||||
#### When to use
|
||||
|
||||
Anything that is exposed as public API should have unit tests.
|
||||
|
||||
#### When to avoid
|
||||
|
||||
You're actually testing implementation details. You're testing implementation
|
||||
details if:
|
||||
|
||||
- Your test does something that the consumer of your code would never do.
|
||||
- IE. Using a private function
|
||||
- A refactor can break your tests
|
||||
|
||||
### Integration Tests
|
||||
|
||||
We write integration tests to gain confidence that several units work together.
|
||||
Generally, we want to mock as little as possible for these tests. In practice,
|
||||
this means only mocking network requests.
|
||||
|
||||
#### When to use
|
||||
|
||||
...
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
These are the most expensive tests to write and maintain. Largely because, when
|
||||
they fail, they have the largest number of potential points of failure. So why
|
||||
do we write them? Because they also buy us the most confidence.
|
||||
|
||||
#### When to use
|
||||
|
||||
Mission critical features and functionality, or to cover a large breadth of
|
||||
functionality until unit tests catch up. Unsure if we should have a test for
|
||||
feature `X` or scenario `Y`? Open an issue and let's discuss.
|
||||
|
||||
## Summary
|
||||
|
||||
- Does your test increase confidence?
|
||||
- Does the test type chosen balance the cost-to-confidence ratio?
|
||||
|
||||
## Further Reading
|
||||
|
||||
### General
|
||||
|
||||
- [Assert(js) Conf 2018 Talks][assert-js-talks]
|
||||
- [Write tests. Not too many. Mostly integration.][kent-talk] - Kent C. Dodds
|
||||
- [I see your point, but…][gleb-talk] - Gleb Bahmutov
|
||||
- [Static vs Unit vs Integration vs E2E Testing][kent-blog] - Kent C. Dodds
|
||||
(Blog)
|
||||
|
||||
### End-to-end Testing w/ Cypress
|
||||
|
||||
- [Getting Started](https://docs.cypress.io/guides/overview/why-cypress.html)
|
||||
- Be sure to check out `Getting Started` and `Core Concepts`
|
||||
- [Best Practices](https://docs.cypress.io/guides/references/best-practices.html)
|
||||
- [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html)
|
||||
|
||||
## Testing Dorito
|
||||
|
||||
[![testing dorito][testing-dorito-img]][testing-dorito]
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[eslint-rules]: https://eslint.org/docs/rules/
|
||||
[typescript-docs]: https://www.typescriptlang.org/docs/home.html
|
||||
[flow-org]: https://flow.org/
|
||||
<!-- Talks -->
|
||||
[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw
|
||||
[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE
|
||||
[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk
|
||||
[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests
|
||||
<!-- Images -->
|
||||
[testing-trophy]: https://twitter.com/kentcdodds/status/960723172591992832?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E960723172591992832&ref_url=https%3A%2F%2Fkentcdodds.com%2Fblog%2Fwrite-tests
|
||||
[aaron-square]: https://twitter.com/Carofine247/status/966727489274961920
|
||||
[gleb-pyramid]: https://twitter.com/Carofine247/status/966764532046684160/photo/3
|
||||
[testing-pyramid]: https://dojo.ministryoftesting.com/dojo/lessons/the-mobile-test-pyramid
|
||||
[testing-dorito]: https://twitter.com/denvercoder/status/960752578198843392
|
||||
[testing-dorito-img]: https://pbs.twimg.com/media/DVVHXycUMAAcN-F?format=jpg&name=4096x4096
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Extensions",
|
||||
"position": 6
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Overview
|
||||
---
|
||||
# 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](#registering-at-runtime) and at
|
||||
[build time](#registering-at-build-time).
|
||||
|
||||
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](../viewer/configuration.md) 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 />` |
|
||||
|
||||
<span style={{"textAlign": 'center', 'fontStyle': 'italic'}}>Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</span>
|
||||
|
||||
### 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,
|
||||
hotkeysManager
|
||||
});
|
||||
|
||||
// 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` along with the
|
||||
application's configuration through the appConfig key (optional).
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager();
|
||||
const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
appConfig,
|
||||
});
|
||||
```
|
||||
|
||||
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 the extension's
|
||||
config, the application's config and `ExtensionManager`'s `ServicesManager` and
|
||||
`CommandsManager` instances.
|
||||
|
||||
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.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Extension</th>
|
||||
<th>Description</th>
|
||||
<th>Modules</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-cornerstone">
|
||||
Cornerstone
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
|
||||
VTK.js
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders text and HTML content for <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12">specific SopClassUIDs</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-segmentation">DICOM Segmentation</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders segmentation images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Panel, Toolbar</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--
|
||||
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,4 @@
|
||||
{
|
||||
"label": "LifeCycle",
|
||||
"position": 2
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Pre Registration
|
||||
---
|
||||
# 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,4 @@
|
||||
{
|
||||
"label": "Modules",
|
||||
"position": 3
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Commands
|
||||
---
|
||||
# Module: Commands
|
||||
|
||||
|
||||
## 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,65 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Panel
|
||||
---
|
||||
# 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',
|
||||
// UI Hint; If the target panel is in a "disabled" state
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
},
|
||||
// Overrides `defaultContext`, if specified
|
||||
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'target-component-id',
|
||||
component: MyComponent,
|
||||
},
|
||||
],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_label: SOP Class Handler
|
||||
---
|
||||
# Module: SOP Class Handler
|
||||
|
||||
An extension can register a [SOP Class][sop-class-link] Handler Module by
|
||||
defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]
|
||||
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,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### More examples :
|
||||
|
||||
- [Dicom-HTML SOP][dicom-html-sop]
|
||||
- [Dicom-PDF SOP][dicom-pdf-sop]
|
||||
- [Dicom-Microscopy SOP][dicom-micro-sop]
|
||||
- [Dicom-Segmentation SOP][dicom-seg-sop]
|
||||
|
||||
## `@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`.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
|
||||
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
|
||||
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
|
||||
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
|
||||
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
sidebar_label: Toolbar
|
||||
---
|
||||
# Module: Toolbar
|
||||
|
||||
An extension can register a Toolbar Module by defining a `getToolbarModule`
|
||||
method. This module is commonly used to define:
|
||||
|
||||
- [Module: Toolbar](#module-toolbar)
|
||||
- [Example Toolbar Module](#example-toolbar-module)
|
||||
- [Button Definitions](#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,50 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
sidebar_label: Viewport
|
||||
---
|
||||
# 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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "FAQ",
|
||||
"position": 10
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
# Browser Support
|
||||
|
||||
The browsers that we support are specified in the `.browserlistrc` file located
|
||||
in the `platform/viewer` project. While we leverage the latest language features
|
||||
when writing code, we rely on `babel` to _transpile_ our code so that it can run
|
||||
in the browsers that we support.
|
||||
|
||||
## In Practice
|
||||
|
||||
The OHIF Viewer is capable of _running_ on:
|
||||
|
||||
- IE 11
|
||||
- FireFox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Edge
|
||||
|
||||
However, we do not have the resources to adequately test and maintain bug free
|
||||
functionality across all of these. In order to push web based medical imaging
|
||||
forward, we focus our development efforts on recent version of modern evergreen
|
||||
browsers.
|
||||
|
||||
Our support of older browsers equates to our willingness to review PRs for bug
|
||||
fixes, and target their minimum JS support whenever possible.
|
||||
|
||||
### Polyfills
|
||||
|
||||
> A polyfill, or polyfiller, is a piece of code (or plugin) that provides the
|
||||
> technology that you, the developer, expect the browser to provide natively.
|
||||
|
||||
An example of a polyfill is that you expect `Array.prototype.filter` to exist,
|
||||
but for some reason, the browser that's being used has not implemented that
|
||||
language feature yet. Our earlier transpilation will rectify _syntax_
|
||||
discrepencies, but unimplemented features require a "temporary" implementation.
|
||||
That's where polyfills step in.
|
||||
|
||||
You can utilize a service like [polyfill.io](https://polyfill.io/v3/) to
|
||||
auto-detect and apply polyfills as needed, or you can update the PWA build to
|
||||
include polyfill's in your bundle by incorporating [core-js][core-js]
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[core-js]: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Overview
|
||||
|
||||
---
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Index
|
||||
|
||||
- [Report a bug][report-bug]
|
||||
- [Request a feature][new-feature]
|
||||
- [Commercial Support & Consulting][commercial-support]
|
||||
- [Academic collaborations][academic]
|
||||
- [FDA Clearance or CE Marking][fda-clearance]
|
||||
- [HIPAA Compliance][hipaa]
|
||||
|
||||
### How do I report a bug?
|
||||
|
||||
Navigate to our [GitHub Repository][new-issue], and submit a new bug report.
|
||||
Follow the steps outlined in the [Bug Report Template][bug-report-template].
|
||||
|
||||
### How can I request a new feature?
|
||||
|
||||
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
|
||||
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](development/contributing.md).
|
||||
If you have resources and would like to fund the development of a feature,
|
||||
please [contact us](http://www.ohif.org) or work with community members that
|
||||
offer [consulting services][commercial-support].
|
||||
|
||||
### Who should I contact about Academic Collaborations?
|
||||
|
||||
[Gordon J. Harris](http://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/)
|
||||
at Massachusetts General Hospital is the primary contact for any academic
|
||||
collaborators. We are always happy to hear about new groups interested in using
|
||||
the OHIF framework, and may be able to provide development support if the
|
||||
proposed collaboration has an impact on cancer research.
|
||||
|
||||
### Does OHIF offer commercial support?
|
||||
|
||||
The Open Health Imaging Foundation does not offer commercial support, however,
|
||||
some community members do offer consulting services. The following contacts may
|
||||
be useful:
|
||||
|
||||
- Rob Lewis ([Radical Imaging](http://radicalimaging.com/))
|
||||
|
||||
**Please file a Pull Request if you wish to add your name or organization to
|
||||
this list.**
|
||||
|
||||
### Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission?
|
||||
|
||||
**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users
|
||||
responsibility to ensure compliance with applicable rules and regulations. The
|
||||
[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF
|
||||
Platform does not prevent your company or group from seeking F.D.A. clearance
|
||||
for a product built using the platform.
|
||||
|
||||
If you have gone this route (or are going there), please let us know because we
|
||||
would be interested to hear about your experience.
|
||||
|
||||
### Is The OHIF Viewer [HIPAA][hipaa-def] Compliant?
|
||||
|
||||
**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA
|
||||
Compliant. It is the users responsibility to ensure compliance with applicable
|
||||
rules and regulations.
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- INDEX -->
|
||||
[report-bug]: #how-do-i-report-a-bug
|
||||
[new-feature]: #how-can-i-request-a-new-feature
|
||||
[commercial-support]: #does-ohif-offer-commercial-support
|
||||
[academic]: #who-should-i-contact-about-academic-collaborations
|
||||
[fda-clearance]: #does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission
|
||||
[hipaa]: #is-the-ohif-viewer-hipaa-compliant
|
||||
<!-- OTHER -->
|
||||
[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/
|
||||
[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en
|
||||
[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act
|
||||
[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose
|
||||
[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title=
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
# PWA vs Packaged
|
||||
|
||||
It's important to know that the OHIF Viewer project provides two different build
|
||||
processes:
|
||||
|
||||
```bash
|
||||
# Static Asset output: For deploying PWAs
|
||||
yarn run build
|
||||
|
||||
# Single `.js` script, for embedding viewer into existing apps
|
||||
yarn run build:package
|
||||
```
|
||||
|
||||
## Progressive Web Application (PWA)
|
||||
|
||||
> [Progressive Web Apps][pwa] are a new breed of web applications that meet the
|
||||
> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us
|
||||
> provide a reliable, fast, and engaging experience across different devices and
|
||||
> network conditions.
|
||||
|
||||
The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build
|
||||
the many small static assets that comprise our application. Also generated is an
|
||||
`index.html` that will serve as an entry point for loading configuration and the
|
||||
application, as well as a `service-worker` that can intelligently cache files so
|
||||
that subsequent requests are from the local file system instead of over the
|
||||
network.
|
||||
|
||||
You can read more about this particular strategy in our
|
||||
[Build for Production Deployment Guide](./../deployment/recipes/build-for-production.md)
|
||||
|
||||
## Commonjs Bundle (Packaged Script)
|
||||
|
||||
The [@ohif/viewer][viewer-npm] package is built with WebPack to provide a React
|
||||
component that can be dropped into a larger application. The `OHIFViewer`
|
||||
component is the entire viewer, configurable via React `props`. This is useful
|
||||
for including the OHIF Viewer in a larger web application, as the entire
|
||||
application can be provided via a `<script>` tag with no build process required.
|
||||
|
||||
The bundle is not as performant or as optimized as the PWA build. It includes
|
||||
fonts, styles, and the core extensions. If you find yourself facing performance
|
||||
issues, you may wish to tweak what's included in this bundle or switch to the
|
||||
PWA build.
|
||||
|
||||
You can read more about this particular strategy in our
|
||||
[Embedded Viewer Deployment Guide](./../deployment/recipes/embedded-viewer.md)
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
||||
[pwa-checklist]: https://developers.google.com/web/progressive-web-apps/checklist
|
||||
[monorepo]: https://github.com/OHIF/Viewers/issues/768
|
||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
# Scope of Project
|
||||
|
||||
The OHIF Viewer is a web based medical imaging viewer. This allows it to be used
|
||||
on almost any device, anywhere. The OHIF Viewer is what is commonly reffered to
|
||||
as a ["Dumb Client"][simplicable]
|
||||
|
||||
> A dumb client is software that fully depends on a connection to a server or
|
||||
> cloud service for its functionality. Without a network connection, the
|
||||
> software offers nothing useful. - [simplicable.com][simplicable]
|
||||
|
||||
While the Viewer persists some data, it's scope is limited to caching things
|
||||
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
|
||||
data source.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
- From the local networok
|
||||
- From a remote web server
|
||||
- From a CDN (content delivery network)
|
||||
- From a service-worker's cache
|
||||
- etc.
|
||||
|
||||
These "static asset" files are referred to collectively as a "Progressive Web
|
||||
Application" (PWA), and have the same capabilities and limitations that all PWAs
|
||||
have.
|
||||
|
||||
All studies, series, images, imageframes, metadata, and the images themselves
|
||||
must come from an external source. There are many, many ways to provide this
|
||||
information, the OHIF Viewer's scope **DOES NOT** encompass providing _any_
|
||||
data; only the configuration necessary to interface with one or more of these
|
||||
many data sources. The OHIF Viewer's scope **DOES** include configuration and
|
||||
support for services that are protected with OpenID-Connect.
|
||||
|
||||
In an effort to aide our users and contributors, we attempt to provide several
|
||||
[deployment and hosting recipes](./deployment/index.md) as potential starting
|
||||
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
|
||||
taste, preferences, etc.
|
||||
|
||||
## FAQ
|
||||
|
||||
_Am I able to cache studies for offline viewing?_
|
||||
|
||||
Not currently. A web page's offline cache capabilities are limited and somewhat
|
||||
volatile (mostly imposed at the browser vendor level). For more robust offline
|
||||
caching, you may want to consider a server on the local network, or packaging
|
||||
the OHIF Viewer as a desktop application.
|
||||
|
||||
_Does the OHIF Viewer work with the local filesystem?_
|
||||
|
||||
It is possible to accomplish this through extensions; however, for an user
|
||||
experience that accomodates a large number of studies, you would likely need to
|
||||
package the OHIF Viewer as an [Electron app][electron].
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[simplicable]: https://simplicable.com/new/dumb-client
|
||||
[electron]: https://electronjs.org/
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,39 @@
|
||||
# Help
|
||||
|
||||
We all need a little help sometimes. Don't let a few roadblocks stand in the way
|
||||
of you building something awesome.
|
||||
|
||||
## Community Support
|
||||
|
||||
If you're a developer looking to contribute code, documentation, or discussion;
|
||||
we are more than happy to help provide clarification and answer questions via
|
||||
[GitHub issues][gh-issues] or our [community forum][google-group]. Regular
|
||||
contributors may also be invited to join our Slack Group to streamline
|
||||
discussion.
|
||||
|
||||
For bug reports and feature requests (including incomplete or confusing
|
||||
documentation), [GitHub issues][gh-issues] continue to be your best avenue of
|
||||
communication.
|
||||
|
||||
Complex issues specific to your organization/situation are still okay to post,
|
||||
but they're less likely to receive a response. Unfortunately, we have limited
|
||||
resources and must be judicious with how we allocate them. If you find yourself
|
||||
in this situation and in need of assistance, it may be in your best interest to
|
||||
persue paid support.
|
||||
|
||||
## Commercial Support
|
||||
|
||||
The Open Health Imaging Foundation does not offer commercial support, however,
|
||||
some community members do offer consulting services:
|
||||
|
||||
- Rob Lewis ([Radical Imaging](http://radicalimaging.com/))
|
||||
|
||||
**Please file a Pull Request if you wish to add your name or organization to
|
||||
this list.**
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues/
|
||||
[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Our process
|
||||
---
|
||||
# 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
|
||||
|
||||
## 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.
|
||||
|
||||
> 🖋 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]: ./development/contributing.md
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Services",
|
||||
"position": 7
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Default Services
|
||||
---
|
||||
# Services (default)
|
||||
|
||||
- [Services (default)](#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();
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Overview
|
||||
---
|
||||
# Services Overview
|
||||
|
||||
## 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]: ../extensions/modules/commands.md
|
||||
[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,4 @@
|
||||
{
|
||||
"label": "UI Services",
|
||||
"position": 3
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Overview
|
||||
---
|
||||
# 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.
|
||||
|
||||
<!-- <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> -->
|
||||
|
||||

|
||||
<!-- <span style={{"textAlign": 'center', 'fontStyle': 'italic', 'paddingBottom':'10px'}}>Diagram showing relationship between React Context and UI Service</span> -->
|
||||
|
||||
|
||||
|
||||
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> -->
|
||||
|
||||

|
||||
<span style={{"textAlign": 'center', 'fontStyle': 'italic'}}>GIF showing successful call of UINotificationService from an extension.</span>
|
||||
|
||||
## 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,53 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
# 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,55 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
# 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,55 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
# 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,4 @@
|
||||
{
|
||||
"label": "Viewer",
|
||||
"position": 5
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: Configuration
|
||||
---
|
||||
# Viewer: Configuration
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
We maintain a number of common viewer application configurations at
|
||||
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
|
||||
passed to the viewer depend on how it's deployed, but the two most common paths
|
||||
are:
|
||||
|
||||
- `index.html` looks for `https://your-website.com/app-config.js` OR
|
||||
- `index.html` passes the values to `OHIF.installViewer()`
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
/**
|
||||
* "White Labeling" is used to change the branding, look, and feel of the OHIF
|
||||
* Viewer. These settings, and the color variables that are used by our components,
|
||||
* are the easiest way to rebrand the application.
|
||||
*
|
||||
* More extensive changes are made possible through swapping out the UI library,
|
||||
* Viewer project, or extensions.
|
||||
*/
|
||||
whiteLabeling: {
|
||||
/* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement('a', {
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'header-brand',
|
||||
href: '/',
|
||||
style: {
|
||||
display: 'block',
|
||||
textIndent: '-9999px',
|
||||
background: 'url(/svg-file-hosted-at-domain-root.svg)',
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: '200px',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Internally, the OHIF Viewer fetches data primarily with the
|
||||
* `cornerstoneWADOImageLoader` and the `DICOMWebClient`. If either of these
|
||||
* receive a non-200 response, this method allows you to handle that error.
|
||||
*
|
||||
* Common use cases include:
|
||||
* - Showing a notification with the UINotificationService
|
||||
* - Redirecting the user
|
||||
* - Refreshing an auth token
|
||||
*
|
||||
* @param {Object} error - JS new Error()
|
||||
* @param {XMLHttpRequest} error.request - The XHR request that's onreadystate change triggered this callback
|
||||
* @param {string} error.response - The XHR's response property
|
||||
* @param {number} error.status - The XHR's status property
|
||||
*/
|
||||
httpErrorHandler: error => {
|
||||
const { request: xhr, response, status } = err;
|
||||
const { responseType, statusText } = xhr;
|
||||
|
||||
// In local files, status is 0 upon success in Firefox
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
console.log(statusText, response, responseType);
|
||||
} else {
|
||||
console.warn('Likely CORS error');
|
||||
}
|
||||
},
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
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',
|
||||
enableStudyLazyLoad: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Supported Keys: https://craig.is/killing/mice
|
||||
hotkeys: [
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
||||
{
|
||||
commandName: 'flipViewportVertical',
|
||||
label: 'Flip Horizontally',
|
||||
keys: ['h'],
|
||||
},
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
label: 'Flip Vertically',
|
||||
keys: ['v'],
|
||||
},
|
||||
],
|
||||
/* Configuration passed to the bundled cornerstone extension
|
||||
*
|
||||
* The cornerstone extension is currently tightly coupled to the platform.
|
||||
* Until we're able to decouple it, this key will serve as a workaround to
|
||||
* pass it configuration.
|
||||
*/
|
||||
cornerstoneExtensionConfig: {
|
||||
/* Whether to show/hide annotation "handles" */
|
||||
hideHandles: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_label: Environment Variables
|
||||
---
|
||||
# Environment Variables
|
||||
|
||||
There are a number of environment variables we use at build time to influence the output application's behavior.
|
||||
|
||||
```bash
|
||||
# Application
|
||||
NODE_ENV=< production | development >
|
||||
DEBUG=< true | false >
|
||||
APP_CONFIG=< relative path to application configuration file >
|
||||
PUBLIC_URL=<>
|
||||
VERSION_NUMBER=<Set by CircleCI>
|
||||
BUILD_NUM=<Set by CircleCI>
|
||||
# i18n
|
||||
USE_LOCIZE=<false>
|
||||
LOCIZE_PROJECTID=<ProjectID to pull translations for>
|
||||
LOCIZE_API_KEY=<To enable Locize live editing of translations>
|
||||
```
|
||||
|
||||
## Setting Environment Variables
|
||||
|
||||
- `npx cross-env`
|
||||
- `.env` files
|
||||
- env variables on build machine, or for terminal session
|
||||
@@ -0,0 +1,326 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
sidebar_label: Internationalization
|
||||
---
|
||||
# Viewer: Internationalization
|
||||
|
||||
OHIF supports internationalization using [i18next](https://www.i18next.com/)
|
||||
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
|
||||
where is the main instance of i18n containing several languages and tools.
|
||||
|
||||
<!-- <div class='row'>
|
||||
<div class='column'>
|
||||
</div>
|
||||
<div class='column'>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div className="text--center">
|
||||
<p>Our translation management is powered by
|
||||
<a href="https://locize.com/" target="_blank" rel="noopener noreferrer"> Locize </a>
|
||||
through their generous support of open source.</p>
|
||||
<a href="https://locize.com/" target="_blank" rel="noopener noreferrer" style={{padding: '20px'}}>
|
||||
<img style={{width:'70px'}} src="https://pbs.twimg.com/profile_images/909709940910120961/oyB0mX2L.jpg" alt="Locize Translation Management Logo"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
yarn add @ohif/i18n
|
||||
|
||||
# OR
|
||||
|
||||
npm install --save @ohif/i18n
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
After installing `@ohif/i18n` npm package, the translation function
|
||||
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
|
||||
[without](#without-react) React.
|
||||
|
||||
A translation will occur every time a text match happens in a
|
||||
[t](https://www.i18next.com/overview/api#t) function.
|
||||
|
||||
The [t](https://www.i18next.com/overview/api#t) function is responsible for
|
||||
getting translations using all the power of i18next.
|
||||
|
||||
E.g.
|
||||
|
||||
Before:
|
||||
|
||||
```html
|
||||
<div>my translated text</div>
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```html
|
||||
<div>{t('my translated text')}</div>
|
||||
```
|
||||
|
||||
If the translation.json file contains a key that matches the HTML content e.g.
|
||||
`my translated text`, it will be replaced automatically by the
|
||||
[t](https://www.i18next.com/overview/api#t) function.
|
||||
|
||||
---
|
||||
|
||||
### With React
|
||||
|
||||
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)
|
||||
function easily.
|
||||
|
||||
#### Using HOCs
|
||||
|
||||
In most cases we used
|
||||
[High Order Components](https://react.i18next.com/latest/withtranslation-hoc) to
|
||||
share the `t` function among OHIF's components.
|
||||
|
||||
E.g.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { withTranslation } from '@ohif/i18n';
|
||||
|
||||
function MyComponent({ t, i18n }) {
|
||||
return <p>{t('my translated text')}</p>;
|
||||
}
|
||||
|
||||
export default withTranslation('MyNameSpace')(MyComponent);
|
||||
```
|
||||
|
||||
> Important: if you are using React outside the OHIF Viewer, check the
|
||||
> [I18nextProvider](#using-outside-of-ohif-viewer) section, `withTranslation`
|
||||
> HOC doesnt works without a I18nextProvider
|
||||
|
||||
#### Using Hooks
|
||||
|
||||
Also, it's possible to get the `t` tool using
|
||||
[React Hooks](https://react.i18next.com/latest/usetranslation-hook), but it
|
||||
requires at least React > 16.8 😉
|
||||
|
||||
### Using outside of OHIF viewer
|
||||
|
||||
OHIF Viewer already sets a main
|
||||
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
|
||||
the shared i18n instance from `@ohif/i18n`, all extensions inside OHIF Viewer
|
||||
will share this same provider at the end, you don't need to set new providers at
|
||||
all.
|
||||
|
||||
But, if you need to use it completely outside of OHIF viewer, you can set the
|
||||
I18nextProvider this way:
|
||||
|
||||
```js
|
||||
import i18n from '@ohif/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import App from './App';
|
||||
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<App />
|
||||
</I18nextProvider>;
|
||||
```
|
||||
|
||||
After setting `I18nextProvider` in your React App, all translations from
|
||||
`@ohif/i18n` should be available following the basic [With React](#with-react)
|
||||
usage.
|
||||
|
||||
---
|
||||
|
||||
### Without React
|
||||
|
||||
When needed, you can also use available translations _without React_.
|
||||
|
||||
E.g.
|
||||
|
||||
```js
|
||||
import { T } from '@ohif/i18n';
|
||||
console.log(T('my translated text'));
|
||||
console.log(T('$t(Common:Play) my translated text'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Main Concepts While Translating
|
||||
|
||||
## Namespaces
|
||||
|
||||
Namespaces are being used to organize translations in smaller portions, combined
|
||||
semantically or by use. Each `.json` file inside `@ohif/i18n` npm package
|
||||
becomes a new namespace automatically.
|
||||
|
||||
- Buttons: All buttons translations
|
||||
- CineDialog: Translations for the toll tips inside the Cine Player Dialog
|
||||
- Common: all common jargons that can be reused like `t('$t(common:image)')`
|
||||
- Header: translations related to OHIF's Header Top Bar
|
||||
- MeasurementTable - Translations for the `@ohif/ui` Measurement Table
|
||||
- UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal
|
||||
|
||||
### How to use another NameSpace inside the current NameSpace?
|
||||
|
||||
i18next provides a parsing feature able to get translations strings from any
|
||||
NameSpace, like this following example getting data from `Common` NameSpace:
|
||||
|
||||
```
|
||||
$t(Common:Reset)
|
||||
```
|
||||
|
||||
## Extending Languages in @ohif/i18n
|
||||
|
||||
Sometimes, even using the same language, some nouns or jargons can change
|
||||
according to the country, states or even from Hospital to Hospital.
|
||||
|
||||
In this cases, you don't need to set an entire language again, you can extend
|
||||
languages creating a new folder inside a pre existent language folder and
|
||||
@ohif/i18n will do the hard work.
|
||||
|
||||
This new folder must to be called with a double character name, like the `UK` in
|
||||
the following file tree:
|
||||
|
||||
```bash
|
||||
|-- src
|
||||
|-- locales
|
||||
index.js
|
||||
|-- en
|
||||
|-- Buttons.json
|
||||
index.js
|
||||
| UK
|
||||
|-- Buttons.js
|
||||
indes.js
|
||||
| US
|
||||
|-- Buttons.js
|
||||
index.js
|
||||
...
|
||||
```
|
||||
|
||||
All properties inside a Namespace will be merged in the new sub language, e.g
|
||||
`en-US` and `en-UK` will merge the props with `en`, using i18next's fallback
|
||||
languages tool.
|
||||
|
||||
You will need to export all Json files in your `index.js` file, mounting an
|
||||
object like this:
|
||||
|
||||
```json
|
||||
{
|
||||
en: {
|
||||
NameSpace: {
|
||||
keyWord1: 'keyWord1Translation',
|
||||
keyWord2: 'keyWord2Translation',
|
||||
keyWord3: 'keyWord3Translation',
|
||||
}
|
||||
},
|
||||
'en-UK': {
|
||||
NameSpace: {
|
||||
keyWord1: 'keyWord1DifferentTranslation',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Please check the `index.js` files inside locales folder for an example of this
|
||||
exporting structure.
|
||||
|
||||
### Extending languages dynamically
|
||||
|
||||
You have access to the i18next instance, so you can use the
|
||||
[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init)
|
||||
method to add and change language resources as needed.
|
||||
|
||||
E.g.
|
||||
|
||||
```js
|
||||
import { i18n } from '@ohif/i18n';
|
||||
i18next.addResourceBundle('pt-BR', 'Buttons', {
|
||||
Angle: 'Ângulo',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### How to set a whole new language
|
||||
|
||||
To set a brand new language you can do it in two different ways:
|
||||
|
||||
- Opening a pull request for `@ohif/i18n` and sharing the translation with the
|
||||
community. 😍 Please see [Contributing](#contributing-with-new-languages)
|
||||
section for further information.
|
||||
|
||||
- Setting it only in your project or extension:
|
||||
|
||||
You'll need a a final object like the following, what is setting French as
|
||||
language, and send it to `addLocales` method.
|
||||
|
||||
```js
|
||||
const newLanguage =
|
||||
{
|
||||
fr: {
|
||||
Commons: {
|
||||
"Reset": "Réinitialiser",
|
||||
"Previous": "Précédent",
|
||||
},
|
||||
Buttons: {
|
||||
"Rectangle": "Rectangle",
|
||||
"Circle": "Cercle",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To make it easier to translate, you can copy the .json files in the /locales
|
||||
folder and theirs index.js exporters, keeping same keys and NameSpaces.
|
||||
Importing the main index.js file, will provide you an Object as expected by the
|
||||
method `addlocales`;
|
||||
|
||||
E.g. of `addLocales` usage
|
||||
|
||||
```js
|
||||
import { addLocales } from '@ohif/i18n';
|
||||
import locales from './locales/index.js';
|
||||
addLocales(locales);
|
||||
```
|
||||
|
||||
You can also set them manually, one by one, using this
|
||||
[method](#extending-languages-dynamically).
|
||||
|
||||
---
|
||||
|
||||
## Language Detections
|
||||
|
||||
@ohif/i18n uses
|
||||
[i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector)
|
||||
to manage detections, also exports a method called initI18n that accepts a new
|
||||
detector config as parameter.
|
||||
|
||||
### Changing the language
|
||||
|
||||
OHIF Viewer accepts a query param called `lng` in the url to change the
|
||||
language.
|
||||
|
||||
E.g.
|
||||
|
||||
```
|
||||
https://docs.ohif.org/demo/?lng=es-MX
|
||||
```
|
||||
|
||||
### Language Persistence
|
||||
|
||||
The user's language preference is kept automatically by the detector and stored
|
||||
at a cookie called 'i18next', and in a localstorage key called 'i18nextLng'.
|
||||
These names can be changed with a new
|
||||
[Detector Config](https://github.com/i18next/i18next-browser-languageDetector).
|
||||
|
||||
## Debugging translations
|
||||
|
||||
There is an environment variable responsible for debugging the translations,
|
||||
called `REACT_APP_I18N_DEBUG`.
|
||||
|
||||
Run the project as following to get full debug information:
|
||||
|
||||
```bash
|
||||
REACT_APP_I18N_DEBUG=true yarn run dev
|
||||
```
|
||||
|
||||
### Contributing with new languages
|
||||
|
||||
Contributions of any kind are welcome! Please check the
|
||||
[instructions](../development/contributing.md).
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: Themeing
|
||||
---
|
||||
# Viewer: Themeing
|
||||
|
||||
Themeing is currently accomplished with color variables that are defined within
|
||||
the [`:root`](https://css-tricks.com/almanac/selectors/r/root/) selector
|
||||
(allowing them to cascade across all elements). This repository's components,
|
||||
and the ones we consume from our
|
||||
[`@ohif/ui` component library](https://react.ohif.org/styling-and-theming)
|
||||
utilize them. We are interested in pursuing more robust themeing options, and
|
||||
open to pull requests and discussion issues.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Interface UI Colors */
|
||||
--default-color: #9ccef9;
|
||||
--hover-color: #ffffff;
|
||||
--active-color: #20a5d6;
|
||||
--ui-border-color: #44626f;
|
||||
--ui-border-color-dark: #3c5d80;
|
||||
--ui-border-color-active: #00a4d9;
|
||||
--primary-background-color: #000000;
|
||||
--box-background-color: #3e5975;
|
||||
|
||||
--text-primary-color: #ffffff;
|
||||
--text-secondary-color: #91b9cd;
|
||||
--input-background-color: #2c363f;
|
||||
--input-placeholder-color: #d3d3d3;
|
||||
|
||||
--table-hover-color: #2c363f;
|
||||
--table-text-primary-color: #ffffff;
|
||||
--table-text-secondary-color: #91b9cd;
|
||||
|
||||
--large-numbers-color: #6fbde2;
|
||||
|
||||
--state-error: #ffcccc;
|
||||
--state-error-border: #ffcccc;
|
||||
--state-error-text: #ffcccc;
|
||||
|
||||
/* Common palette */
|
||||
--ui-yellow: #e29e4a;
|
||||
--ui-sky-blue: #6fbde2;
|
||||
|
||||
/* State palette */
|
||||
--ui-state-error: #ffcccc;
|
||||
--ui-state-error-border: #993333;
|
||||
--ui-state-error-text: #661111;
|
||||
--ui-gray-lighter: #436270;
|
||||
--ui-gray-light: #516873;
|
||||
--ui-gray: #263340;
|
||||
--ui-gray-dark: #16202b;
|
||||
--ui-gray-darker: #151a1f;
|
||||
--ui-gray-darkest: #14202a;
|
||||
|
||||
--calendar-day-color: #d3d3d3;
|
||||
--calendar-day-border-color: #d3d3d3;
|
||||
--calendar-day-active-hover-background-color: #516873;
|
||||
--calendar-main-color: #263340;
|
||||
--viewport-border-thickness: 1px;
|
||||
}
|
||||
```
|
||||
|
||||
## White Labeling
|
||||
|
||||
> A white-label product is a product or service produced by one company (the
|
||||
> producer) that other companies (the marketers) rebrand to make it appear as if
|
||||
> they had made it - [Wikipedia: White-Label Product][wikipedia]
|
||||
|
||||
Current white-labeling options are limited. We expose the ability to replace the
|
||||
"Logo" section of the application with a custom "Logo" component. You can do
|
||||
this by adding a `whiteLabeling` key to your
|
||||
[configuration file](./configuration.md).
|
||||
|
||||
```js
|
||||
function RadicalImagingLogo(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'header-brand',
|
||||
href: 'http://radicalimaging.com',
|
||||
},
|
||||
React.createElement('h5', {}, 'RADICAL IMAGING')
|
||||
);
|
||||
}
|
||||
|
||||
props.whiteLabeling = {
|
||||
createLogoComponentFn: RadicalImagingLogo,
|
||||
};
|
||||
```
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[wikipedia]: https://en.wikipedia.org/wiki/White-label_product
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version-2.0/tutorialSidebar": [
|
||||
{
|
||||
"type": "autogenerated",
|
||||
"dirName": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
[
|
||||
"2.0",
|
||||
"1.0"
|
||||
]
|
||||