From a63bf9d5a35d5e74494c8f64ea2e019ea65eb765 Mon Sep 17 00:00:00 2001 From: maniarathi Date: Wed, 16 Sep 2020 14:46:59 -0700 Subject: [PATCH 01/73] Change psycopg to be binary (#1842) --- .github/workflows/compatibility_tests.yml | 1 + server/requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/compatibility_tests.yml b/.github/workflows/compatibility_tests.yml index bd3c41e8..f0e55f38 100644 --- a/.github/workflows/compatibility_tests.yml +++ b/.github/workflows/compatibility_tests.yml @@ -25,6 +25,7 @@ jobs: cellxgene-main-with-python-and-anndata-versions: name: python versions x anndata versions runs-on: ubuntu-latest + continue-on-error: true strategy: matrix: python-version: [3.6, 3.7, 3.8] diff --git a/server/requirements-dev.txt b/server/requirements-dev.txt index 0fb57ce8..0882128a 100644 --- a/server/requirements-dev.txt +++ b/server/requirements-dev.txt @@ -3,7 +3,7 @@ black bumpversion>=0.5 codecov>=2.0.15 parameterized>=0.7.0 -psycopg2==2.7.7 +psycopg2-binary>=2.8.5 pytest>=3.6.3 python-jose>=3.2.0 scanpy>=1.4.6 From 25c272ae8e33edab808f369c2bc65d0cc7a08342 Mon Sep 17 00:00:00 2001 From: bmccandless Date: Wed, 16 Sep 2020 17:37:52 -0700 Subject: [PATCH 02/73] minor fix to auth redirect (#1845) The previous version added and extra "/" to the url after login: e.g: https://cellxgene.dev.single-cell.czi.technology/d/pbmc3k.cxg// --- server/auth/auth_oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/auth/auth_oauth.py b/server/auth/auth_oauth.py index 2b4ddf45..a5107934 100644 --- a/server/auth/auth_oauth.py +++ b/server/auth/auth_oauth.py @@ -158,7 +158,7 @@ class AuthTypeOAuth(AuthTypeClientBase): def login(self): callbackurl = f"{self.api_base_url}/oauth2/callback" return_path = request.args.get("dataset", "") - return_to = f"{self.web_base_url}/{return_path}/" + return_to = f"{self.web_base_url}/{return_path}" # save the return path in the session cookie, accessed in the callback function session["oauth_callback_redirect"] = return_to response = self.client.authorize_redirect(redirect_uri=callbackurl) From 14fbe0aa77a50d8a9a78ed2608cb2e19d9810bb9 Mon Sep 17 00:00:00 2001 From: bmccandless Date: Thu, 17 Sep 2020 17:14:08 -0700 Subject: [PATCH 03/73] Fix the /health endpoint (#1847) * Fix the /health endpoint #1846 Keep both the old and new locations until the deployments are upgraded. --- server/app/app.py | 38 ++++++++++++++++++---- server/test/unit/common/test_app_config.py | 14 ++++++-- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/server/app/app.py b/server/app/app.py index d926f515..7e02b4e1 100644 --- a/server/app/app.py +++ b/server/app/app.py @@ -99,6 +99,10 @@ def dataset_index(url_dataroot=None, dataset=None): ) +# TODO: This route will be deprecated, but needs to be left for a short time until all the +# deployments are upgraded to the new location for the health check (or else the upgrade will +# fail). Once the upgrade is complete, the deployments can move to the new health check URL +# and this route will be removed. @webbp.route("/health", methods=["GET"]) @cache_control_always(no_store=True) def health(): @@ -224,6 +228,13 @@ def dataroot_index(): return redirect(config.server_config.multi_dataset__index) +class HealthAPI(Resource): + @cache_control(no_store=True) + def get(self): + config = current_app.app_config + return health_check(config) + + class DatasetResource(Resource): """Base class for all Resources that act on datasets.""" @@ -312,8 +323,18 @@ class LayoutObsAPI(DatasetResource): return common_rest.layout_obs_put(request, data_adaptor) -def get_api_resources(bp_api, url_dataroot=None): - api = Api(bp_api) +def get_api_base_resources(bp_base): + """Add resources that are accessed from the api_base_url""" + api = Api(bp_base) + + # Diagnostics routes + api.add_resource(HealthAPI, "/health") + return api + + +def get_api_dataroot_resources(bp_dataroot, url_dataroot=None): + """Add resources that refer to a dataset""" + api = Api(bp_dataroot) def add_resource(resource, url): """convenience function to make the outer function less verbose""" @@ -385,18 +406,23 @@ class Server: parse = urlparse(api_base_url) api_path = parse.path + bp_base = Blueprint("bp_base", __name__, url_prefix=api_path) + base_resources = get_api_base_resources(bp_base) + self.app.register_blueprint(base_resources.blueprint) + if app_config.is_multi_dataset(): # NOTE: These routes only allow the dataset to be in the directory # of the dataroot, and not a subdirectory. We may want to change # the route format at some point for dataroot_dict in server_config.multi_dataset__dataroot.values(): url_dataroot = dataroot_dict["base_url"] - bp_api = Blueprint( + bp_dataroot = Blueprint( f"api_dataset_{url_dataroot}", __name__, url_prefix=f"{api_path}/{url_dataroot}/" + api_version ) - resources = get_api_resources(bp_api, url_dataroot) - self.app.register_blueprint(resources.blueprint) + dataroot_resources = get_api_dataroot_resources(bp_dataroot, url_dataroot) + self.app.register_blueprint(dataroot_resources.blueprint) + self.app.add_url_rule( f"/{url_dataroot}//", f"dataset_index_{url_dataroot}", @@ -412,7 +438,7 @@ class Server: else: bp_api = Blueprint("api", __name__, url_prefix=f"{api_path}{api_version}") - resources = get_api_resources(bp_api) + resources = get_api_dataroot_resources(bp_api) self.app.register_blueprint(resources.blueprint) self.app.add_url_rule( "/static/", diff --git a/server/test/unit/common/test_app_config.py b/server/test/unit/common/test_app_config.py index f8a20093..6b86d771 100644 --- a/server/test/unit/common/test_app_config.py +++ b/server/test/unit/common/test_app_config.py @@ -142,7 +142,7 @@ class AppConfigTest(unittest.TestCase): config = AppConfig() backend_port = find_available_port("localhost", 10000) config.update_server_config( - app__api_base_url=f"http://localhost:{backend_port}/additional/path/before/dataroot", + app__api_base_url=f"http://localhost:{backend_port}/additional/path", multi_dataset__dataroot=f"{PROJECT_ROOT}/example-dataset" ) @@ -151,11 +151,21 @@ class AppConfigTest(unittest.TestCase): with test_server(["-p", str(backend_port)], app_config=config) as server: session = requests.Session() self.assertEqual(server, f"http://localhost:{backend_port}") - response = session.get(f"{server}/additional/path/before/dataroot/d/pbmc3k.h5ad/api/v0.2/config") + response = session.get(f"{server}/additional/path/d/pbmc3k.h5ad/api/v0.2/config") self.assertEqual(response.status_code, 200) data_config = response.json() self.assertEqual(data_config["config"]["displayNames"]["dataset"], "pbmc3k") + # test the health check at the correct url + response = session.get(f"{server}/additional/path/health") + assert response.json()["status"] == "pass" + + # also check that the old URL still works. + # NOTE: this old URL location will soon be deprecated, and when that happens + # this check can be removed. + response = session.get(f"{server}/health") + assert response.json()["status"] == "pass" + def test_configfile_with_specialization(self): # test that per_dataset_config config load the default config, then the specialized config From 210042814fb560713bff50d5dfe68b2f55ed7499 Mon Sep 17 00:00:00 2001 From: Severiano Badajoz Date: Fri, 18 Sep 2020 13:16:28 -0700 Subject: [PATCH 04/73] Info Drawer format adjustments (#1853) This PR tweaks the look and feel of the info drawer in response to QA from @signechambers1 --- .../e2e/__snapshots__/e2e.test.js.snap | 4 +- .../__snapshots__/e2eAnnotations.test.js.snap | 16 +++--- .../src/components/infoDrawer/infoFormat.js | 55 +++++++++++++++---- .../leftSidebar/topLeftLogoAndTitle.js | 2 - client/src/components/menubar/infoMenu.js | 2 +- client/src/components/util/truncate.js | 7 ++- 6 files changed, 61 insertions(+), 25 deletions(-) diff --git a/client/__tests__/e2e/__snapshots__/e2e.test.js.snap b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap index e7207fb7..d33b3262 100644 --- a/client/__tests__/e2e/__snapshots__/e2e.test.js.snap +++ b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`did launch page launched 1`] = `"pbmc3kc3k"`; +exports[`did launch page launched 1`] = `"pbmc3kc3k"`; -exports[`metadata loads categories and values from dataset appear 1`] = `"
louvainvain
"`; +exports[`metadata loads categories and values from dataset appear 1`] = `"
louvainvain
"`; diff --git a/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap b/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap index 67b743ce..01ed89a8 100644 --- a/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap +++ b/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap @@ -2,22 +2,22 @@ exports[`annotations stacked bar graph renders 1`] = ` Array [ - "
TEST-LABELLABEL
0
", - "
unassignedigned
2133
", + "
TEST-LABELLABEL
0
", + "
unassignedigned
2133
", ] `; exports[`annotations stacked bar graph renders 2`] = ` Array [ - "
TEST-LABELLABEL
0
", - "
unassignedigned
2638
", + "
TEST-LABELLABEL
0
", + "
unassignedigned
2638
", ] `; -exports[`annotations truncate midpoint whitespace 1`] = `"123 456 456"`; +exports[`annotations truncate midpoint whitespace 1`] = `"123 456 456"`; -exports[`annotations truncate midpoint whitespace 2`] = `"123 456 456"`; +exports[`annotations truncate midpoint whitespace 2`] = `"123 456 456"`; -exports[`annotations truncate single character 1`] = `"T"`; +exports[`annotations truncate single character 1`] = `"T"`; -exports[`annotations truncate single character 2`] = `"T"`; +exports[`annotations truncate single character 2`] = `"T"`; diff --git a/client/src/components/infoDrawer/infoFormat.js b/client/src/components/infoDrawer/infoFormat.js index 8361b6de..eacaa406 100644 --- a/client/src/components/infoDrawer/infoFormat.js +++ b/client/src/components/infoDrawer/infoFormat.js @@ -1,6 +1,8 @@ import { H3, H1, UL, Classes } from "@blueprintjs/core"; import React from "react"; +import Truncate from "../util/truncate"; + const renderContributors = (contributors, affiliations, skeleton) => { // eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII if (!contributors || contributors.length === 0 || true) return null; @@ -79,23 +81,56 @@ const renderOrganism = (organism, skeleton) => { ); }; +const ONTOLOGY_KEY = "ontology_term_id"; +const CAT_WIDTH = "30%"; +const VAL_WIDTH = "35%"; // Render list of metadata attributes found in categorical field -// Ignores categories with empty or null values const renderSingleValueCategories = (singleValueCategories, skeleton) => { if (singleValueCategories.size === 0) return null; return ( <>

Dataset Metadata

    - {Array.from(singleValueCategories).map((pair) => { - if (!pair[1] || pair[1] === "") return null; - return ( -
  • {`${pair[0]}: ${pair[1]}`}
  • - ); - })} + {Array.from(singleValueCategories).reduce((elems, pair) => { + const [category, value] = pair; + // If the value is empty skip it + if (!value) return elems; + + // If this category is a ontology term, let's add its value to the previous node + if (String(category).includes(ONTOLOGY_KEY)) { + const prevElem = elems.pop(); + // Props aren't extensible so we must clone and alter the component to append the new child + elems.push( + React.cloneElement( + prevElem, + prevElem.props, + // Concat returns a new array + prevElem.props.children.concat([ + + {value} + , + ]) + ) + ); + } else { + // Create the list item + elems.push( +
  • + + {`${category}:`} + + + {value} + +
  • + ); + } + return elems; + }, [])}
); diff --git a/client/src/components/leftSidebar/topLeftLogoAndTitle.js b/client/src/components/leftSidebar/topLeftLogoAndTitle.js index f2346593..6224c1a1 100644 --- a/client/src/components/leftSidebar/topLeftLogoAndTitle.js +++ b/client/src/components/leftSidebar/topLeftLogoAndTitle.js @@ -2,7 +2,6 @@ import React from "react"; import { connect } from "react-redux"; import { Button } from "@blueprintjs/core"; -import { IconNames } from "@blueprintjs/icons"; import * as globals from "../../globals"; import Logo from "../framework/logo"; @@ -60,7 +59,6 @@ class LeftSideBar extends React.Component { ", - "
unassignedigned
2133
", + "
unassignedigned
2133
", ] `; exports[`annotations stacked bar graph renders 2`] = ` Array [ "
TEST-LABELLABEL
0
", - "
unassignedigned
2638
", + "
unassignedigned
2638
", ] `; diff --git a/client/configuration/eslint/eslint.js b/client/configuration/eslint/eslint.js index bafb7d31..51b619db 100644 --- a/client/configuration/eslint/eslint.js +++ b/client/configuration/eslint/eslint.js @@ -4,6 +4,7 @@ module.exports = { extends: [ "airbnb", "plugin:eslint-comments/recommended", + "plugin:@blueprintjs/recommended", "plugin:compat/recommended", "plugin:prettier/recommended", "prettier/react", diff --git a/client/package-lock.json b/client/package-lock.json index c94ba503..68eb0080 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4156,6 +4156,216 @@ "tslib": "~1.10.0" } }, + "@blueprintjs/eslint-plugin": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/eslint-plugin/-/eslint-plugin-0.3.0.tgz", + "integrity": "sha512-bQEdE4ApEHxCDV8hT9uIxeRbDFKOtRLBT3/Zy3Ku+nowDAYl/8jwZKp6lJuR/nqvsfuIXTnVef6ivwdBEieQfA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^4.2.0", + "eslint": "^7.9.0" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz", + "integrity": "sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.3.0", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/typescript-estree": "4.3.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz", + "integrity": "sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "eslint": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.10.0.tgz", + "integrity": "sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + } + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "@blueprintjs/icons": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.19.0.tgz", @@ -4184,6 +4394,76 @@ "minimist": "^1.2.0" } }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -5137,6 +5417,32 @@ } } }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, "@npmcli/move-file": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", @@ -5489,6 +5795,22 @@ "eslint-utils": "^2.0.0" } }, + "@typescript-eslint/scope-manager": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz", + "integrity": "sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0" + } + }, + "@typescript-eslint/types": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.3.0.tgz", + "integrity": "sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==", + "dev": true + }, "@typescript-eslint/typescript-estree": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", @@ -5512,6 +5834,24 @@ } } }, + "@typescript-eslint/visitor-keys": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz", + "integrity": "sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -8796,6 +9136,15 @@ } } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10187,6 +10536,31 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -10202,6 +10576,15 @@ "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" }, + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "favicons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/favicons/-/favicons-5.5.0.tgz", @@ -11159,6 +11542,28 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + } + } + }, "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -11691,6 +12096,12 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "dev": true }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, "ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", @@ -14189,6 +14600,12 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -15251,6 +15668,12 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pbkdf2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", @@ -17065,6 +17488,12 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rgb-regex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", @@ -17100,6 +17529,12 @@ "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", diff --git a/client/package.json b/client/package.json index 9e6f20ef..feec7fad 100644 --- a/client/package.json +++ b/client/package.json @@ -84,6 +84,7 @@ "@babel/preset-react": "^7.10.4", "@babel/register": "^7.10.5", "@babel/runtime": "^7.10.5", + "@blueprintjs/eslint-plugin": "^0.3.0", "@sentry/webpack-plugin": "^1.12.0", "babel-eslint": "^10.1.0", "babel-jest": "^26.1.0", diff --git a/client/src/actions/embedding.js b/client/src/actions/embedding.js index 615bcd68..7b781a0d 100644 --- a/client/src/actions/embedding.js +++ b/client/src/actions/embedding.js @@ -5,20 +5,23 @@ action creators related to embeddings choice import { AnnoMatrixObsCrossfilter } from "../annoMatrix"; import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers"; -export async function _switchEmbedding(prevAnnoMatrix, prevCrossfilter, newEmbeddingName) { +export async function _switchEmbedding( + prevAnnoMatrix, + prevCrossfilter, + newEmbeddingName +) { /* DRY helper used by this and reembedding action creators */ const base = prevAnnoMatrix.base(); const embeddingDf = await base.fetch("emb", newEmbeddingName); const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf); - const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix, prevCrossfilter.obsCrossfilter).select( - "emb", - newEmbeddingName, - { - mode: "all", - } - ); + const obsCrossfilter = await new AnnoMatrixObsCrossfilter( + annoMatrix, + prevCrossfilter.obsCrossfilter + ).select("emb", newEmbeddingName, { + mode: "all", + }); return [annoMatrix, obsCrossfilter]; } @@ -30,7 +33,10 @@ export const layoutChoiceAction = (newLayoutChoice) => async ( On layout choice, make sure we have selected all on the previous layout, AND the new layout. */ - const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } = getState(); + const { + annoMatrix: prevAnnoMatrix, + obsCrossfilter: prevCrossfilter, + } = getState(); const [annoMatrix, obsCrossfilter] = await _switchEmbedding( prevAnnoMatrix, prevCrossfilter, diff --git a/client/src/components/autosave/filenameDialog.js b/client/src/components/autosave/filenameDialog.js index 3937ecee..08fe1462 100644 --- a/client/src/components/autosave/filenameDialog.js +++ b/client/src/components/autosave/filenameDialog.js @@ -3,11 +3,12 @@ import { connect } from "react-redux"; import { Button, - Tooltip, - InputGroup, - Dialog, Classes, + Code, Colors, + Dialog, + InputGroup, + Tooltip, } from "@blueprintjs/core"; @connect((state) => ({ @@ -145,9 +146,9 @@ class FilenameDialog extends React.Component {

Your annotations are stored in this file: - + {filenameText}-{idhash}.csv - +

(We added a unique ID to your filename) diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index 71175b2d..a18608b2 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -5,12 +5,13 @@ https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771 https://bl.ocks.org/mbostock/3019563 */ import React, { useEffect, useRef, useState, useCallback } from "react"; -import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core"; +import { Button, ButtonGroup, Icon, Tooltip } from "@blueprintjs/core"; import { connect } from "react-redux"; import * as d3 from "d3"; import { interpolateCool } from "d3-scale-chromatic"; import Async from "react-async"; import memoize from "memoize-one"; +import { IconNames } from "@blueprintjs/icons"; import * as globals from "../../globals"; import actions from "../../actions"; import { histogramContinuous } from "../../util/dataframe/histogram"; @@ -26,7 +27,7 @@ function maybeScientific(x) { const _ticks = x.ticks(4); if (x.domain().some((n) => Math.abs(n) >= 10000)) { - /* + /* heuristic: if the last tick d3 wants to render has one significant digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render 4.20e+n @@ -99,7 +100,7 @@ const HistogramFooter = React.memo( pvalAdj, }) => { /* - Footer of each histogram. Will render range, title, and optionally + Footer of each histogram. Will render range, title, and optionally differential expression info. Required props: @@ -214,10 +215,7 @@ const HistogramHeader = React.memo( > {onScatterPlotXClick && onScatterPlotYClick ? ( - +

} /> - + ); }); diff --git a/client/src/components/menubar/infoMenu.js b/client/src/components/menubar/infoMenu.js index d136ae0c..a97b8b63 100644 --- a/client/src/components/menubar/infoMenu.js +++ b/client/src/components/menubar/infoMenu.js @@ -1,6 +1,13 @@ // jshint esversion: 6 import React from "react"; -import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core"; +import { + Button, + ButtonGroup, + Menu, + MenuItem, + Popover, + Position, +} from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import styles from "./menubar.css"; @@ -11,7 +18,7 @@ const handleClick = (dispatch) => { const InformationMenu = React.memo((props) => { const { libraryVersions, tosURL, privacyURL, dispatch } = props; return ( -
+ @@ -64,13 +71,13 @@ const InformationMenu = React.memo((props) => { >
+ ); }); diff --git a/client/src/components/menubar/undoRedo.js b/client/src/components/menubar/undoRedo.js index 601dba32..8591505c 100644 --- a/client/src/components/menubar/undoRedo.js +++ b/client/src/components/menubar/undoRedo.js @@ -1,12 +1,13 @@ import React from "react"; -import { AnchorButton, Tooltip } from "@blueprintjs/core"; +import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; import { tooltipHoverOpenDelay } from "../../globals"; import styles from "./menubar.css"; const UndoRedo = React.memo((props) => { const { undoDisabled, redoDisabled, dispatch } = props; return ( -
+ { > { dispatch({ type: "@@undoable/undo" }); @@ -32,7 +33,7 @@ const UndoRedo = React.memo((props) => { > { dispatch({ type: "@@undoable/redo" }); @@ -43,7 +44,7 @@ const UndoRedo = React.memo((props) => { data-testid="redo" /> -
+ ); }); diff --git a/client/src/components/miniHistogram/index.js b/client/src/components/miniHistogram/index.js index eb57f832..59153d91 100644 --- a/client/src/components/miniHistogram/index.js +++ b/client/src/components/miniHistogram/index.js @@ -72,7 +72,6 @@ export default class MiniHistogram extends React.PureComponent { popoverClassName={Classes.POPOVER_CONTENT_SIZING} > Date: Tue, 29 Sep 2020 15:32:21 -0700 Subject: [PATCH 13/73] Make sure there are more than 1 values in a category before rendering it (#1871) --- client/src/components/categorical/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/categorical/index.js b/client/src/components/categorical/index.js index 99ccbfcd..5003ab8c 100644 --- a/client/src/components/categorical/index.js +++ b/client/src/components/categorical/index.js @@ -185,7 +185,9 @@ class Categories extends React.Component { {/* READ ONLY CATEGORICAL FIELDS */} {/* this is duplicative but flat, could be abstracted */} {allCategoryNames.map((catName) => - !schema.annotations.obsByName[catName].writable ? ( + !schema.annotations.obsByName[catName].writable && + (schema.annotations.obsByName[catName].categories?.length > 1 || + !schema.annotations.obsByName[catName].categories) ? ( Date: Tue, 29 Sep 2020 18:31:59 -0500 Subject: [PATCH 14/73] remove AppFeature and all references to it in the code/tests (#1893) * remove AppFeature and all references to it in the code/tests Co-authored-by: bmccandless --- server/common/config/client_config.py | 4 --- server/data_common/data_adaptor.py | 25 ------------------- .../unit/common/config/test_app_config.py | 8 +++--- server/test/unit/common/test_api.py | 1 - .../unit/common/test_writable_annotation.py | 17 ------------- .../unit/data_anndata/test_anndata_adaptor.py | 17 ------------- 6 files changed, 4 insertions(+), 68 deletions(-) diff --git a/server/common/config/client_config.py b/server/common/config/client_config.py index 9fec9042..ccddf2c1 100644 --- a/server/common/config/client_config.py +++ b/server/common/config/client_config.py @@ -17,9 +17,6 @@ def get_client_config(app_config, data_adaptor): # make sure the configuration has been checked. app_config.check_config() - # features - features = [f.todict() for f in data_adaptor.get_features(annotation)] - # display_names title = app_config.get_title(data_adaptor) about = app_config.get_about(data_adaptor) @@ -75,7 +72,6 @@ def get_client_config(app_config, data_adaptor): # gather it all together client_config = {} config = client_config["config"] = {} - config["features"] = features config["displayNames"] = display_names config["library_versions"] = library_versions config["links"] = links diff --git a/server/data_common/data_adaptor.py b/server/data_common/data_adaptor.py index 3945ea7a..36af8339 100644 --- a/server/data_common/data_adaptor.py +++ b/server/data_common/data_adaptor.py @@ -155,17 +155,6 @@ class DataAdaptor(metaclass=ABCMeta): """ pass - def get_features(self, annotations=None): - """Return list of features, to return as part of the config route""" - features = [ - AppFeature("/cluster/", method="POST", available=False), - AppFeature("/layout/obs", method="GET", available=self.get_embedding_names() is not None), - AppFeature("/layout/obs", method="PUT", available=self.dataset_config.embeddings__enable_reembedding), - AppFeature("/diffexp/", method="POST", available=self.dataset_config.diffexp__enable), - AppFeature("/annotations/obs", method="PUT", available=annotations is not None), - ] - return features - def update_parameters(self, parameters): parameters.update(self.parameters) @@ -388,17 +377,3 @@ class DataAdaptor(metaclass=ABCMeta): except RuntimeError: lastmod = None return lastmod - - -class AppFeature(object): - def __init__(self, path, available=False, method="POST", extra={}): - self.path = path - self.available = available - self.method = method - self.extra = extra - [setattr(self, key, value) for key, value in extra.items()] - - def todict(self): - d = dict(available=self.available, method=self.method, path=self.path) - d.update(self.extra) - return d diff --git a/server/test/unit/common/config/test_app_config.py b/server/test/unit/common/config/test_app_config.py index 726ff86f..2dc3273d 100644 --- a/server/test/unit/common/config/test_app_config.py +++ b/server/test/unit/common/config/test_app_config.py @@ -40,11 +40,11 @@ class AppConfigTest(ConfigTests): expected_config = yaml.load(default_config, Loader=yaml.Loader) - server_config = app_default_config['server'] - dataset_config = app_default_config['dataset'] + server_config = app_default_config["server"] + dataset_config = app_default_config["dataset"] - expected_server_config = expected_config['server'] - expected_dataset_config = expected_config['dataset'] + expected_server_config = expected_config["server"] + expected_dataset_config = expected_config["dataset"] self.assertDictEqual(app_default_config, expected_config) self.assertDictEqual(server_config, expected_server_config) diff --git a/server/test/unit/common/test_api.py b/server/test/unit/common/test_api.py index 7f524f7d..b7dca258 100644 --- a/server/test/unit/common/test_api.py +++ b/server/test/unit/common/test_api.py @@ -49,7 +49,6 @@ class EndPoints(object): result_data = result.json() self.assertIn("library_versions", result_data["config"]) self.assertEqual(result_data["config"]["displayNames"]["dataset"], "pbmc3k") - self.assertEqual(len(result_data["config"]["features"]), 5) def test_get_layout_fbs(self): endpoint = "layout/obs" diff --git a/server/test/unit/common/test_writable_annotation.py b/server/test/unit/common/test_writable_annotation.py index 9d2f5f1d..0e369e56 100644 --- a/server/test/unit/common/test_writable_annotation.py +++ b/server/test/unit/common/test_writable_annotation.py @@ -268,20 +268,3 @@ class WritableAnnotationTest(unittest.TestCase): all_col_schema["cat_B"], {"name": "cat_B", "type": "categorical", "categories": ["label_B"], "writable": True}, ) - - def test_config(self): - features = self.data.get_features(self.annotations) - - # test each for singular presence and accuracy of available flag - def check_feature(method, path, available): - feature = list( - filter(lambda f: f.method == method and f.path == path and f.available == available, features) - ) - self.assertIsNotNone(feature) - self.assertEqual(len(feature), 1) - - check_feature("POST", "/cluster/", False) - check_feature("POST", "/diffexp/", self.data.dataset_config.diffexp__enable) - check_feature("GET", "/layout/obs", True) - check_feature("PUT", "/layout/obs", self.data.dataset_config.embeddings__enable_reembedding) - check_feature("PUT", "/annotations/obs", True) diff --git a/server/test/unit/data_anndata/test_anndata_adaptor.py b/server/test/unit/data_anndata/test_anndata_adaptor.py index d4a3a778..d0df5db1 100644 --- a/server/test/unit/data_anndata/test_anndata_adaptor.py +++ b/server/test/unit/data_anndata/test_anndata_adaptor.py @@ -94,23 +94,6 @@ class AdaptorTest(unittest.TestCase): with pytest.raises(TypeError): self.data._create_schema() - def test_config(self): - features = self.data.get_features(annotations=None) - - # test each for singular presence and accuracy of available flag - def check_feature(method, path, available): - feature = list( - filter(lambda f: f.method == method and f.path == path and f.available == available, features) - ) - self.assertIsNotNone(feature) - self.assertEqual(len(feature), 1) - - check_feature("POST", "/cluster/", False) - check_feature("POST", "/diffexp/", self.data.dataset_config.diffexp__enable) - check_feature("GET", "/layout/obs", True) - check_feature("PUT", "/layout/obs", self.data.dataset_config.embeddings__enable_reembedding) - check_feature("PUT", "/annotations/obs", False) - def test_layout(self): fbs = self.data.layout_to_fbs_matrix(fields=None) layout = decode_fbs.decode_matrix_FBS(fbs) From 998fa4762d987fb3ec99b5013e74b327b732f1d9 Mon Sep 17 00:00:00 2001 From: Madison Dunitz Date: Wed, 30 Sep 2020 11:16:13 -0500 Subject: [PATCH 15/73] run black formatter on repo (#1891) * add black to lint make cmd * add black dependency to installation to push test pipeline --- .github/workflows/push_tests.yml | 3 ++- Makefile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push_tests.yml b/.github/workflows/push_tests.yml index b4cfe599..70fcf947 100644 --- a/.github/workflows/push_tests.yml +++ b/.github/workflows/push_tests.yml @@ -31,9 +31,10 @@ jobs: - name: Install dependencies run: | pip install flake8 + pip install black cd client npm install - - name: Lint with flake8 + - name: Format with black and lint with flake8 run: | make lint-server - name: Lint src with eslint diff --git a/Makefile b/Makefile index 56dccf59..ca8754f7 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ fmt-py: lint: lint-server lint-client .PHONY: lint-server -lint-server: +lint-server: fmt-py flake8 server --per-file-ignores='server/test/fixtures/dataset_config_outline.py:F821 server/test/fixtures/server_config_outline.py:F821' From 04a3c3c6b6cb9b836445ddab03537432e0044943 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Wed, 30 Sep 2020 11:45:10 -0700 Subject: [PATCH 16/73] Partial fix for 1830 (#1863) * Remove door icon from log in button * Move log in and info buttons from the top bar to in line with the cellxgene icon and dataset name * Hover over on login button should say "Log in to cellxgene" * Show email closes #1830 --- client/src/components/categorical/index.js | 2 +- .../leftSidebar/topLeftLogoAndTitle.js | 110 ++++++++++++------ client/src/components/menubar/authButtons.js | 3 +- client/src/components/menubar/index.js | 13 +-- client/src/components/menubar/infoMenu.js | 35 +++++- server/auth/auth_test.py | 1 + 6 files changed, 107 insertions(+), 57 deletions(-) diff --git a/client/src/components/categorical/index.js b/client/src/components/categorical/index.js index 5003ab8c..63db6cac 100644 --- a/client/src/components/categorical/index.js +++ b/client/src/components/categorical/index.js @@ -187,7 +187,7 @@ class Categories extends React.Component { {allCategoryNames.map((catName) => !schema.annotations.obsByName[catName].writable && (schema.annotations.obsByName[catName].categories?.length > 1 || - !schema.annotations.obsByName[catName].categories) ? ( + !schema.annotations.obsByName[catName].categories) ? ( ({ datasetTitle: state.config?.displayNames?.dataset ?? "", + auth: state.config?.authentication, + userinfo: state.userinfo, + libraryVersions: state.config?.["library_versions"], + aboutLink: state.config?.links?.["about-dataset"], + tosURL: state.config?.parameters?.["about_legal_tos"], + privacyURL: state.config?.parameters?.["about_legal_privacy"], })) class LeftSideBar extends React.Component { handleClick = () => { @@ -20,7 +28,16 @@ class LeftSideBar extends React.Component { }; render() { - const { datasetTitle } = this.props; + const { + datasetTitle, + auth, + userinfo, + libraryVersions, + aboutLink, + privacyURL, + tosURL, + dispatch, + } = this.props; return (
- - - cell +
+ - × - - gene - - - + gene + +
+
+ + + + {!userinfo.is_authenticated ? ( + + ) : null} +
); } diff --git a/client/src/components/menubar/authButtons.js b/client/src/components/menubar/authButtons.js index 9eaa3ba5..985b3da3 100644 --- a/client/src/components/menubar/authButtons.js +++ b/client/src/components/menubar/authButtons.js @@ -11,7 +11,7 @@ const Auth = React.memo((props) => { return ( @@ -19,7 +19,6 @@ const Auth = React.memo((props) => { type="button" data-testid="auth-button" disabled={false} - icon={!userinfo.is_authenticated ? "log-in" : "log-out"} href={!userinfo.is_authenticated ? auth.login : auth.logout} > {!userinfo.is_authenticated ? "Log In" : "Log Out"} diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js index 3fd840c0..b4271302 100644 --- a/client/src/components/menubar/index.js +++ b/client/src/components/menubar/index.js @@ -6,8 +6,7 @@ import * as globals from "../../globals"; import styles from "./menubar.css"; import actions from "../../actions"; import Clip from "./clip"; -import AuthButtons from "./authButtons"; -import InformationMenu from "./infoMenu"; + import Subset from "./subset"; import UndoRedoReset from "./undoRedo"; import DiffexpButtons from "./diffexpButtons"; @@ -204,7 +203,6 @@ class MenuBar extends React.PureComponent { render() { const { dispatch, - libraryVersions, disableDiffexp, undoDisabled, redoDisabled, @@ -212,17 +210,12 @@ class MenuBar extends React.PureComponent { clipPercentileMin, clipPercentileMax, graphInteractionMode, - aboutLink, showCentroidLabels, - privacyURL, - tosURL, categoricalSelection, colorAccessor, subsetPossible, subsetResetPossible, enableReembedding, - auth, - userinfo, } = this.props; const { pendingClipPercentiles } = this.state; @@ -248,10 +241,6 @@ class MenuBar extends React.PureComponent { zIndex: 3, }} > - - { @@ -16,7 +16,14 @@ const handleClick = (dispatch) => { }; const InformationMenu = React.memo((props) => { - const { libraryVersions, tosURL, privacyURL, dispatch } = props; + const { + libraryVersions, + tosURL, + privacyURL, + auth, + userinfo, + dispatch, + } = props; return ( { handleClick(dispatch)} - icon={IconNames.INFO_SIGN} + icon="info-sign" text="Dataset Overview" /> { href={privacyURL} target="_blank" text="Privacy Policy" + rel="noopener" /> ) : null} + + {auth?.["requires_client_login"] && + userinfo?.["is_authenticated"] ? ( + <> + + + + ) : null} } position={Position.BOTTOM_RIGHT} + modifiers={{ + preventOverflow: { enabled: false }, + hide: { enabled: false }, + }} > + + ); + } return ( - - + - - {!userinfo.is_authenticated ? "Log In" : "Log Out"} - - - + Log In + + ); }); diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js index b4271302..115eb6ff 100644 --- a/client/src/components/menubar/index.js +++ b/client/src/components/menubar/index.js @@ -7,6 +7,7 @@ import styles from "./menubar.css"; import actions from "../../actions"; import Clip from "./clip"; +import AuthButtons from "./authButtons"; import Subset from "./subset"; import UndoRedoReset from "./undoRedo"; import DiffexpButtons from "./diffexpButtons"; @@ -216,6 +217,8 @@ class MenuBar extends React.PureComponent { subsetPossible, subsetResetPossible, enableReembedding, + userinfo, + auth, } = this.props; const { pendingClipPercentiles } = this.state; @@ -241,6 +244,7 @@ class MenuBar extends React.PureComponent { zIndex: 3, }} > + { - dispatch({ type: "toggle dataset drawer" }); -}; - -const InformationMenu = React.memo((props) => { - const { - libraryVersions, - tosURL, - privacyURL, - auth, - userinfo, - dispatch, - } = props; - return ( - - - handleClick(dispatch)} - icon="info-sign" - text="Dataset Overview" - /> - - - - - - - {tosURL ? ( - - ) : null} - {privacyURL ? ( - - ) : null} - - {auth?.["requires_client_login"] && - userinfo?.["is_authenticated"] ? ( - <> - - - - ) : null} - - } - position={Position.BOTTOM_RIGHT} - modifiers={{ - preventOverflow: { enabled: false }, - hide: { enabled: false }, - }} - > - + + + ); +} + export default Auth; diff --git a/client/src/components/termsPrompt/index.js b/client/src/components/termsPrompt/index.js index c6209a00..9aef013c 100644 --- a/client/src/components/termsPrompt/index.js +++ b/client/src/components/termsPrompt/index.js @@ -8,26 +8,7 @@ import { Colors, Icon, } from "@blueprintjs/core"; - -const CookieDecision = "cxg.cookieDecision"; - -function storageGet(key, defaultValue = null) { - try { - const val = window.localStorage.getItem(key); - if (val === null) return defaultValue; - return val; - } catch (e) { - return defaultValue; - } -} - -function storageSet(key, value) { - try { - window.localStorage.setItem(key, value); - } catch { - // continue - } -} +import { storageGet, storageSet, KEYS } from "../util/localStorage"; @connect((state) => ({ tosURL: state.config?.parameters?.["about_legal_tos"], @@ -37,7 +18,7 @@ class TermsPrompt extends React.PureComponent { constructor(props) { super(props); const { tosURL, privacyURL } = this.props; - const cookieDecision = storageGet(CookieDecision, null); + const cookieDecision = storageGet(KEYS.COOKIE_DECISION, null); const hasDecided = cookieDecision !== null; this.state = { hasDecided, @@ -55,7 +36,7 @@ class TermsPrompt extends React.PureComponent { handleOK = () => { this.setState({ isOpen: false }); - storageSet(CookieDecision, "yes"); + storageSet(KEYS.COOKIE_DECISION, "yes"); if (window.cookieDecisionCallback instanceof Function) { try { window.cookieDecisionCallback(); @@ -67,7 +48,7 @@ class TermsPrompt extends React.PureComponent { handleNo = () => { this.setState({ isOpen: false }); - storageSet(CookieDecision, "no"); + storageSet(KEYS.COOKIE_DECISION, "no"); }; renderTos() { diff --git a/client/src/components/util/localStorage.js b/client/src/components/util/localStorage.js new file mode 100644 index 00000000..5766d9f9 --- /dev/null +++ b/client/src/components/util/localStorage.js @@ -0,0 +1,22 @@ +export const KEYS = { + COOKIE_DECISION: "cxg.cookieDecision", + LOGIN_PROMPT: "cxg.LOGIN_PROMPT", +}; + +export function storageGet(key, defaultValue = null) { + try { + const val = window.localStorage.getItem(key); + if (val === null) return defaultValue; + return val; + } catch (e) { + return defaultValue; + } +} + +export function storageSet(key, value) { + try { + window.localStorage.setItem(key, value); + } catch { + // continue + } +} From 86ff48ae36d55944aa3ba66b5025a5fba51c8183 Mon Sep 17 00:00:00 2001 From: maniarathi Date: Fri, 9 Oct 2020 10:09:32 -0700 Subject: [PATCH 32/73] Revert "Allow columns encoded in float64 to be rendered as part of continuous value histograms. (#1905)" (#1925) This reverts commit b048fd8d9a8102f479214e7b8aff85362665c88c. --- client/src/components/continuous/continuous.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/src/components/continuous/continuous.js b/client/src/components/continuous/continuous.js index f1119507..ad5c908d 100644 --- a/client/src/components/continuous/continuous.js +++ b/client/src/components/continuous/continuous.js @@ -14,12 +14,7 @@ class Continuous extends React.PureComponent { if (!schema) return null; const obsIndex = schema.annotations.obs.index; const allContinuousNames = schema.annotations.obs.columns - .filter( - (col) => - col.type === "int32" || - col.type === "float32" || - col.type === "float64" - ) + .filter((col) => col.type === "int32" || col.type === "float32") .filter((col) => col.name !== obsIndex) .filter((col) => !col.writable) // skip user annotations - they will be treated as categorical .map((col) => col.name); From beb46bf3df3f69eac17b527be7c3afe4d5015944 Mon Sep 17 00:00:00 2001 From: Severiano Badajoz Date: Fri, 9 Oct 2020 12:42:02 -0600 Subject: [PATCH 33/73] add and check system arg to state auth type in e2e test(#1924) * add and check system arg to state auth type * add tolower Co-authored-by: czimergebot <35308261+czimergebot@users.noreply.github.com> --- client/Makefile | 2 +- client/__tests__/e2e/e2e.test.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/Makefile b/client/Makefile index eb5c1a78..1a8d3a81 100644 --- a/client/Makefile +++ b/client/Makefile @@ -35,7 +35,7 @@ smoke-test: start_server_and_test \ 'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \ $(CXG_SERVER_PORT) \ - 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false' + 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE="test" npm run e2e -- --verbose false' # start an instance of cellxgene and run the end-to-end annotations tests .PHONY: smoke-test-annotations diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 2bd8ea8c..7e044c40 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -522,7 +522,12 @@ test("lasso moves after pan", async () => { expect(panCount).toBe(initialCount); }); -describe("auth buttons", () => { +const describeIfCalledByMakeFileTarget = + process.env.CXG_AUTH_TYPE?.toLowerCase() === "test" + ? describe + : describe.skip; + +describeIfCalledByMakeFileTarget("auth buttons", () => { test("login then logout", async () => { await goToPage(appUrlBase); await clickOnUntil("log-in", async () => { From 5325495123f426f0e22f4949b8eab54281b79679 Mon Sep 17 00:00:00 2001 From: maniarathi Date: Mon, 12 Oct 2020 11:20:58 -0700 Subject: [PATCH 34/73] Speed up dataset drawer rendering (#1926) --- .../src/components/infoDrawer/infoDrawer.js | 98 ++++--------------- .../src/components/infoDrawer/infoFormat.js | 88 ++++++----------- 2 files changed, 53 insertions(+), 133 deletions(-) diff --git a/client/src/components/infoDrawer/infoDrawer.js b/client/src/components/infoDrawer/infoDrawer.js index 57bfd77b..9f41ef14 100644 --- a/client/src/components/infoDrawer/infoDrawer.js +++ b/client/src/components/infoDrawer/infoDrawer.js @@ -1,13 +1,9 @@ import React, { PureComponent } from "react"; -import { connect, shallowEqual } from "react-redux"; +import { connect } from "react-redux"; import { Drawer } from "@blueprintjs/core"; -import Async from "react-async"; import InfoFormat from "./infoFormat"; -import { - selectableCategoryNames, - createCategorySummaryFromDfCol, -} from "../../util/stateManager/controlsHelpers"; +import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers"; @connect((state) => { return { @@ -20,44 +16,6 @@ import { }; }) class InfoDrawer extends PureComponent { - static watchAsync(props, prevProps) { - return !shallowEqual(props.watchProps, prevProps.watchProps); - } - - fetchAsyncProps = async (props) => { - const { schema } = props.watchProps; - const { annoMatrix } = this.props; - - const allCategoryNames = selectableCategoryNames(schema).sort(); - - const nonUserAnnoCategories = allCategoryNames.map((catName) => { - const isUserAnno = schema?.annotations?.obsByName[catName]?.writable; - if (!isUserAnno) return annoMatrix.fetch("obs", catName); - return null; - }); - const singleValueCategories = ( - await Promise.all(nonUserAnnoCategories) - ).reduce((acc, categoryData, i) => { - // Actually check to see if it is null(user anno) - if (!categoryData) return acc; - const catName = allCategoryNames[i]; - - const column = categoryData.icol(0); - const colSchema = schema.annotations.obsByName[catName]; - - const categorySummary = createCategorySummaryFromDfCol(column, colSchema); - - const { numCategoryValues } = categorySummary; - // Add to the array if the category has only one value - if (numCategoryValues === 1) { - acc.set(catName, categorySummary.allCategoryValues[0]); - } - return acc; - }, new Map()); - - return { singleValueCategories }; - }; - handleClose = () => { const { dispatch } = this.props; @@ -74,45 +32,31 @@ class InfoDrawer extends PureComponent { dataPortalProps, } = this.props; + const allCategoryNames = selectableCategoryNames(schema).sort(); + const singleValueCategories = new Map(); + + allCategoryNames.forEach((catName) => { + const isUserAnno = schema?.annotations?.obsByName[catName]?.writable; + const colSchema = schema.annotations.obsByName[catName]; + if (!isUserAnno && colSchema.categories?.length === 1) { + singleValueCategories.set(catName, colSchema.categories[0]); + } + }); + return ( - - - - - - {(error) => { - console.error(error); - return Failed to load info; - }} - - - {(asyncProps) => { - const { singleValueCategories } = asyncProps; - return ( - - ); - }} - - + ); } diff --git a/client/src/components/infoDrawer/infoFormat.js b/client/src/components/infoDrawer/infoFormat.js index eacaa406..880a5f7d 100644 --- a/client/src/components/infoDrawer/infoFormat.js +++ b/client/src/components/infoDrawer/infoFormat.js @@ -1,15 +1,15 @@ -import { H3, H1, UL, Classes } from "@blueprintjs/core"; +import { H3, H1, UL } from "@blueprintjs/core"; import React from "react"; import Truncate from "../util/truncate"; -const renderContributors = (contributors, affiliations, skeleton) => { +const renderContributors = (contributors, affiliations) => { // eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII if (!contributors || contributors.length === 0 || true) return null; return ( <> -

Contributors

-

+

Contributors

+

{contributors.map((contributor) => { const { email, name, institution } = contributor; @@ -22,7 +22,7 @@ const renderContributors = (contributors, affiliations, skeleton) => { ); })}

- {renderAffiliations(affiliations, skeleton)} + {renderAffiliations(affiliations)} ); }; @@ -39,14 +39,14 @@ const buildAffiliations = (contributors = []) => { return affiliations; }; -const renderAffiliations = (affiliations, skeleton) => { +const renderAffiliations = (affiliations) => { if (affiliations.length === 0) return null; return ( <> -

Affiliations

+

Affiliations