diff --git a/README.md b/README.md index ab521c4f6a..a1dc150ec0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A generic data portal that supports some basic interaction with Gen3 services li - [npm](https://www.npmjs.com/) ### Installing + ``` npm install ``` @@ -18,44 +19,49 @@ npm install See [docs/guide_running_portal_locally.md](docs/guide_running_portal_locally.md) for a step-by-step guide to running portal locally. The portal's `/dev.html` path loads javascript and most css -from `localhost`. Test code under local development with this procedure: -* `npm install` -* launch the webpack dev server, and configure local code with the same configuration as the server to test against. For example - if we intend to test against qa.planx-pla.net, then: +from `localhost`. Test code under local development with this procedure: + +- `npm install` +- launch the webpack dev server, and configure local code with the same configuration as the server to test against. For example - if we intend to test against qa.planx-pla.net, then: + ``` HOSTNAME=qa.planx-pla.net NODE_ENV=auto bash ./runWebpack.sh ``` + , or for qa-brain: + ``` HOSTNAME=qa-brain.planx-pla.net NODE_ENV=auto bash ./runWebpack.sh ``` You can also use the `autoprod` value for `NODE_ENV` to do the `auto` setup, then run `webpack` in production mode, so it generates `.js` and `.html` files instead of launching the dev server - ex: + ``` HOSTNAME=qa-brain.planx-pla.net NODE_ENV=autoprod GEN3_BUNDLE=all bash ./runWebpack.sh ``` Tiered-access settings can be configured through either the `TIER_ACCESS_LEVEL` environment variable (site-wide) or through the `tierAccessLevel` property on guppyConfig blocks for each Data Explorer tab in the gitops.json (index-scoped). To use the index-scoped config style, all guppyConfig blocks in the portal config must contain the `tierAccessLevel` property. See `docs/portal_config.md` for thorough example of portal config structure. ->**NOTE:** To locally test site-wide Tiered Access features, the additional environment variables `TIER_ACCESS_LEVEL` and `TIER_ACCESS_LIMIT` should have the same values as the server's "global.tier_access_level" and "global.tier_access_limit" properties in its [`manifest.json`](https://github.com/uc-cdis/cdis-manifest). +> **NOTE:** To locally test site-wide Tiered Access features, the additional environment variables `TIER_ACCESS_LEVEL` and `TIER_ACCESS_LIMIT` should have the same values as the server's "global.tier_access_level" and "global.tier_access_limit" properties in its [`manifest.json`](https://github.com/uc-cdis/cdis-manifest). > > **Example**:`HOSTNAME=qa-brain.planx-pla.net TIER_ACCESS_LEVEL=regular TIER_ACCESS_LIMIT=50 NODE_ENV=auto bash ./runWebpack.sh` If the index-scoped tiered-access setting is used, the `tierAccessLevel` properties in the guppyConfig blocks in gitops.json should have the same values as the server's "guppyConfig[index].tier_access_level" in its [`manifest.json`](https://github.com/uc-cdis/cdis-manifest). Tabs should be configured with the same tiered-access level as the ES index they use. +- Accept the self-signed certificate at https://localhost:9443/bundle.js -* Accept the self-signed certificate at https://localhost:9443/bundle.js - -* Load the test environment's `/dev.html` - ex: https://qa-brian.planx-pla.net/dev.html - +- Load the test environment's `/dev.html` - ex: https://qa-brian.planx-pla.net/dev.html ### Local development and gitops -Most production commons currently load custom configuration via gitops. The configuration for a production commons is available in that commons' gitops repository (mostly https://github.com/uc-cdis/cdis-manifest), and can be copied for local development. The `runWebpack.sh` script automates this process when `NODE_ENV` is set to `auto` - ex: +Most production commons currently load custom configuration via gitops. The configuration for a production commons is available in that commons' gitops repository (mostly https://github.com/uc-cdis/cdis-manifest), and can be copied for local development. The `runWebpack.sh` script automates this process when `NODE_ENV` is set to `auto` - ex: + ``` HOSTNAME=qa-brain.planx-pla.net NODE_ENV=auto bash ./runWebpack.sh ``` Note: the legacy `dev` NODE_ENV is still available, but the `APP` environment must also be manually set to load the configuration that matches the dictionary from HOSTNAME - ex: + ``` HOSTNAME=qa.planx-pla.net NODE_ENV=dev APP=dev bash ./runWebpack.sh ``` @@ -65,13 +71,14 @@ HOSTNAME=qa.planx-pla.net NODE_ENV=dev APP=dev bash ./runWebpack.sh The portal webpack configurations selects between multiple application entry points at build time: -* `commons` - the default data commons portal -* `covid19` - a portal for pandemic response commons -* `nct` - a portal for clinical trials -* `ecosystem` - a portal for Gen3 data ecosystem -* `workspace` - a scaled down portal for workspace accounts +- `commons` - the default data commons portal +- `covid19` - a portal for pandemic response commons +- `nct` - a portal for clinical trials +- `ecosystem` - a portal for Gen3 data ecosystem +- `workspace` - a scaled down portal for workspace accounts We can use the https://remote/dev.html trick to test a local workspace build by setting the `GEN3_BUNDLE` variable to `workspace`: + ``` HOSTNAME=qa.planx-pla.net GEN3_BUNDLE=workspace bash ./runWebpack.sh ``` @@ -79,14 +86,14 @@ HOSTNAME=qa.planx-pla.net GEN3_BUNDLE=workspace bash ./runWebpack.sh That just changes the webpack config to serve the workspace bundle as `bundle.js` - which is what `dev.html` expects. The portal `Dockerfile` runs a deploy time webpack build to incorporate -deploy-time configuration. The `GEN3_BUNDLE` environment variable determines which application gets built at run time. - +deploy-time configuration. The `GEN3_BUNDLE` environment variable determines which application gets built at run time. ### Customized Basename ->:warning: To use this feature, make sure the to set the `BASENAME` to a customized value in the portal deployed to the remote before you run the local dev server with the customized basename. Also the customized basename you used for local portal dev server should be the same as you have set for the remote deployment. +> :warning: To use this feature, make sure the to set the `BASENAME` to a customized value in the portal deployed to the remote before you run the local dev server with the customized basename. Also the customized basename you used for local portal dev server should be the same as you have set for the remote deployment. Available from `3.23.0`, you can supply a customized basename for portal by setting the `BASENAME` variable: + ``` HOSTNAME=qa.planx-pla.net NODE_ENV=auto BASENAME=/portal bash ./runWebpack.sh ``` @@ -101,12 +108,15 @@ To run Storybook: `npm run storybook` ### Run Windmill using Docker + Build the container image first + ``` docker build -t windmill . ``` Then run the container + ``` docker run --rm -e HOSTNAME=qa.planx-pla.net -p 443:443 -ti windmill ``` @@ -114,10 +124,13 @@ docker run --rm -e HOSTNAME=qa.planx-pla.net -p 443:443 -ti windmill You will then need to visit `https://localhost` and accept the self-signed certificate warnings ### Deployment + docker run -d --name=dataportal -p 80:80 quay.io/cdis/data-portal ### GraphQL configuration + The configurations of Homepage charts are specified data/config/.json, or gitops.json in gitops repo. For each common, we need to specify the following json entities: + ``` "graphql": { "boardCounts": [ @@ -156,6 +169,7 @@ The configurations of Homepage charts are specified data/config/.js ``` + - `boardCounts` are the counts that you want to display in the top-left of dashboard's - `chartCounts` are the counts that you want to display in the bar chart of dashboard's - `projectDetails` are the counts that you want to display in the list of projects. It could be same as `boardCounts`, in this case, you only need to point to `boardCounts`. @@ -183,7 +197,9 @@ We support categorical horizontal grouped bar charts, and the chart will be usin ``` ### Certificates configuration + All the configurations of necessary certificates are define in src/.json. For each common, we need to specify the following json entities: + ``` "components": { "certs": { @@ -220,18 +236,23 @@ All the configurations of necessary certificates are define in src/ ``` + Then, specify all the required certificates that need to be completed before using the portal in following entry: + ``` "requiredCerts": [""] ``` + Default is an empty list. ### Style Guide + When styling components, we adhere to a few rules. We style using class selectors (`.class-name` instead of `#class-name`), and separate class names with hyphens instead of camel case (`.class-name` instead of `.className`). The CSS file should be named {component}.css, and be in the same folder as the component. It is then imported into the component's .jsx file. We are moving toward using the [BEM methodology](http://getbem.com/introduction/) in terms of CSS organizational conventions. This means we are dividing chunks of code within a component into blocks, are avoiding nesting components, and are using the naming convention of `{block}__{elements}--{modifier}`. `{element}` and `{modifier}` are optional depending on the situation - see the [BEM guidelines](http://getbem.com/introduction/) for more examples. For our example, say we have a simple component called `Component`: + ``` import './Component.css'; @@ -247,6 +268,7 @@ class Component extends React.Component { } } ``` + Our block would be `.component`, and elements in that block would consist of the buttons and the title. So our CSS would look like this, based on the BEM naming conventions: ``` @@ -302,3 +324,11 @@ class Component extends React.Component { } } ``` + +### Linting + +We use ESLint and Stylelint to lint and automatically format code. + +- `npm run eslint` Will run ESLint on the entire code base and automatically try to fix all JS and JS like files. +- `npm run eslint-new` Will run ESLint only on newly added files in the current git branch and automatically try to fix the JS and JS like files. +- `npm run stylelint` Will run Stylelint on all CSS and CSS-like files in the code base and automatically try to fix them. diff --git a/package-lock.json b/package-lock.json index 5adfe53409..8809925fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@storybook/addon-actions": "^6.0.0", "@storybook/react": "^6.0.0", "@types/redux-mock-store": "^1.0.3", + "@upsetjs/venn.js": "^1.4.2", "antd": "^4.16.7", "body-scroll-lock": "^4.0.0-beta.0", "brace": "^0.10.0", @@ -6895,6 +6896,46 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@upsetjs/venn.js": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-1.4.2.tgz", + "integrity": "sha512-sFyczoc4T0FonsMiHo/7AXqTpuBOOlIGTIMli5tFdfTXUECogGsnHY4+BQfHX9EaYk4zxBno/9PKsxEfY3CZLA==", + "dependencies": { + "fmin": "^0.0.2" + }, + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/@upsetjs/venn.js/node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@upsetjs/venn.js/node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "license": "MIT", @@ -7338,6 +7379,14 @@ "ajv": "^6.9.1" } }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "engines": { + "node": ">=0.4.2" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "license": "ISC", @@ -7390,7 +7439,8 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } @@ -10321,6 +10371,11 @@ "node": ">= 0.6" } }, + "node_modules/contour_plot": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/contour_plot/-/contour_plot-0.0.1.tgz", + "integrity": "sha512-Nil2HI76Xux6sVGORvhSS8v66m+/h5CwFkBJDO+U5vWaMdNC0yXNCsGDPbzPhvqOEU5koebhdEvD372LI+IyLw==" + }, "node_modules/convert-source-map": { "version": "1.8.0", "license": "MIT", @@ -11432,6 +11487,22 @@ "version": "0.7.0", "license": "MIT" }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "dev": true, @@ -11575,6 +11646,14 @@ "node": ">=0.10.0" } }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delay": { "version": "5.0.0", "license": "MIT", @@ -11925,6 +12004,17 @@ "webpack": "^4 || ^5" } }, + "node_modules/dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" + } + }, "node_modules/dset": { "version": "3.1.2", "license": "MIT", @@ -13816,6 +13906,29 @@ "readable-stream": "^2.3.6" } }, + "node_modules/fmin": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/fmin/-/fmin-0.0.2.tgz", + "integrity": "sha512-sSi6DzInhl9d8yqssDfGZejChO8d2bAGIpysPsvYsxFe898z89XhCZg6CPNV3nhUhFefeC/AXZK2bAJxlBjN6A==", + "dependencies": { + "contour_plot": "^0.0.1", + "json2module": "^0.0.3", + "rollup": "^0.25.8", + "tape": "^4.5.1", + "uglify-js": "^2.6.2" + } + }, + "node_modules/fmin/node_modules/uglify-js": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz", + "integrity": "sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/focus-trap": { "version": "6.9.4", "license": "MIT", @@ -14756,6 +14869,25 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "engines": { + "node": ">=4" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "license": "MIT", @@ -18494,6 +18626,17 @@ "version": "5.0.1", "license": "ISC" }, + "node_modules/json2module": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json2module/-/json2module-0.0.3.tgz", + "integrity": "sha512-qYGxqrRrt4GbB8IEOy1jJGypkNsjWoIMlZt4bAsmUScCA507Hbc2p1JOhBzqn45u3PWafUgH2OnzyNU7udO/GA==", + "dependencies": { + "rw": "^1.3.2" + }, + "bin": { + "json2module": "bin/json2module" + } + }, "node_modules/json2mq": { "version": "0.2.0", "license": "MIT", @@ -24179,6 +24322,14 @@ "node": ">=8" } }, + "node_modules/resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha512-Fn9X8rX8yYF4m81rZCK/5VmrmsSbqS/i3rDLl6ZZHAXgC2nTAx3dhwG8q8odP/RmdLa2YrybDJaAMg+X1ajY3w==", + "dependencies": { + "through": "~2.3.4" + } + }, "node_modules/ret": { "version": "0.1.15", "license": "MIT", @@ -24222,6 +24373,96 @@ "inherits": "^2.0.1" } }, + "node_modules/rollup": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.25.8.tgz", + "integrity": "sha512-a2S4Bh3bgrdO4BhKr2E4nZkjTvrJ2m2bWjMTzVYtoqSCn0HnuxosXnaJUHrMEziOWr3CzL9GjilQQKcyCQpJoA==", + "dependencies": { + "chalk": "^1.1.1", + "minimist": "^1.2.0", + "source-map-support": "^0.3.2" + }, + "bin": { + "rollup": "bin/rollup" + } + }, + "node_modules/rollup/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/rollup/node_modules/source-map": { + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", + "integrity": "sha512-htQyLrrRLkQ87Zfrir4/yN+vAUd6DNjVayEjTSHXu29AYQJw57I4/xEL/M6p6E/woPNJwvZt6rVlzc7gFEJccQ==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/rollup/node_modules/source-map-support": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.3.3.tgz", + "integrity": "sha512-9O4+y9n64RewmFoKUZ/5Tx9IHIcXM6Q+RTSw6ehnqybUz4a7iwR3Eaw80uLtqqQ5D0C+5H03D4KKGo9PdP33Gg==", + "dependencies": { + "source-map": "0.1.32" + } + }, + "node_modules/rollup/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "license": "BSD-3-Clause", @@ -26358,6 +26599,34 @@ "node": ">=6" } }, + "node_modules/tape": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-4.16.1.tgz", + "integrity": "sha512-U4DWOikL5gBYUrlzx+J0oaRedm2vKLFbtA/+BRAXboGWpXO7bMP8ddxlq3Cse2bvXFQ0jZMOj6kk3546mvCdFg==", + "dependencies": { + "call-bind": "~1.0.2", + "deep-equal": "~1.1.1", + "defined": "~1.0.0", + "dotignore": "~0.1.2", + "for-each": "~0.3.3", + "glob": "~7.2.3", + "has": "~1.0.3", + "inherits": "~2.0.4", + "is-regex": "~1.1.4", + "minimist": "~1.2.6", + "object-inspect": "~1.12.2", + "resolve": "~1.22.1", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.2.6", + "through": "~2.3.8" + }, + "bin": { + "tape": "bin/tape" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar": { "version": "6.1.11", "license": "ISC", @@ -33978,6 +34247,37 @@ } } }, + "@upsetjs/venn.js": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-1.4.2.tgz", + "integrity": "sha512-sFyczoc4T0FonsMiHo/7AXqTpuBOOlIGTIMli5tFdfTXUECogGsnHY4+BQfHX9EaYk4zxBno/9PKsxEfY3CZLA==", + "requires": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1", + "fmin": "^0.0.2" + }, + "dependencies": { + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "requires": { @@ -34306,6 +34606,11 @@ "version": "3.5.2", "requires": {} }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" + }, "ansi-align": { "version": "3.0.1", "requires": { @@ -34333,7 +34638,9 @@ "version": "0.0.8" }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "3.2.1", @@ -36255,6 +36562,11 @@ "content-type": { "version": "1.0.4" }, + "contour_plot": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/contour_plot/-/contour_plot-0.0.1.tgz", + "integrity": "sha512-Nil2HI76Xux6sVGORvhSS8v66m+/h5CwFkBJDO+U5vWaMdNC0yXNCsGDPbzPhvqOEU5koebhdEvD372LI+IyLw==" + }, "convert-source-map": { "version": "1.8.0", "requires": { @@ -37022,6 +37334,19 @@ "dedent": { "version": "0.7.0" }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, "deep-extend": { "version": "0.6.0", "dev": true @@ -37113,6 +37438,11 @@ "isobject": "^3.0.1" } }, + "defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" + }, "delay": { "version": "5.0.0" }, @@ -37334,6 +37664,14 @@ "dotenv-defaults": "^2.0.2" } }, + "dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "requires": { + "minimatch": "^3.0.4" + } + }, "dset": { "version": "3.1.2" }, @@ -38615,6 +38953,25 @@ "readable-stream": "^2.3.6" } }, + "fmin": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/fmin/-/fmin-0.0.2.tgz", + "integrity": "sha512-sSi6DzInhl9d8yqssDfGZejChO8d2bAGIpysPsvYsxFe898z89XhCZg6CPNV3nhUhFefeC/AXZK2bAJxlBjN6A==", + "requires": { + "contour_plot": "^0.0.1", + "json2module": "^0.0.3", + "rollup": "^0.25.8", + "tape": "^4.5.1", + "uglify-js": "3.14.3" + }, + "dependencies": { + "uglify-js": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz", + "integrity": "sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==" + } + } + }, "focus-trap": { "version": "6.9.4", "requires": { @@ -39349,6 +39706,21 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "requires": { + "ansi-regex": "3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==" + } + } + }, "has-bigints": { "version": "1.0.2" }, @@ -41699,6 +42071,14 @@ "json-stringify-safe": { "version": "5.0.1" }, + "json2module": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json2module/-/json2module-0.0.3.tgz", + "integrity": "sha512-qYGxqrRrt4GbB8IEOy1jJGypkNsjWoIMlZt4bAsmUScCA507Hbc2p1JOhBzqn45u3PWafUgH2OnzyNU7udO/GA==", + "requires": { + "rw": "^1.3.2" + } + }, "json2mq": { "version": "0.2.0", "requires": { @@ -45394,6 +45774,14 @@ "signal-exit": "^3.0.2" } }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha512-Fn9X8rX8yYF4m81rZCK/5VmrmsSbqS/i3rDLl6ZZHAXgC2nTAx3dhwG8q8odP/RmdLa2YrybDJaAMg+X1ajY3w==", + "requires": { + "through": "~2.3.4" + } + }, "ret": { "version": "0.1.15" }, @@ -45416,6 +45804,74 @@ "inherits": "^2.0.1" } }, + "rollup": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.25.8.tgz", + "integrity": "sha512-a2S4Bh3bgrdO4BhKr2E4nZkjTvrJ2m2bWjMTzVYtoqSCn0HnuxosXnaJUHrMEziOWr3CzL9GjilQQKcyCQpJoA==", + "requires": { + "chalk": "^1.1.1", + "minimist": "^1.2.0", + "source-map-support": "^0.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "source-map": { + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", + "integrity": "sha512-htQyLrrRLkQ87Zfrir4/yN+vAUd6DNjVayEjTSHXu29AYQJw57I4/xEL/M6p6E/woPNJwvZt6rVlzc7gFEJccQ==", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "source-map-support": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.3.3.tgz", + "integrity": "sha512-9O4+y9n64RewmFoKUZ/5Tx9IHIcXM6Q+RTSw6ehnqybUz4a7iwR3Eaw80uLtqqQ5D0C+5H03D4KKGo9PdP33Gg==", + "requires": { + "source-map": "0.1.32" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "requires": { + "ansi-regex": "3.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" + } + } + }, "rst-selector-parser": { "version": "2.2.3", "requires": { @@ -46882,6 +47338,28 @@ "tapable": { "version": "1.1.3" }, + "tape": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-4.16.1.tgz", + "integrity": "sha512-U4DWOikL5gBYUrlzx+J0oaRedm2vKLFbtA/+BRAXboGWpXO7bMP8ddxlq3Cse2bvXFQ0jZMOj6kk3546mvCdFg==", + "requires": { + "call-bind": "~1.0.2", + "deep-equal": "~1.1.1", + "defined": "~1.0.0", + "dotignore": "~0.1.2", + "for-each": "~0.3.3", + "glob": "~7.2.3", + "has": "~1.0.3", + "inherits": "~2.0.4", + "is-regex": "~1.1.4", + "minimist": "~1.2.6", + "object-inspect": "~1.12.2", + "resolve": "~1.22.1", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.2.6", + "through": "~2.3.8" + } + }, "tar": { "version": "6.1.11", "requires": { diff --git a/package.json b/package.json index 3f0ab2b60f..c0cc712974 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@storybook/addon-actions": "^6.0.0", "@storybook/react": "^6.0.0", "@types/redux-mock-store": "^1.0.3", + "@upsetjs/venn.js": "^1.4.2", "antd": "^4.16.7", "body-scroll-lock": "^4.0.0-beta.0", "brace": "^0.10.0", @@ -164,6 +165,13 @@ "default-browser-id": { "meow": "6.1.0" }, + "@upsetjs/venn.js": { + "uglify-js": "3.14.3", + "ansi-regex": "3.0.1" + }, + "has-ansi": { + "ansi-regex": "3.0.1" + }, "@reactour/utils": "0.3.0", "@reactour/popover": "0.4.1", "@reactour/mask": "0.5.1" @@ -190,6 +198,7 @@ "schema": "node ./data/getSchema", "params": "node ./data/getTexts > src/params.js", "eslint": "./node_modules/.bin/eslint --ext js --ext jsx --ext ts --ext tsx --ignore-pattern '**/__generated__/**' --ignore-pattern '**/*.min.js' --ignore-pattern '**/setupJest.js' --ignore-pattern '**/gqlHelper.js' --ignore-pattern '**/formHooks.jsx' --ignore-pattern '**/*.stories.jsx' --fix src data", + "eslint-new": "./node_modules/.bin/eslint --ext js --ext jsx --ext ts --ext tsx --ignore-pattern '**/__generated__/**' --ignore-pattern '**/*.min.js' --ignore-pattern '**/setupJest.js' --ignore-pattern '**/gqlHelper.js' --ignore-pattern '**/formHooks.jsx' --ignore-pattern '**/*.json' --ignore-pattern '**/Dockerfile' --ignore-pattern '**/*.png' --ignore-pattern '**/*.ico' --ignore-pattern '**/*.stories.jsx' --ignore-pattern '**/*.css' --ignore-pattern '**/*.md' --ignore-pattern '**/*.yml' --quiet --fix $(git diff --name-only master | xargs)", "elint": "./node_modules/.bin/eslint --ext js --ext jsx --ext ts --ext tsx --ignore-pattern '**/__generated__/**' --ignore-pattern '**/*.min.js' --ignore-pattern '**/setupJest.js'", "storybook": "start-storybook -p 6006 -s .storybook/public", "stylelint": "stylelint 'src/**/*.less' 'src/**/*.css' --config .stylelintrc.js --fix", diff --git a/src/Analysis/GWASV2/GWASContainer.jsx b/src/Analysis/GWASV2/GWASContainer.jsx index bd80f95f3c..22e9372b39 100644 --- a/src/Analysis/GWASV2/GWASContainer.jsx +++ b/src/Analysis/GWASV2/GWASContainer.jsx @@ -1,110 +1,135 @@ -import React, { useState } from "react"; -import { Space, Button, Popconfirm } from "antd"; -import SelectStudyPopulation from "./SelectStudyPopulation/SelectStudyPopulation"; -import "./GWASV2.css"; +import React, { useState } from 'react'; +import { Space, Button, Popconfirm } from 'antd'; +import SelectStudyPopulation from './SelectStudyPopulation/SelectStudyPopulation'; +import ProgressBar from './Shared/ProgressBar/ProgressBar'; +import AttritionTable from './Shared/AttritionTable/AttritionTable'; +import { useSourceFetch } from './Shared/wizardEndpoints/cohortMiddlewareApi'; +import { gwasV2Steps } from './Shared/constants'; +import './GWASV2.css'; const GWASContainer = () => { + const { loading, sourceId } = useSourceFetch(); const [current, setCurrent] = useState(0); const [ selectedStudyPopulationCohort, setSelectedStudyPopulationCohort, ] = useState({}); - const gwasSteps = [ - { - title: "Step 1", - description: "Select Study Population", - }, - { - title: "Step 2", - description: "Select Outcome Phenotypes", - }, - { - title: "Step 3", - description: "Select Covariate Phenotype", - }, - { - title: "Step 4", - description: "Configure GWAS", - }, - ]; + const [selectedControlCohort] = useState(undefined); + const [selectedCaseCohort] = useState(undefined); + const [selectedCovariates] = useState([]); + const [selectedDichotomousCovariates] = useState([]); const generateStep = () => { // steps 2 & 3 very similar switch (current) { - case 0: - // select study population - return ( - - ); - case 1: - // outcome (customdichotomous or not) - return step 2; - case 2: - // covariates (customdichtomous or not) - return step 3; - case 3: - // all other input (mafs, imputation, etc), review, and submit - return step 4; - default: - // required for eslint - return null; + case 0: + // select study population + return ( + + ); + case 1: + // outcome (customdichotomous or not) + return step 2; + case 2: + // covariates (customdichtomous or not) + return step 3; + case 3: + // all other input (mafs, imputation, etc), review, and submit + return step 4; + default: + // required for eslint + return null; } }; + + let nextButtonEnabled = true; + if ( + current === 0 + && Object.keys(selectedStudyPopulationCohort).length === 0 + ) { + nextButtonEnabled = false; + } + return ( + + {!loading && sourceId && ( + + + + + )} {/* Inline style block needed so centering rule doesn't impact other workflows */} - -
- -
+ +
+ +
{generateStep(current)}
-
+
resetGWASType()} - okText="Yes" - cancelText="No" + okText='Yes' + cancelText='No' > - - {current < gwasSteps.length - 1 && ( + {current < gwasV2Steps.length - 1 && ( )} - {current === gwasSteps.length - 1 && ( -
+ {current === gwasV2Steps.length - 1 && ( +
)}
diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.css b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.css new file mode 100644 index 0000000000..7bf04ca490 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.css @@ -0,0 +1,90 @@ +.gwasv2-attrition-table .ant-collapse-header { + background: #cfdbe6; +} + +.gwasv2-attrition-table .ant-collapse-content-box { + padding: 0; +} + +.gwasv2-attrition-table .ant-collapse-expand-icon svg { + display: none; +} + +.gwasv2-attrition-table .ant-collapse-expand-icon { + background: #2e77b8; + border-radius: 16px; + color: white; + height: 18px; + width: 18px; + margin: 3px 10px 0; + text-align: center; +} + +.gwasv2-attrition-table +.ant-collapse-header +.ant-collapse-expand-icon:before { + content: "\2303"; + display: block; + font-size: 16px; + transition: all 0.1s ease-in-out; + margin-top: -1px; +} + +.gwasv2-attrition-table +.ant-collapse-header[aria-expanded="true"] +.ant-collapse-expand-icon:before { + margin-top: -1px; +} + +.gwasv2-attrition-table +.ant-collapse-header[aria-expanded="false"] +.ant-collapse-expand-icon:before { + transform: rotate(180deg); + transform-origin: 7px 9px; + margin: 1px 2px; +} + +.gwasv2-attrition-table table tr { + border-top: 1px solid #e2e2e3; + height: 32px; +} + +.gwasv2-attrition-table table +tr:nth-child(2n) { + background-color: #fafafb; +} + +.gwasv2-attrition-table table { + width: 100%; + padding: 0 16px; +} + +.gwasv2-attrition-table table th, +.gwasv2-attrition-table table td { + text-align: left; + padding: 0 16px; +} + +.gwasv2-attrition-table table thead { + background-color: #e9eef2; + height: 40px; +} + +.gwasv2-attrition-table table .gwasv2-attrition-table--leftpad { + padding-left: 26px; +} +.gwasv2-attrition-table table .gwasv2-attrition-table--chart { + text-align: center; +} +.gwasv2-attrition-table table .gwasv2-attrition-table--rightborder { + border-right: 2px solid #e2e2e3; + padding-right: 32px; +} + +.gwasv2-attrition-table--w5 { + width: 5%; +} + +.gwasv2-attrition-table--w15 { + width: 15%; +} diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.jsx b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.jsx new file mode 100644 index 0000000000..e8352dc5c2 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.jsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Collapse } from 'antd'; +import AttritionTableRow from './AttritionTableRow'; +import '../../../GWASUIApp/GWASUIApp.css'; +import './AttritionTable.css'; + +const { Panel } = Collapse; + +const AttritionTable = ({ + selectedCohort, + otherSelectedCohort, + outcome, + selectedCovariates, + selectedDichotomousCovariates, + sourceId, + tableHeader, +}) => { + const [covariateSubsets, setCovariateSubsets] = useState([]); + const getCovariateRow = (selectedCovs = [], selectedCustomdichotomousCovs = []) => { + const subsets = []; + // todo: handle case of deselecting/selecting existing covariates (100% missing?) after adding custom dichotomous + const allCovariates = [...selectedCovs, ...selectedCustomdichotomousCovs]; + allCovariates + .slice() + .reverse() + .forEach((covariate, i) => { + subsets.push( + allCovariates + .slice() + .reverse() + .slice(allCovariates.length - i - 1), + ); + }); + return subsets; + }; + + useEffect(() => { + setCovariateSubsets(getCovariateRow(selectedCovariates, selectedDichotomousCovariates)); + }, [selectedCovariates, selectedDichotomousCovariates]); + + return ( +
+ event.stopPropagation()}> + + + + + + + + + + + + + + + + {selectedCohort?.cohort_definition_id && ( + + + + )} + {selectedCohort?.cohort_definition_id && covariateSubsets.length > 0 ? ( + covariateSubsets.map((item) => ( + + )) + ) : null} + +
+ Type + ChartNameSize + Non-Hispanic Black + Non-Hispanic AsianNon-Hispanic WhiteHispanic
+
+
+
+ ); +}; + +AttritionTable.propTypes = { + selectedCohort: PropTypes.object, + otherSelectedCohort: PropTypes.object, + outcome: PropTypes.object, + selectedCovariates: PropTypes.array.isRequired, + selectedDichotomousCovariates: PropTypes.array.isRequired, + sourceId: PropTypes.number.isRequired, + tableHeader: PropTypes.string.isRequired, +}; + +AttritionTable.defaultProps = { + selectedCohort: undefined, + otherSelectedCohort: undefined, + outcome: undefined, +}; + +export default AttritionTable; diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.stories.jsx b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.stories.jsx new file mode 100644 index 0000000000..2f6a9efbe8 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTable.stories.jsx @@ -0,0 +1,128 @@ +import React, { useState } from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { rest } from 'msw'; +import AttritionTable from './AttritionTable'; +import '../../GWASV2.css'; + +export default { + title: 'Tests3/GWASV2/AttritionTable', + component: AttritionTable, +}; + +const mockedQueryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, +}); + +const MockTemplate = () => { + //for later: + //const [selectedOutome, setSelectedOutcome] = useState({ variable_type: 'custom_dichotomous', cohort_ids: [1, 2], provided_name: 'test outcome', uuid: '1234' }) + const [selectedOutome, setSelectedOutcome] = useState({ concept_id: 2000006886, concept_name: 'Attribute1', concept_code: '', concept_type: 'MVP Continuous', }) + const [selectedCohort, setSelectedCohort] = useState({cohort_definition_id: 123, cohort_name: 'cohort name abc'}); + const [otherSelectedCohort, otherSetSelectedCohort] = useState({cohort_definition_id: 456, cohort_name: 'cohort name def'}); + const [selectedDichotomousCovariates, setSelectedDichotomousCovariates] = useState([ + { variable_type: 'custom_dichotomous', cohort_ids: [1, 2], provided_name: 'dichotomous test1' , uuid: '12345'}, + { variable_type: 'custom_dichotomous', cohort_ids: [3, 4], provided_name: 'dichotomous test2' , uuid: '123456'}]); + const [selectedCovariates, setSelectedCovariates] = useState([ + { + concept_id: 2000006886, prefixed_concept_id: 'ID_2000006886', concept_name: 'Attribute1', concept_code: '', concept_type: 'MVP Continuous', + }, + { + concept_id: 2000006885, prefixed_concept_id: 'ID_2000006885', concept_name: 'Attribute10', concept_code: '', concept_type: 'MVP Continuous', + }, + { + concept_id: 2000000708, prefixed_concept_id: 'ID_2000000708', concept_name: 'Attribute11', concept_code: '', concept_type: 'MVP Continuous', + }]); + + return ( + + + + + + ); +}; + +let rowCount = 0; + +export const MockedSuccess = MockTemplate.bind({}); +MockedSuccess.parameters = { + msw: { + handlers: [ + rest.post('http://:cohortmiddlewarepath/cohort-middleware/concept-stats/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinition/breakdown-by-concept-id/:breakdownconceptid', (req, res, ctx) => { + const { cohortmiddlewarepath } = req.params; + const { cohortdefinition } = req.params; + rowCount++; + if (rowCount == 12) { + // simulate empty response scenario: + return res( + ctx.delay(200*rowCount), + ctx.json({ + concept_breakdown: null}) + ); + } + return res( + ctx.delay(200*rowCount), + ctx.json({ + concept_breakdown: [ + { + concept_value: 'ASN', + concept_value_as_concept_id: 2000007029, + concept_value_name: 'non-Hispanic Asian', + persons_in_cohort_with_value: 40178 * (20-rowCount), // just to mock/generate different numbers for different cohorts, + }, + { + concept_value: 'EUR', + concept_value_as_concept_id: 2000007031, + concept_value_name: 'non-Hispanic White', + persons_in_cohort_with_value: 39648 * (20-rowCount), // just to mock/generate different numbers for different cohorts, + }, + { + concept_value: 'AFR', + concept_value_as_concept_id: 2000007030, + concept_value_name: 'non-Hispanic Black', + persons_in_cohort_with_value: 40107 * (20-rowCount), // just to mock/generate different numbers for different cohorts, + }, + { + concept_value: 'HIS', + concept_value_as_concept_id: 2000007028, + concept_value_name: 'Hispanic', + persons_in_cohort_with_value: 40038 * (20-rowCount), // just to mock/generate different numbers for different cohorts, + }, + ], + }), + ); + }), + ], + }, +}; + +export const MockedError = MockTemplate.bind({}); +MockedError.parameters = { + msw: { + handlers: [ + rest.post('', (req, res, ctx) => res( + ctx.delay(800), + ctx.status(403), + )), + ], + }, +}; diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTableRow.jsx b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTableRow.jsx new file mode 100644 index 0000000000..6adc9e7f51 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/AttritionTableRow.jsx @@ -0,0 +1,144 @@ +/* eslint-disable camelcase */ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useQuery } from 'react-query'; +import { Spin } from 'antd'; +import { + fetchConceptStatsByHareSubset, + fetchConceptStatsByHareSubsetCC, + queryConfig, +} from '../wizardEndpoints/cohortMiddlewareApi'; +import BarChart from './ChartIcons/BarChart'; +import EulerDiagram from './ChartIcons/EulerDiagram'; +import './AttritionTable.css'; + +const AttritionTableRow = ({ + cohortDefinitionId, + otherCohortDefinitionId, + rowType, + rowName, + covariateSubset, + sourceId, +}) => { + const [breakdownSize, setBreakdownSize] = useState(undefined); + const [breakdownColumns, setBreakdownColumns] = useState([]); + const [afr, setAfr] = useState(undefined); + const [asn, setAsn] = useState(undefined); + const [eur, setEur] = useState(undefined); + const [his, setHis] = useState(undefined); + + const { data, status } = useQuery( + [ + 'conceptstatsbyharesubset', + covariateSubset, + cohortDefinitionId, + otherCohortDefinitionId, + ], + () => (!(cohortDefinitionId && otherCohortDefinitionId) + // if there are not two cohorts selected, then quantitative + // Otherwise if there are two cohorts selected, case control + ? fetchConceptStatsByHareSubset( + cohortDefinitionId, + covariateSubset, + sourceId, + ) : fetchConceptStatsByHareSubsetCC( + cohortDefinitionId, + otherCohortDefinitionId, + covariateSubset, + sourceId, + )), + queryConfig, + ); + + const { breakdown } = { breakdown: data?.concept_breakdown }; + + const getSizeByColumn = (hare) => { + const hareIndex = breakdownColumns.findIndex( + ({ concept_value }) => concept_value === hare, + ); + return hareIndex > -1 + ? breakdownColumns[hareIndex].persons_in_cohort_with_value + : 0; + }; + + useEffect(() => { + if (breakdown?.length) { + const filteredBreakdown = breakdown.filter( + ({ concept_value }) => concept_value !== 'OTH', + ); + setBreakdownSize( + filteredBreakdown.reduce( + (acc, curr) => acc + curr.persons_in_cohort_with_value, + 0, + ), + ); + setBreakdownColumns(filteredBreakdown); + } else { + setBreakdownSize(0); + setBreakdownColumns([]); + } + }, [breakdown, cohortDefinitionId, covariateSubset, sourceId]); + + useEffect(() => { + setAfr(getSizeByColumn('AFR')); + setAsn(getSizeByColumn('ASN')); + setEur(getSizeByColumn('EUR')); + setHis(getSizeByColumn('HIS')); + }, [breakdownColumns]); + + const determineChartIcon = (rowTypeInput) => { + if (rowTypeInput === 'Cohort') { + return null; + } + + /* + TODO: Write logic such that if the covariate is numeric, it is bar chart + and if the covariate is dichotomous, it should be a euler diagram + */ + return (Math.random() > 0.5) ? : ; + }; + return ( + + + {rowType} + + + {determineChartIcon(rowType)} + + {rowName} + + {status === 'loading' ? : breakdownSize || 0} + + + {status === 'loading' ? : afr || 0} + + + {status === 'loading' ? : asn || 0} + + + {status === 'loading' ? : eur || 0} + + + {status === 'loading' ? : his || 0} + + + ); +}; + +AttritionTableRow.propTypes = { + cohortDefinitionId: PropTypes.number, + otherCohortDefinitionId: PropTypes.number, + rowType: PropTypes.string.isRequired, + rowName: PropTypes.string.isRequired, + covariateSubset: PropTypes.array.isRequired, + sourceId: PropTypes.number.isRequired, +}; + +AttritionTableRow.defaultProps = { + cohortDefinitionId: undefined, + otherCohortDefinitionId: undefined, +}; + +export default AttritionTableRow; diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/BarChart.jsx b/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/BarChart.jsx new file mode 100644 index 0000000000..c37d67fd13 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/BarChart.jsx @@ -0,0 +1,39 @@ +import React from 'react'; + +const BarChart = () => ( + + An icon of a bar chart + + + + + + +); + +export default BarChart; diff --git a/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/EulerDiagram.jsx b/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/EulerDiagram.jsx new file mode 100644 index 0000000000..73d5715e8b --- /dev/null +++ b/src/Analysis/GWASV2/Shared/AttritionTable/ChartIcons/EulerDiagram.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const EulerDiagram = () => ( + + An icon of a Euler Diagram + + +); + +export default EulerDiagram; diff --git a/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.jsx b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.jsx new file mode 100644 index 0000000000..cc139a6727 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useQueries } from 'react-query'; +import { Spin } from 'antd'; +import { fetchSimpleOverlapInfo, queryConfig, addCDFilter } from '../wizardEndpoints/cohortMiddlewareApi'; +import Simple3SetsEulerDiagram from './Simple3SetsEulerDiagram'; + +const CohortsOverlapDiagram = ({ + sourceId, + selectedStudyPopulationCohort, + selectedCaseCohort, + selectedControlCohort, + selectedCovariates, + selectedDichotomousCovariates, +}) => { + const results = useQueries([ + { + queryKey: [ + 'checkoverlap', + sourceId, + selectedStudyPopulationCohort, + selectedCaseCohort, + selectedCovariates, + selectedDichotomousCovariates, + ], + queryFn: () => fetchSimpleOverlapInfo( + sourceId, + selectedStudyPopulationCohort.cohort_definition_id, + selectedCaseCohort.cohort_definition_id, + selectedCovariates, + selectedDichotomousCovariates, + ), + ...queryConfig, + }, + { + queryKey: [ + 'checkoverlap', + sourceId, + selectedStudyPopulationCohort, + selectedControlCohort, + selectedCovariates, + selectedDichotomousCovariates, + ], + queryFn: () => fetchSimpleOverlapInfo( + sourceId, + selectedStudyPopulationCohort.cohort_definition_id, + selectedControlCohort.cohort_definition_id, + selectedCovariates, + selectedDichotomousCovariates, + ), + ...queryConfig, + }, + { + queryKey: [ + 'checkoverlap', + sourceId, + selectedCaseCohort, + selectedControlCohort, + selectedCovariates, + selectedDichotomousCovariates, + ], + queryFn: () => fetchSimpleOverlapInfo( + sourceId, + selectedCaseCohort.cohort_definition_id, + selectedControlCohort.cohort_definition_id, + selectedCovariates, + selectedDichotomousCovariates, + ), + ...queryConfig, + }, + // special case: the overlap of study population with case, excluding any intersection w/ cohort: + { + queryKey: [ + 'checkoverlap', + sourceId, + selectedStudyPopulationCohort, + selectedCaseCohort, + selectedControlCohort, + selectedCovariates, + selectedDichotomousCovariates, + ], + queryFn: () => fetchSimpleOverlapInfo( + sourceId, + selectedStudyPopulationCohort.cohort_definition_id, + selectedCaseCohort.cohort_definition_id, + selectedCovariates, + addCDFilter(selectedCaseCohort.cohort_definition_id, + selectedControlCohort.cohort_definition_id, + selectedDichotomousCovariates), // ==> adds case/control as extra variable + ), + ...queryConfig, + }, + ]); + + const { + statusPopCase, statusPopControl, statusCaseControl, statusPopCaseControl, + dataPopCase, dataPopControl, dataCaseControl, dataPopCaseMinusPopCaseControl, + } = { + statusPopCase: results[0].status, + statusPopControl: results[1].status, + statusCaseControl: results[2].status, + statusPopCaseControl: results[3].status, + dataPopCase: results[0].data, + dataPopControl: results[1].data, + dataCaseControl: results[2].data, + dataPopCaseMinusPopCaseControl: results[3].data, + }; + + if ([statusPopCase, statusPopControl, statusCaseControl, statusPopCaseControl].some((status) => status === 'error')) { + return Error getting data for diagram; + } if ([statusPopCase, statusPopControl, statusCaseControl, statusPopCaseControl].some((status) => status === 'loading')) { + return ; + } + const eulerArgs = { + set1Size: selectedStudyPopulationCohort.size, + set2Size: selectedCaseCohort.size, + set3Size: selectedControlCohort.size, + set12Size: dataPopCase.cohort_overlap.overlap_after_filter, + set13Size: dataPopControl.cohort_overlap.overlap_after_filter, + set23Size: dataCaseControl.cohort_overlap.overlap_after_filter, + set123Size: dataPopCase.cohort_overlap.overlap_after_filter - dataPopCaseMinusPopCaseControl.cohort_overlap.overlap_after_filter, + set1Label: selectedStudyPopulationCohort.cohort_name, + set2Label: selectedCaseCohort.cohort_name, + set3Label: selectedControlCohort.cohort_name, + }; + return ( + + ); +}; + +CohortsOverlapDiagram.propTypes = { + sourceId: PropTypes.number.isRequired, + selectedStudyPopulationCohort: PropTypes.object.isRequired, + selectedCaseCohort: PropTypes.object.isRequired, + selectedControlCohort: PropTypes.object.isRequired, + selectedCovariates: PropTypes.array.isRequired, + selectedDichotomousCovariates: PropTypes.array.isRequired, +}; + +export default CohortsOverlapDiagram; diff --git a/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.stories.jsx b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.stories.jsx new file mode 100644 index 0000000000..c54e40ac23 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/CohortsOverlapDiagram.stories.jsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { rest } from 'msw'; +import CohortsOverlapDiagram from './CohortsOverlapDiagram'; + +export default { + title: "Tests3/GWASV2/CohortsOverlapDiagram", + component: CohortsOverlapDiagram, +}; + +const mockedQueryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, +}); + +const Template = (args) => + + +; + + +const selectedStudyPopulationCohort = { + cohort_definition_id: 25, + cohort_name: 'Mock population ', + size: 4000, +}; +const selectedCaseCohort = { + cohort_definition_id: 123, + cohort_name: 'Mock case cohort A', + size: 2500, +}; +const selectedControlCohort = { + cohort_definition_id: 456, + cohort_name: 'Mock control cohort with loooong name to test how a long name is rendered', + size: 1800, +}; + +export const SuccessCase = Template.bind({}); +SuccessCase.args = { + sourceId: 123, + selectedStudyPopulationCohort: selectedStudyPopulationCohort, + selectedCaseCohort: selectedCaseCohort, + selectedControlCohort: selectedControlCohort, + selectedCovariates: [], + selectedDichotomousCovariates: [], +} +SuccessCase.parameters = { + // msw mocking: + msw: { + handlers: [ + rest.post('http://:cohortmiddlewarepath/cohort-middleware/cohort-stats/check-overlap/by-source-id/:sourceid/by-cohort-definition-ids/:cohortdefinitionA/:cohortdefinitionB', (req, res, ctx) => { + const { cohortmiddlewarepath } = req.params; + const { cohortdefinitionA } = req.params; + const { cohortdefinitionB } = req.params; + return res( + ctx.delay(1100), + ctx.json( + {"cohort_overlap": + {"overlap_after_filter": Math.floor(Math.random() * 500)} // because of random here, we get some data that does not really make sense...SuccessCase2 tries to fix that for some of the relevant group overlaps... + }), + ); + }), + ], + }, +}; + +// similar to test above, but with some overlap values fixed: +export const SuccessCase2 = Template.bind({}); +SuccessCase2.args = { + sourceId: 123, + selectedStudyPopulationCohort: selectedStudyPopulationCohort, + selectedCaseCohort: selectedCaseCohort, + selectedControlCohort: selectedControlCohort, + selectedCovariates: [], + selectedDichotomousCovariates: [], +} +let variableOverlap = 234; +SuccessCase2.parameters = { + // msw mocking: + msw: { + handlers: [ + rest.post('http://:cohortmiddlewarepath/cohort-middleware/cohort-stats/check-overlap/by-source-id/:sourceid/by-cohort-definition-ids/:cohortdefinitionA/:cohortdefinitionB', (req, res, ctx) => { + const { cohortmiddlewarepath } = req.params; + const { cohortdefinitionA } = req.params; + const { cohortdefinitionB } = req.params; + return res( + ctx.delay(500), + ctx.json( + {"cohort_overlap": + {"overlap_after_filter": ( + cohortdefinitionA == selectedStudyPopulationCohort.cohort_definition_id && + cohortdefinitionB == selectedCaseCohort.cohort_definition_id + ) ? variableOverlap--: + ( + cohortdefinitionA == selectedCaseCohort.cohort_definition_id && + cohortdefinitionB == selectedControlCohort.cohort_definition_id + ) ? 1000 : Math.floor(Math.random() * 500)} + }), + ); + }), + ], + }, +}; + + +export const ErrorCase = Template.bind({}); +ErrorCase.args = { + sourceId: 123, + selectedStudyPopulationCohort: selectedStudyPopulationCohort, + selectedCaseCohort: selectedCaseCohort, + selectedControlCohort: selectedControlCohort, + selectedCovariates: [], + selectedDichotomousCovariates: [], +} +// mock endpoint failure: +ErrorCase.parameters = { + msw: { + handlers: [ + rest.post('http://:cohortmiddlewarepath/cohort-middleware/cohort-stats/check-overlap/by-source-id/:sourceid/by-cohort-definition-ids/:cohortdefinitionA/:cohortdefinitionB', (req, res, ctx) => res( + ctx.delay(800), + ctx.status(403), + )), + ], + }, +}; diff --git a/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.jsx b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.jsx new file mode 100644 index 0000000000..bb8be6e760 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.jsx @@ -0,0 +1,76 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import * as d3 from 'd3-selection'; +import * as venn from '@upsetjs/venn.js'; + +const Simple3SetsEulerDiagram = ({ + set1Size, + set2Size, + set3Size, + set12Size, + set13Size, + set23Size, + set123Size, + // optional labels: + set1Label, + set2Label, + set3Label, + set12Label, + set13Label, + set23Label, + set123Label, +}) => { + const sets = [ + { sets: ['1'], size: set1Size, label: set1Label || Number(set1Size).toLocaleString() }, // TODO - these items take "color": as an attribute. So we can use that to guarantee the color and add a legend. + { sets: ['2'], size: set2Size, label: set2Label || Number(set2Size).toLocaleString() }, + { sets: ['3'], size: set3Size, label: set3Label || Number(set3Size).toLocaleString() }, + { sets: ['1', '2'], size: set12Size, label: set12Label || Number(set12Size).toLocaleString() }, + { sets: ['1', '3'], size: set13Size, label: set13Label || Number(set13Size).toLocaleString() }, + { sets: ['2', '3'], size: set23Size, label: set23Label || Number(set23Size).toLocaleString() }, + { sets: ['1', '2', '3'], size: set123Size, label: set123Label || Number(set123Size).toLocaleString() }, + ]; + + useEffect(() => { + // some basic validation: + if ((set1Size < set12Size || set1Size < set13Size) + || (set2Size < set12Size || set2Size < set23Size) + || (set3Size < set13Size || set3Size < set23Size)) { + throw Error('Error: invalid set sizes. A set overlap cannot be bigger than the set itself.'); + } + const chart = venn.VennDiagram(); + d3.select('#euler').datum(sets).call(chart); + }, [sets]); + + return ( +
+ ); +}; + +Simple3SetsEulerDiagram.propTypes = { + set1Size: PropTypes.number.isRequired, + set1Label: PropTypes.string, + set2Size: PropTypes.number.isRequired, + set2Label: PropTypes.string, + set3Size: PropTypes.number.isRequired, + set3Label: PropTypes.string, + set12Size: PropTypes.number.isRequired, + set12Label: PropTypes.string, + set13Size: PropTypes.number.isRequired, + set13Label: PropTypes.string, + set23Size: PropTypes.number.isRequired, + set23Label: PropTypes.string, + set123Size: PropTypes.number.isRequired, + set123Label: PropTypes.string, +}; + +Simple3SetsEulerDiagram.defaultProps = { + set1Label: undefined, + set2Label: undefined, + set3Label: undefined, + set12Label: undefined, + set13Label: undefined, + set23Label: undefined, + set123Label: undefined, +}; + +export default Simple3SetsEulerDiagram; diff --git a/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.stories.jsx b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.stories.jsx new file mode 100644 index 0000000000..a82a4ab882 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/CohortsOverlapDiagram/Simple3SetsEulerDiagram.stories.jsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import Simple3GroupsEulerDiagram from './Simple3SetsEulerDiagram'; + +export default { + title: "Tests3/GWASV2/EulerDiagram", + component: Simple3GroupsEulerDiagram, +}; + +const Template = (args) => ; + +export const FirstStepActive = Template.bind({}); +FirstStepActive.args = { + set1Size: 1000, + set2Size: 900, + set3Size: 4000, + set12Size: 100, + set13Size: 200, + set23Size: 300, + set123Size: 123, +}; + +export const SecondStepError = Template.bind({}); +SecondStepError.args = { + set1Size: 1000, + set2Size: 900, + set3Size: 4000, + set12Size: 100, + set13Size: 20000, + set23Size: 300, + set123Size: 123, +}; diff --git a/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.css b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.css new file mode 100644 index 0000000000..677e98f733 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.css @@ -0,0 +1,123 @@ +.progress-bar { + width: 100%; + margin-bottom: 75px; +} + +.progress-bar__steps { + width: 60%; + float: left; +} + +.progress-bar button.ant-btn-default { + float: right; + color: #2e77b8; + border: 1px solid #2e77b8; + transition: none; + margin-top: 5px; +} + +@media only screen and (max-width: 1350px) { + .progress-bar { + margin-bottom: 0; + } + .progress-bar button.ant-btn-default, + .progress-bar__steps { + float: none; + } + .progress-bar button.ant-btn-default { + margin: 25px 0; + } +} + +.progress-bar button.ant-btn-default:hover { + background: var(--g3-secondary-btn__bg-color--hover); + color: white; +} + +.progress-bar .ant-steps-item-icon { + font-size: 22px; + text-align: left; + width: auto; +} + +.progress-bar .ant-steps-item-container { + border-bottom: 3px solid black; +} + +.progress-bar .ant-steps-item-active:before { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 0; + border-bottom: solid 3px #ef8523; + animation: border_anim 0.3s linear forwards; +} + +.progress-bar .ant-steps-item-title { + line-height: 37px; +} + +.progress-bar .ant-steps-item-wait .ant-steps-item-icon > .ant-steps-icon, +.progress-bar .ant-steps-item-finish .ant-steps-item-icon > .ant-steps-icon, +.progress-bar +.ant-steps-item-process +> .ant-steps-item-container +> .ant-steps-item-icon +.ant-steps-icon, +.ant-steps-item-wait +> .ant-steps-item-container +> .ant-steps-item-content +> .ant-steps-item-title { + color: black; +} + +.progress-bar +.ant-steps-item-active.ant-steps-item-process +> .ant-steps-item-container +> .ant-steps-item-icon +.ant-steps-icon { + color: #ab590d; +} + +.progress-bar +.ant-steps-item-active.ant-steps-item-process +> .ant-steps-item-container +> .ant-steps-item-content +> .ant-steps-item-title { + color: #ab590d; +} + +.progress-bar .ant-steps-item-active .ant-steps-item-container { + border-bottom: none; +} + +.progress-bar .ant-steps-item-wait .ant-steps-item-icon, +.progress-bar +.ant-steps-item-process +> .ant-steps-item-container +> .ant-steps-item-icon { + background: none; + border: none; +} + +.progress-bar +.ant-steps-horizontal:not(.ant-steps-label-vertical) +.ant-steps-item { + min-width: 250px; + padding-left: 0; + margin-right: 16px; +} + +.progress-bar .ant-steps-item-title::after { + display: none; +} + +@keyframes border_anim { + 0% { + width: 0%; + } + 100% { + width: 100%; + } +} diff --git a/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.jsx b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.jsx new file mode 100644 index 0000000000..3d3e8e3078 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Steps } from 'antd'; +import { gwasV2Steps } from '../constants'; +import './ProgressBar.css'; + +const { Step } = Steps; +const ProgressBar = ({ current }) => ( +
+
+ + {gwasV2Steps.map((item, index) => ( + {index + 1}} + title={`${current <= index ? item.title : item.secondaryTitle}`} + /> + ))} + +
+ +
+); + +ProgressBar.propTypes = { + current: PropTypes.number.isRequired, +}; + +export default ProgressBar; diff --git a/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.stories.jsx b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.stories.jsx new file mode 100644 index 0000000000..746eb762a1 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.stories.jsx @@ -0,0 +1,29 @@ +import React, { useState } from "react"; +import ProgressBar from "./ProgressBar"; + +export default { + title: "Tests3/GWASV2/ProgressBar", + component: ProgressBar, +}; + +const Template = (args) => ; + +export const FirstStepActive = Template.bind({}); +FirstStepActive.args = { + current: 0, +}; + +export const SecondStepActive = Template.bind({}); +SecondStepActive.args = { + current: 1, +}; + +export const ThirdStepActive = Template.bind({}); +ThirdStepActive.args = { + current: 2, +}; + +export const FourthStepActive = Template.bind({}); +FourthStepActive.args = { + current: 3, +}; diff --git a/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.test.jsx b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.test.jsx new file mode 100644 index 0000000000..bc52dbb8ee --- /dev/null +++ b/src/Analysis/GWASV2/Shared/ProgressBar/ProgressBar.test.jsx @@ -0,0 +1,81 @@ +import React from "react"; +import Enzyme, { render, mount } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +import ProgressBar from "./ProgressBar"; +import { gwasV2Steps } from "../../Shared/constants"; + +Enzyme.configure({ adapter: new Adapter() }); + +/* + Code to aid in Jest Mocking, see: + https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function +*/ +window.matchMedia = + window.matchMedia || + function() { + return { + matches: false, + addListener() {}, + removeListener() {}, + }; + }; + +/* HELPER METHODS */ +const testElementText = (wrapper, elNum, text) => { + const testElement = wrapper.find(`div.ant-steps-item:nth-child(${elNum})`); + expect(testElement).toHaveLength(1); + expect(testElement.text()).toEqual(text); +}; + +const testElementClass = (wrapper, elNum, className) => { + /* + Enzyme has problems using Selectors, work around from: + https://stackoverflow.com/questions/56145868/how-to-test-all-children-from-a-selector-except-the-first-child-in-jest + */ + wrapper.find("div.ant-steps-item").forEach((item, index) => { + if (index === elNum - 1) { + expect(item.hasClass(className)).toEqual(true); + } else { + expect(item.hasClass(className)).toEqual(false); + } + }); +}; + +/* TESTS */ +/* Test active step class */ +describe("Test that active step class renders with active class when current is between 0 and 3", () => { + for (let i = 0; i < 4; i = i + 1) { + const wrapper = mount(); + it(`should render step ${i + + 1} with active class when current is ${i}`, () => { + testElementClass(wrapper, i + 1, "ant-steps-item-active"); + }); + } +}); + +/* Test Dynamic Text for Steps */ +describe("Test that each step renders with correct text when current is between 0 and 3", () => { + for (let i = 0; i < 4; i = i + 1) { + const wrapper = render(); + const correctTextStep1 = + i === 0 ? gwasV2Steps[0].title : gwasV2Steps[0].secondaryTitle; + const correctTextStep2 = + i <= 1 ? gwasV2Steps[1].title : gwasV2Steps[1].secondaryTitle; + const correctTextStep3 = + i <= 2 ? gwasV2Steps[2].title : gwasV2Steps[2].secondaryTitle; + const correctTextStep4 = + i <= 3 ? gwasV2Steps[3].title : gwasV2Steps[3].secondaryTitle; + it(`should render first step with correct text: 1${correctTextStep1} when current is ${i}`, () => { + testElementText(wrapper, 1, `1${correctTextStep1}`); + }); + it(`should render second step with correct text: 2${correctTextStep2} when current is ${i}`, () => { + testElementText(wrapper, 2, `2${correctTextStep2}`); + }); + it(`should render third step with correct text: 3${correctTextStep3} when current is ${i}`, () => { + testElementText(wrapper, 3, `3${correctTextStep3}`); + }); + it(`should render fourth step with correct text: 4${correctTextStep4} when current is ${i}`, () => { + testElementText(wrapper, 4, `4${correctTextStep4}`); + }); + } +}); diff --git a/src/Analysis/GWASV2/Shared/constants.js b/src/Analysis/GWASV2/Shared/constants.js new file mode 100644 index 0000000000..f5066d0363 --- /dev/null +++ b/src/Analysis/GWASV2/Shared/constants.js @@ -0,0 +1,19 @@ +/* eslint-disable import/prefer-default-export */ +export const gwasV2Steps = [ + { + title: 'Select Study Population', + secondaryTitle: 'Edit Study Population', + }, + { + title: 'Select Outcome Phenotype', + secondaryTitle: 'Edit Outcome Phenotype', + }, + { + title: 'Select Covariate Phenotype', + secondaryTitle: 'Edit Covariate Phenotype', + }, + { + title: 'Configure GWAS', + secondaryTitle: 'Configure GWAS', + }, +]; diff --git a/src/Analysis/GWASV2/Shared/wizardEndpoints/cohortMiddlewareApi.js b/src/Analysis/GWASV2/Shared/wizardEndpoints/cohortMiddlewareApi.js index 3184bef09a..b6c8e729ca 100644 --- a/src/Analysis/GWASV2/Shared/wizardEndpoints/cohortMiddlewareApi.js +++ b/src/Analysis/GWASV2/Shared/wizardEndpoints/cohortMiddlewareApi.js @@ -60,6 +60,34 @@ export const fetchOverlapInfo = async ( return getOverlapStats.json(); }; +// Basically a copy of fetchOverlapInfo above, but without the HARE arguments: +export const fetchSimpleOverlapInfo = async ( + sourceId, + cohortAId, + cohortBId, + selectedCovariates, + selectedDichotomousCovariates, +) => { + const variablesPayload = { + variables: [ + ...selectedDichotomousCovariates.map(({ uuid, ...withNoId }) => withNoId), + ...selectedCovariates.map((c) => ({ + variable_type: 'concept', + concept_id: c.concept_id, + })), + ], + }; + const statsEndPoint = `${cohortMiddlewarePath}cohort-stats/check-overlap/by-source-id/${sourceId}/by-cohort-definition-ids/${cohortAId}/${cohortBId}`; + const reqBody = { + method: 'POST', + credentials: 'include', + headers, + body: JSON.stringify(variablesPayload), + }; + const getOverlapStats = await fetch(statsEndPoint, reqBody); + return getOverlapStats.json(); +}; + export const filterSubsetCovariates = (subsetCovariates) => { const filteredSubsets = []; subsetCovariates.forEach((covariate) => {