diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7c063f..452c13d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,18 @@ env: docker_repository: nlpsandbox/phi-deidentifier-app jobs: + build: + env: + working-directory: ./client + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install modules + working-directory: ${{env.working-directory}} + run: npm ci + - name: Run ESLint + working-directory: ${{env.working-directory}} + run: npx eslint docker: # needs: [test] runs-on: ubuntu-latest diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml new file mode 100644 index 0000000..3a6ec77 --- /dev/null +++ b/client/.eslintrc.yml @@ -0,0 +1,21 @@ +env: + browser: true + es2021: true +extends: + - 'plugin:react/recommended' + - google +parser: '@typescript-eslint/parser' +parserOptions: + ecmaFeatures: + jsx: true + ecmaVersion: 12 + sourceType: module +plugins: + - react + - '@typescript-eslint' +rules: + no-invalid-this: off + require-jsdoc: off + indent: + - error + - 2 diff --git a/client/package-lock.json b/client/package-lock.json index f8e7b72..ce1be9a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2579,18 +2579,70 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, "@typescript-eslint/eslint-plugin": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.2.tgz", - "integrity": "sha512-uiQQeu9tWl3f1+oK0yoAv9lt/KXO24iafxgQTkIYO/kitruILGx3uH+QtIAHqxFV+yIsdnJH+alel9KuE3J15Q==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz", + "integrity": "sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==", "requires": { - "@typescript-eslint/experimental-utils": "4.15.2", - "@typescript-eslint/scope-manager": "4.15.2", + "@typescript-eslint/experimental-utils": "4.17.0", + "@typescript-eslint/scope-manager": "4.17.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz", + "integrity": "sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==", + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.17.0", + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/typescript-estree": "4.17.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz", + "integrity": "sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0" + } + }, + "@typescript-eslint/types": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.17.0.tgz", + "integrity": "sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==" + }, + "@typescript-eslint/typescript-estree": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz", + "integrity": "sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz", + "integrity": "sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "eslint-visitor-keys": "^2.0.0" + } + } } }, "@typescript-eslint/experimental-utils": { @@ -2607,14 +2659,53 @@ } }, "@typescript-eslint/parser": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.15.2.tgz", - "integrity": "sha512-SHeF8xbsC6z2FKXsaTb1tBCf0QZsjJ94H6Bo51Y1aVEZ4XAefaw5ZAilMoDPlGghe+qtq7XdTiDlGfVTOmvA+Q==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.17.0.tgz", + "integrity": "sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==", "requires": { - "@typescript-eslint/scope-manager": "4.15.2", - "@typescript-eslint/types": "4.15.2", - "@typescript-eslint/typescript-estree": "4.15.2", + "@typescript-eslint/scope-manager": "4.17.0", + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/typescript-estree": "4.17.0", "debug": "^4.1.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz", + "integrity": "sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0" + } + }, + "@typescript-eslint/types": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.17.0.tgz", + "integrity": "sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==" + }, + "@typescript-eslint/typescript-estree": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz", + "integrity": "sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz", + "integrity": "sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==", + "requires": { + "@typescript-eslint/types": "4.17.0", + "eslint-visitor-keys": "^2.0.0" + } + } } }, "@typescript-eslint/scope-manager": { @@ -5852,6 +5943,12 @@ } } }, + "eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true + }, "eslint-config-react-app": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz", @@ -14621,9 +14718,9 @@ }, "dependencies": { "ajv": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz", - "integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.1.tgz", + "integrity": "sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -15341,9 +15438,9 @@ "optional": true }, "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" }, "v8-to-istanbul": { "version": "7.1.0", diff --git a/client/package.json b/client/package.json index 48ac4b0..8220822 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.1", + "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", @@ -39,5 +40,12 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.17.0", + "@typescript-eslint/parser": "^4.17.0", + "eslint": "^7.21.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-react": "^7.22.0" } } diff --git a/client/src/App.test.js b/client/src/App.test.js index 1f03afe..a2b8750 100644 --- a/client/src/App.test.js +++ b/client/src/App.test.js @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react'; +import {render, screen} from '@testing-library/react'; +import {React} from 'react'; import App from './App'; test('renders learn react link', () => { diff --git a/client/src/components/AnnotationView.js b/client/src/components/AnnotationView.js index 68d7e9e..f345dce 100644 --- a/client/src/components/AnnotationView.js +++ b/client/src/components/AnnotationView.js @@ -1,39 +1,56 @@ -import { Paper, Zoom } from '@material-ui/core'; -import { DataGrid } from '@material-ui/data-grid'; +import {Paper} from '@material-ui/core'; +import {DataGrid} from '@material-ui/data-grid'; import React from 'react'; -import { deidentificationStates } from './DeidentifiedText'; - -export function AnnotationView(props) { +import {deidentificationStates} from './DeidentifiedText'; +import PropTypes from 'prop-types'; +function AnnotationView(props) { const types = [ - {type: "text_date", name: "Date", key: "textDateAnnotations"}, - {type: "text_physical_address", name: "Physical Address", key: "textPhysicalAddressAnnotations"}, - {type: "text_person_name", name: "Person Name", key: "textPersonNameAnnotations"} - ] + {type: 'text_date', name: 'Date', key: 'textDateAnnotations'}, + {type: 'text_physical_address', name: 'Physical Address', key: + 'textPhysicalAddressAnnotations'}, + {type: 'text_person_name', name: 'Person Name', key: + 'textPersonNameAnnotations'}, + ]; let allAnnotations; - if (props.annotations === deidentificationStates.EMPTY || props.annotations === deidentificationStates.LOADING || props.annotations === deidentificationStates.ERROR) { - allAnnotations = [] + if (props.annotations === deidentificationStates.EMPTY || + props.annotations === deidentificationStates.LOADING || + props.annotations === deidentificationStates.ERROR) { + allAnnotations = []; } else { allAnnotations = types.map( (type) => props.annotations[type.key].map((annotation, index) => { - return {type: type.name, id: String(type.type)+'_'+String(index), ...annotation}; - }) + return { + type: type.name, + id: String(type.type)+'_'+String(index), + ...annotation, + }; + }), ).flat(); } const columns = [ - {field: 'id', headerName: "ID", hide: true}, - {field: 'type', headerName: "Type", width: 125}, - {field: 'text', headerName: "Text", width: 130}, - {field: 'start', headerName: "Start", width: 90}, - {field: 'length', headerName: "Length", width: 100}, - {field: 'confidence', headerName: "Confidence", width: 130} - ] + {field: 'id', headerName: 'ID', hide: true}, + {field: 'type', headerName: 'Type', width: 125}, + {field: 'text', headerName: 'Text', width: 130}, + {field: 'start', headerName: 'Start', width: 90}, + {field: 'length', headerName: 'Length', width: 100}, + {field: 'confidence', headerName: 'Confidence', width: 130}, + ]; return ( - + ); -} \ No newline at end of file +} + +AnnotationView.propTypes = { + annotations: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.number, + ]), +}; + +export default AnnotationView; diff --git a/client/src/components/App.js b/client/src/components/App.js index 17c336d..9b70ec6 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -1,50 +1,60 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Config from '../config'; -import { DeidentifiedNoteApi, ToolApi } from '../apis'; -import { DeidentifyRequestFromJSON } from '../models'; -import { Configuration } from '../runtime'; -import { encodeString, decodeString } from '../stringSmuggler'; +import {DeidentifiedNoteApi, ToolApi} from '../apis'; +import {DeidentifyRequestFromJSON} from '../models'; +import {Configuration} from '../runtime'; +import {encodeString, decodeString} from '../stringSmuggler'; -import { withStyles } from '@material-ui/core/styles'; -import { AppBar, Box, Button, IconButton, Paper, Toolbar, Grid, Typography, TextField, Fab } from '@material-ui/core'; +import {withStyles} from '@material-ui/core/styles'; +import {AppBar, Box, Button, IconButton, Paper, Toolbar, Grid, Typography, + TextField, Fab} from '@material-ui/core'; import InfoIcon from '@material-ui/icons/Info'; import AddIcon from '@material-ui/icons/Add'; -import { DeidentifiedText, deidentificationStates } from './DeidentifiedText'; -import { InfoDialog } from './InfoDialog'; -import { DeidentificationConfigForm } from './DeidentificationConfigForm'; -import { AnnotationView } from './AnnotationView'; +import {DeidentifiedText, deidentificationStates} from './DeidentifiedText'; +import {InfoDialog} from './InfoDialog'; +import {DeidentificationConfigForm} from './DeidentificationConfigForm'; +import AnnotationView from './AnnotationView'; -const config = new Config() +const config = new Config(); const apiConfiguration = new Configuration({basePath: config.serverApiUrl()}); const deidentifiedNotesApi = new DeidentifiedNoteApi(apiConfiguration); const toolApi = new ToolApi(apiConfiguration); +const defaultText = + 'On 12/26/2020, Ms. Chloe Price met with Dr. Prescott in Seattle.'; + const styles = (theme) => { return { root: { - backgroundColor: "#282c34", - minHeight: "100vh", - justifyContent: "center", - color: "white", - overflow: "auto", - padding: "20px", - paddingBottom: "50px" + backgroundColor: '#282c34', + minHeight: '100vh', + justifyContent: 'center', + color: 'white', + overflow: 'auto', + padding: '20px', + paddingBottom: '50px', }, deidButton: { - backgroundColor: "#ADD8E6" - } + backgroundColor: '#ADD8E6', + }, }; -} +}; class App extends React.Component { + static propTypes = { + location: PropTypes.object, + history: PropTypes.object, + }; + constructor(props) { super(props); // Try loading state from URL - const { location } = props; + const {location} = props; const queryInUrl = location.pathname.slice(1); let deidentifyRequest; let showInfo; @@ -56,16 +66,17 @@ class App extends React.Component { deidentificationSteps: [{ key: 0, confidenceThreshold: 20, - maskingCharConfig: {maskingChar: "*"}, - annotationTypes: ["text_person_name", "text_physical_address", "text_date"] + maskingCharConfig: {maskingChar: '*'}, + annotationTypes: [ + 'text_person_name', 'text_physical_address', 'text_date'], }], note: { - text: "On 12/26/2020, Ms. Chloe Price met with Dr. Prescott in Seattle.", - noteType: "0000", // FIXME: figure out whether and how to get this - identifier: "0000", - patientId: "0000" + text: defaultText, + noteType: '0000', // FIXME: figure out whether and how to get this + identifier: '0000', + patientId: '0000', }, - keyMax: 0 + keyMax: 0, }; showInfo = true; } @@ -74,58 +85,62 @@ class App extends React.Component { deidentifiedNoteText: deidentificationStates.EMPTY, deidentifiedAnnotations: deidentificationStates.EMPTY, deidentifyRequest: deidentifyRequest, - showInfo: showInfo + showInfo: showInfo, }; this.handleTextAreaChange = this.handleTextAreaChange.bind(this); } updateUrl = () => { - const queryInUrl = "/" + encodeString(JSON.stringify(this.state.deidentifyRequest)); + const queryInUrl = '/' + + encodeString(JSON.stringify(this.state.deidentifyRequest)); this.props.history.push(queryInUrl); } deidentifyNote = () => { // Mark de-identified text as loading - this.setState({deidentifiedNoteText: deidentificationStates.LOADING}) + this.setState({deidentifiedNoteText: deidentificationStates.LOADING}); // Build de-identification request - let deidentifyRequest = new DeidentifyRequestFromJSON(this.state.deidentifyRequest); + const deidentifyRequest = + new DeidentifyRequestFromJSON(this.state.deidentifyRequest); // Make de-identification request - deidentifiedNotesApi.createDeidentifiedNotes({deidentifyRequest: deidentifyRequest}) + deidentifiedNotesApi.createDeidentifiedNotes( + {deidentifyRequest: deidentifyRequest}) .then((deidentifyResponse) => { this.setState({ deidentifiedNoteText: deidentifyResponse.deidentifiedNote.text, - deidentifiedAnnotations: deidentifyResponse.deidentifiedAnnotations + deidentifiedAnnotations: deidentifyResponse.deidentifiedAnnotations, }); }) .catch(() => { this.setState({ deidentifiedNoteText: deidentificationStates.ERROR, - deidentifiedAnnotations: deidentificationStates.ERROR + deidentifiedAnnotations: deidentificationStates.ERROR, }); }); } replaceDeidentificationStep = (index, newStep) => { - let deidentificationSteps = [...this.state.deidentifyRequest.deidentificationSteps]; + const deidentificationSteps = + [...this.state.deidentifyRequest.deidentificationSteps]; deidentificationSteps[index] = newStep; this.setState( { deidentifyRequest: { ...this.state.deidentifyRequest, - deidentificationSteps: deidentificationSteps - } + deidentificationSteps: deidentificationSteps, + }, }, - () => this.updateUrl() + () => this.updateUrl(), ); } - + updateDeidentificationStep = (index, newSettings) => { const newStep = { ...this.state.deidentifyRequest.deidentificationSteps[index], - ...newSettings + ...newSettings, }; this.replaceDeidentificationStep(index, newStep); } @@ -137,21 +152,23 @@ class App extends React.Component { ...this.state.deidentifyRequest, note: { ...this.state.deidentifyRequest.note, - text: event.target.value - } - } + text: event.target.value, + }, + }, }, - () => this.updateUrl() + () => this.updateUrl(), ); } addDeidStep = (event) => { - let deidentificationSteps = [...this.state.deidentifyRequest.deidentificationSteps]; + const deidentificationSteps = + [...this.state.deidentifyRequest.deidentificationSteps]; const newDeidStep = { confidenceThreshold: 20, - maskingCharConfig: {maskingChar: "*"}, - annotationTypes: ["text_person_name", "text_physical_address", "text_date"], - key: this.state.deidentifyRequest.keyMax+1 + maskingCharConfig: {maskingChar: '*'}, + annotationTypes: [ + 'text_person_name', 'text_physical_address', 'text_date'], + key: this.state.deidentifyRequest.keyMax+1, }; deidentificationSteps.push(newDeidStep); this.setState( @@ -159,42 +176,55 @@ class App extends React.Component { deidentifyRequest: { ...this.state.deidentifyRequest, deidentificationSteps: deidentificationSteps, - keyMax: this.state.deidentifyRequest.keyMax + 1 - } + keyMax: this.state.deidentifyRequest.keyMax + 1, + }, }, - () => this.updateUrl() + () => this.updateUrl(), ); } redoDeidentificationStep = (index, oldKey, newKey, newValue) => { // Delete a key from a deid step, and add a new key, value pair to it - let {[oldKey]: omitted, ...newDeidStep} = this.state.deidentifyRequest.deidentificationSteps[index]; + /* eslint-disable no-unused-vars */ + const {[oldKey]: omitted, ...newDeidStep} = + this.state.deidentifyRequest.deidentificationSteps[index]; + /* eslint-enable no-unused-vars */ newDeidStep[newKey] = newValue; - + this.replaceDeidentificationStep(index, newDeidStep); } deleteDeidentificationStep = (index) => { - let deidentificationSteps = [...this.state.deidentifyRequest.deidentificationSteps]; + const deidentificationSteps = + [...this.state.deidentifyRequest.deidentificationSteps]; deidentificationSteps.splice(index, 1); this.setState( { deidentifyRequest: { ...this.state.deidentifyRequest, - deidentificationSteps: deidentificationSteps - } + deidentificationSteps: deidentificationSteps, + }, }, - () => this.updateUrl() + () => this.updateUrl(), ); } render() { - const { classes } = this.props; + const {classes} = this.props; - const leftColumn = + const leftColumn = - Input Note + + Input Note + @@ -210,16 +240,20 @@ class App extends React.Component { - + - Deidentification Steps + + Deidentification Steps + { - this.state.deidentifyRequest.deidentificationSteps.map((deidStep, index) => - + this.state.deidentifyRequest.deidentificationSteps.map( + (deidStep, index) => - + , ) } - + ; - const rightColumn = + const rightColumn = - De-identified Note + + De-identified Note + @@ -246,37 +290,49 @@ class App extends React.Component { - Annotations + + Annotations + - + ; return ( -
- - - NLP Sandbox PHI Deidentifier - {this.setState({showInfo: true})}}> - - - - - {leftColumn} - - {rightColumn} - - - {this.setState({showInfo: false})}} - toolApi={toolApi} - /> -
+
+ + + + NLP Sandbox PHI Deidentifier + + { + this.setState({showInfo: true}); + }}> + + + + + {leftColumn} + + {rightColumn} + + + { + this.setState({showInfo: false}); + }} + toolApi={toolApi} + /> +
); } } +App.propTypes = { + classes: PropTypes.object, +}; + export default withStyles(styles)(App); diff --git a/client/src/components/DeidentificationConfigForm.js b/client/src/components/DeidentificationConfigForm.js index 97b3e80..88ac51c 100644 --- a/client/src/components/DeidentificationConfigForm.js +++ b/client/src/components/DeidentificationConfigForm.js @@ -1,28 +1,39 @@ import React from 'react'; -import { DeidentificationStepAnnotationTypesEnum } from '../models'; -import { Collapse, Paper, Table, TableRow, TableCell, AppBar, Toolbar, +import {DeidentificationStepAnnotationTypesEnum} from '../models'; +import {Collapse, Paper, Table, TableRow, TableCell, AppBar, Toolbar, Typography, IconButton, TextField, Select, MenuItem} from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; +import PropTypes from 'prop-types'; const DEIDENTIFICATION_STRATEGIES = { - "maskingCharConfig": "Masking Character", - "annotationTypeMaskConfig": "Annotation Type Mask", - "redactConfig": "Redaction" -} + 'maskingCharConfig': 'Masking Character', + 'annotationTypeMaskConfig': 'Annotation Type Mask', + 'redactConfig': 'Redaction', +}; const ANNOTATION_TYPE_NAMES = { - "text_date": "Date", - "text_person_name": "Person Name", - "text_physical_address": "Physical Address" -} + 'text_date': 'Date', + 'text_person_name': 'Person Name', + 'text_physical_address': 'Physical Address', +}; export class DeidentificationConfigForm extends React.Component { + static propTypes = { + updateDeidStep: PropTypes.func.isRequired, + redoDeidStep: PropTypes.func.isRequired, + deleteDeidStep: PropTypes.func.isRequired, + annotationTypes: PropTypes.array.isRequired, + confidenceThreshold: PropTypes.number.isRequired, + index: PropTypes.number.isRequired, + maskingCharConfig: PropTypes.object, + }; + constructor(props) { super(props); this.state = { - expand: false - } + expand: false, + }; } updateDeidStep = (newSettings) => { @@ -30,11 +41,12 @@ export class DeidentificationConfigForm extends React.Component { } getStrategy() { - // Return the current deidentification strategy for this deidentification step + // Return the current deidentification strategy for this deidentification + // step let strategy; - const deidStrategies = Object.keys(DEIDENTIFICATION_STRATEGIES) + const deidStrategies = Object.keys(DEIDENTIFICATION_STRATEGIES); for (let i = 0; i < deidStrategies.length; i++) { - strategy = deidStrategies[i] + strategy = deidStrategies[i]; if (strategy in this.props) { return strategy; } @@ -44,10 +56,14 @@ export class DeidentificationConfigForm extends React.Component { handleStrategyChange = (event) => { const newStrategyName = event.target.value; const oldStrategyName = this.getStrategy(); - if (newStrategyName === "maskingCharConfig") { - this.props.redoDeidStep(this.props.index, oldStrategyName, newStrategyName, {maskingChar: "*"}); + if (newStrategyName === 'maskingCharConfig') { + this.props.redoDeidStep( + this.props.index, oldStrategyName, newStrategyName, {maskingChar: '*'}, + ); } else { - this.props.redoDeidStep(this.props.index, oldStrategyName, newStrategyName, {}) + this.props.redoDeidStep( + this.props.index, oldStrategyName, newStrategyName, {}, + ); } } @@ -55,61 +71,73 @@ export class DeidentificationConfigForm extends React.Component { const maskingChar = event.target.value; if (maskingChar) { this.updateDeidStep({ - maskingCharConfig: { maskingChar: maskingChar } + maskingCharConfig: {maskingChar: maskingChar}, }); } else { this.updateDeidStep({ - maskingCharConfig: {} + maskingCharConfig: {}, }); } } handleConfidenceThresholdChange = (event) => { this.updateDeidStep({ - confidenceThreshold: parseFloat(event.target.value) + confidenceThreshold: parseFloat(event.target.value), }); } handleAnnotationTypeDelete = (event, index) => { - const annotationTypes = this.props.annotationTypes - const newAnnotationTypes = annotationTypes.slice(0, index).concat(annotationTypes.slice(index+1)); + const annotationTypes = this.props.annotationTypes; + const newAnnotationTypes = + annotationTypes.slice(0, index).concat(annotationTypes.slice(index+1)); this.updateDeidStep({ - annotationTypes: newAnnotationTypes + annotationTypes: newAnnotationTypes, }); } handleAnnotationTypeAdd = (event) => { const annotationType = event.target.value; this.updateDeidStep({ - annotationTypes: this.props.annotationTypes.concat(annotationType) + annotationTypes: this.props.annotationTypes.concat(annotationType), }); } handleDelete = () => { this.setState( - { expand: false }, () => { + {expand: false}, () => { setTimeout( - () => {this.props.deleteDeidStep(this.props.index);}, - 250 - ) - } + () => { + this.props.deleteDeidStep(this.props.index); + }, + 250, + ); + }, ); } componentDidMount = () => { - this.setState({ expand: true }); + this.setState({expand: true}); } render = () => { - const allAnnotationTypes = Object.values(DeidentificationStepAnnotationTypesEnum) - const borderRadius = 10 + const allAnnotationTypes = + Object.values(DeidentificationStepAnnotationTypesEnum); + const borderRadius = 10; return ( - - + + - De-identification Step #{this.props.index + 1} - + + De-identification Step #{this.props.index + 1} + + + + @@ -118,20 +146,25 @@ export class DeidentificationConfigForm extends React.Component { Obfuscation method - {Object.keys(DEIDENTIFICATION_STRATEGIES).map((strategy) => { - return {DEIDENTIFICATION_STRATEGIES[strategy]}; + return + {DEIDENTIFICATION_STRATEGIES[strategy]} + ; })}     - {this.getStrategy() === "maskingCharConfig" && + {this.getStrategy() === 'maskingCharConfig' && @@ -146,7 +179,7 @@ export class DeidentificationConfigForm extends React.Component { @@ -159,15 +192,36 @@ export class DeidentificationConfigForm extends React.Component { {this.props.annotationTypes.map((annotationType, index) => { return ( - {ANNOTATION_TYPE_NAMES[annotationType]} {this.handleAnnotationTypeDelete(event, index);}}> + + {ANNOTATION_TYPE_NAMES[annotationType]} + { + this.handleAnnotationTypeDelete(event, index); + }} + > + + + ); })} - {this.props.annotationTypes.length < allAnnotationTypes.length && - click to add - {allAnnotationTypes.filter(annotationType => !this.props.annotationTypes.includes(annotationType)).map((annotationType) => { + {allAnnotationTypes.filter( + (annotationType) => ( + !this.props.annotationTypes.includes(annotationType) + ), + ).map((annotationType, index) => { return ( - {ANNOTATION_TYPE_NAMES[annotationType]} + + {ANNOTATION_TYPE_NAMES[annotationType]} + ); })} @@ -179,4 +233,4 @@ export class DeidentificationConfigForm extends React.Component { ); } -} \ No newline at end of file +} diff --git a/client/src/components/DeidentifiedText.js b/client/src/components/DeidentifiedText.js index 00422e8..5b49551 100644 --- a/client/src/components/DeidentifiedText.js +++ b/client/src/components/DeidentifiedText.js @@ -1,40 +1,52 @@ import React from 'react'; -import { Paper, TextField } from '@material-ui/core'; +import {Paper, TextField} from '@material-ui/core'; +import PropTypes from 'prop-types'; export const deidentificationStates = { EMPTY: 0, LOADING: 1, - ERROR: 2 -} + ERROR: 2, +}; export class DeidentifiedText extends React.Component { + static propTypes = { + text: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + }; + render() { let content; let color; - if (this.props.text === deidentificationStates.EMPTY || this.props.text === "") { - color = "grey"; - content = "Add a clinical note on the left and click on 'Deidentify Note'"; + if ( + this.props.text === deidentificationStates.EMPTY || + this.props.text === '' + ) { + color = 'grey'; + content = + 'Add a clinical note on the left and click on \'Deidentify Note\''; } else if (this.props.text === deidentificationStates.LOADING) { - color = "grey"; - content = "Loading..." + color = 'grey'; + content = 'Loading...'; } else if (this.props.text === deidentificationStates.ERROR) { - color = "grey"; - content = "API call resulted in error!" + color = 'grey'; + content = 'API call resulted in error!'; } else { - color = "black"; + color = 'black'; content = this.props.text; } return ( - + ); diff --git a/client/src/components/InfoDialog.js b/client/src/components/InfoDialog.js index 545affa..b75c908 100644 --- a/client/src/components/InfoDialog.js +++ b/client/src/components/InfoDialog.js @@ -1,21 +1,26 @@ import React from 'react'; -import { Box, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, Grid, CircularProgress, Link, Paper, Table, TableHead, TableRow, TableCell, TableBody, TableContainer, withStyles } from '@material-ui/core'; +import {Box, Dialog, DialogTitle, DialogContent, DialogContentText, + DialogActions, Button, Grid, CircularProgress, Link, Paper, Table, + TableHead, TableRow, TableCell, TableBody, TableContainer, + withStyles} from '@material-ui/core'; import Config from '../config'; +import {ToolApi} from '../apis'; +import PropTypes from 'prop-types'; export const toolInfoStates = { LOADING: 1, - ERROR: 2 -} + ERROR: 2, +}; const StyledTableCell = withStyles((theme) => ({ head: { - backgroundColor: "grey", + backgroundColor: 'grey', color: theme.palette.common.white, - padding: theme.spacing(1.5) + padding: theme.spacing(1.5), }, body: { fontSize: 14, - padding: theme.spacing(1.5) + padding: theme.spacing(1.5), }, }))(TableCell); @@ -32,16 +37,28 @@ function ToolDependencyRow(props) { { props.toolDependency.toolType } { props.toolDependency.toolApiVersion } - { props.toolDependency.name } + + + { props.toolDependency.name } + { props.toolDependency.version } - { props.toolDependency.author } + + + { props.toolDependency.author } + { props.toolDependency.repository } { props.toolDependency.license } - { props.toolDependency.description } + + { props.toolDependency.description } + ); } +ToolDependencyRow.propTypes = { + toolDependency: PropTypes.object, +}; + function ToolDependenciesTable(props) { if (props.toolDependencies === toolInfoStates.LOADING) { return ; @@ -49,34 +66,45 @@ function ToolDependenciesTable(props) { return API ERROR; } else { return -
- - - Type - API Version - Name - Version - Author - Repository - License - Description - - - - - {props.toolDependencies.map((toolDependency) => )} - -
- + + + + Type + API Version + Name + Version + Author + Repository + License + Description + + + + + {props.toolDependencies.map( + (toolDependency, index) => , + )} + +
+ ; } } export class InfoDialog extends React.Component { + static propTypes = { + toolApi: PropTypes.instanceOf(ToolApi), + handleClose: PropTypes.func, + open: PropTypes.bool, + } + constructor(props) { super(props); this.state = { toolDependencies: toolInfoStates.LOADING, - deidentifierInfo: toolInfoStates.LOADING + deidentifierInfo: toolInfoStates.LOADING, }; } @@ -85,12 +113,12 @@ export class InfoDialog extends React.Component { this.props.toolApi.getToolDependencies() .then((apiResponse) => { this.setState({ - toolDependencies: apiResponse.toolDependencies + toolDependencies: apiResponse.toolDependencies, }); }) .catch((error) => { this.setState({ - toolDependencies: toolInfoStates.ERROR + toolDependencies: toolInfoStates.ERROR, }); }); @@ -98,13 +126,13 @@ export class InfoDialog extends React.Component { this.props.toolApi.getTool() .then((apiResponse) => { this.setState({ - deidentifierInfo: apiResponse - }) + deidentifierInfo: apiResponse, + }); }) .catch(() => { this.setState({ - deidentifierInfo: toolInfoStates.ERROR - }) + deidentifierInfo: toolInfoStates.ERROR, + }); }); } @@ -112,23 +140,25 @@ export class InfoDialog extends React.Component { const config = new Config(); let content; if (this.state.deidentifierInfo === toolInfoStates.LOADING) { - content = + content = ; } else if (this.state.deidentifierInfo === toolInfoStates.ERROR) { content = API Error; } else { - const toolInfo = this.state.deidentifierInfo; content = You are currently using version {config.version()} of the NLP + href={config.source()}>NLP Sandbox PHI Deidentifier Web Client, a tool made for testing the effectiveness of community-created, open source PHI annotators submitted to the NLP Sandbox. You can input a clinical note, which will be annotated and de-identified using the following NLP Sandbox specification-compliant tools: - - + + ; } return ( - + ); diff --git a/client/src/config.js b/client/src/config.js index 481323f..40e0076 100644 --- a/client/src/config.js +++ b/client/src/config.js @@ -21,7 +21,7 @@ export default class Config { const serverProtocol = this.serverProtocol(); const serverHost = this.serverHost(); const serverPort = this.serverPort(); - return `${serverProtocol}${serverHost}:${serverPort}` + return `${serverProtocol}${serverHost}:${serverPort}`; } serverApiPath() { @@ -41,7 +41,6 @@ export default class Config { source() { // URL to source of web client - return "https://github.com/nlpsandbox/phi-deidentifier-app"; + return 'https://github.com/nlpsandbox/phi-deidentifier-app'; } - -} \ No newline at end of file +} diff --git a/client/src/index.js b/client/src/index.js index 88a61dc..57ded63 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter, Route, Switch } from 'react-router-dom'; +import {BrowserRouter, Route, Switch} from 'react-router-dom'; import './index.css'; import App from './components/App'; import reportWebVitals from './reportWebVitals'; @@ -13,7 +13,7 @@ ReactDOM.render( , - document.getElementById('root') + document.getElementById('root'), ); // If you want to start measuring performance in your app, pass a function diff --git a/client/src/reportWebVitals.js b/client/src/reportWebVitals.js index 5253d3a..a8cc5b3 100644 --- a/client/src/reportWebVitals.js +++ b/client/src/reportWebVitals.js @@ -1,6 +1,6 @@ -const reportWebVitals = onPerfEntry => { +const reportWebVitals = (onPerfEntry) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); diff --git a/client/src/stringSmuggler.js b/client/src/stringSmuggler.js index 2e51224..1b9c93d 100644 --- a/client/src/stringSmuggler.js +++ b/client/src/stringSmuggler.js @@ -5,11 +5,11 @@ // then use that as the url. export function encodeString(decodedString) { - let buff = new Buffer(decodedString); + const buff = new Buffer(decodedString); return buff.toString('base64'); } export function decodeString(encodedString) { - let buff = new Buffer(encodedString, 'base64'); + const buff = new Buffer(encodedString, 'base64'); return buff.toString('utf-8'); }