From e55595cc55e782dfa7a3dd88c9b093d7843d91b5 Mon Sep 17 00:00:00 2001 From: Matt Weiden <538456+mweiden@users.noreply.github.com> Date: Tue, 12 May 2020 13:01:57 -0700 Subject: [PATCH 1/5] Fix smoke tests to work with remote deployments of cellxgene (#1469) * Add test for terms of service * Add workaround for chromium CSP require-trusted-types-for error --- client/__tests__/e2e/e2e.test.js | 11 +++++++++++ client/__tests__/e2e/puppeteerUtils.js | 4 ++-- client/__tests__/e2e/testBrowser.js | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 1710da9c..226d37f1 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -29,6 +29,17 @@ describe("did launch", () => { ); expect(element).toBe(data.title); }); + + test("terms of service, if they are there", async () => { + try { + await utils.clickOn("tos-cookies-accept", { timeout: 500 }); + } catch { + console.warn("No terms of service footer detected.") + } + page.waitFor(50); // give the footer a chance to disappear + const result = await page.$("[data-testid='tos-cookies-accept']"); + expect(result).toBeNull(); + }); }); describe("metadata loads", () => { diff --git a/client/__tests__/e2e/puppeteerUtils.js b/client/__tests__/e2e/puppeteerUtils.js index c72a865b..e7374f8b 100644 --- a/client/__tests__/e2e/puppeteerUtils.js +++ b/client/__tests__/e2e/puppeteerUtils.js @@ -44,8 +44,8 @@ export const puppeteerUtils = (page) => ({ }, async clickOn(testid, options = {}) { - await this.waitByID(testid); - const click = await page.click(`[data-testid='${testid}']`, options); + await this.waitByID(testid, options); + const click = await page.click(`[data-testid='${testid}']`); await page.waitFor(50); return click; }, diff --git a/client/__tests__/e2e/testBrowser.js b/client/__tests__/e2e/testBrowser.js index 8aa57bca..2931c17f 100644 --- a/client/__tests__/e2e/testBrowser.js +++ b/client/__tests__/e2e/testBrowser.js @@ -36,6 +36,10 @@ export async function setupTestBrowser() { page.on("console", async (msg) => { // If there is a console.error but an error is not thrown, this will ensure the test fails if (msg.type() === "error") { + // TODO: chromium does not currently support the CSP directive on the + // line below, so we swallow this error. Remove this when the test + // suite uses a browser version that supports this directive. + if (msg.text() === "Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.\n") return; const errorMsgText = await Promise.all( // TODO can we do this without internal properties? msg.args().map((arg) => arg._remoteObject.description) From 730410c5e1bc2a2a412269f182fd635c4245a651 Mon Sep 17 00:00:00 2001 From: Matt Weiden <538456+mweiden@users.noreply.github.com> Date: Tue, 12 May 2020 13:19:38 -0700 Subject: [PATCH 2/5] Autoformat python to fix lint errors (#1470) * Autoformat python to fix lint errors * Fix lint errors not caught by black --- server/app/app.py | 2 +- server/common/app_config.py | 10 ++++----- server/data_anndata/anndata_adaptor.py | 10 ++++----- server/data_common/data_adaptor.py | 11 ++++----- server/data_common/matrix_loader.py | 4 ++-- server/data_cxg/cxg_adaptor.py | 4 ++-- server/eb/app.py | 6 ++--- server/eb/check_requirements.py | 12 +++------- server/test/test_api.py | 16 ++++++------- server/test/test_writable_annotation.py | 30 ++++++++++++------------- 10 files changed, 50 insertions(+), 55 deletions(-) diff --git a/server/app/app.py b/server/app/app.py index d22a6731..80aefc7f 100644 --- a/server/app/app.py +++ b/server/app/app.py @@ -105,7 +105,7 @@ def get_data_adaptor(dataset=None): raise DatasetAccessError("Invalid dataset {dataset}") if datapath is None: - return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, f"Invalid dataset NONE", loglevel=logging.INFO) + return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, "Invalid dataset NONE", loglevel=logging.INFO) cache_manager = current_app.matrix_data_cache_manager return cache_manager.data_adaptor(datapath, config) diff --git a/server/common/app_config.py b/server/common/app_config.py index b228fee2..8e01419d 100644 --- a/server/common/app_config.py +++ b/server/common/app_config.py @@ -279,13 +279,13 @@ class AppConfig(object): if self.server__csp_directives is not None: for k, v in self.server__csp_directives.items(): if not isinstance(k, str): - raise ConfigurationError(f"CSP directive names must be a string.") + raise ConfigurationError("CSP directive names must be a string.") if isinstance(v, list): for policy in v: if not isinstance(policy, str): - raise ConfigurationError(f"CSP directive value must be a string or list of strings.") + raise ConfigurationError("CSP directive value must be a string or list of strings.") elif not isinstance(v, str): - raise ConfigurationError(f"CSP directive value must be a string or list of strings.") + raise ConfigurationError("CSP directive value must be a string or list of strings.") # scripts can be string (filename) or dict (attributes). Convert string to dict. scripts = [] @@ -476,8 +476,8 @@ class AppConfig(object): with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor: if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False): context["messagefn"]( - f"CAUTION: due to the size of your dataset, " - f"running differential expression may take longer or fail." + "CAUTION: due to the size of your dataset, " + "running differential expression may take longer or fail." ) max_workers = self.diffexp__alg_cxg__max_workers diff --git a/server/data_anndata/anndata_adaptor.py b/server/data_anndata/anndata_adaptor.py index fe7bda4f..60113b6d 100644 --- a/server/data_anndata/anndata_adaptor.py +++ b/server/data_anndata/anndata_adaptor.py @@ -183,13 +183,13 @@ class AnndataAdaptor(DataAdaptor): def _validate_and_initialize(self): if anndata_version_is_pre_070() and self.config.adaptor__anndata_adaptor__backed: warnings.warn( - f"Use of --backed mode with anndata versions older than 0.7 will have serious " + "Use of --backed mode with anndata versions older than 0.7 will have serious " "performance issues. Please update to at least anndata 0.7 or later." ) # var and obs column names must be unique if not self.data.obs.columns.is_unique or not self.data.var.columns.is_unique: - raise KeyError(f"All annotation column names must be unique.") + raise KeyError("All annotation column names must be unique.") self._alias_annotation_names() self._validate_data_types() @@ -222,8 +222,8 @@ class AnndataAdaptor(DataAdaptor): X0 = self.data.X[0, 0:1] if sparse.isspmatrix(X0) and not sparse.isspmatrix_csc(X0): warnings.warn( - f"Anndata data matrix is sparse, but not a CSC (columnar) matrix. " - f"Performance may be improved by using CSC." + "Anndata data matrix is sparse, but not a CSC (columnar) matrix. " + "Performance may be improved by using CSC." ) if self.data.X.dtype != "float32": warnings.warn( @@ -295,7 +295,7 @@ class AnndataAdaptor(DataAdaptor): valid_layouts.append(layout) if len(valid_layouts) == 0: - raise PrepareError(f"No valid layout data.") + raise PrepareError("No valid layout data.") # cap layouts to MAX_LAYOUTS return layouts[0:MAX_LAYOUTS] diff --git a/server/data_common/data_adaptor.py b/server/data_common/data_adaptor.py index 7aa5d452..cd542cc0 100644 --- a/server/data_common/data_adaptor.py +++ b/server/data_common/data_adaptor.py @@ -230,18 +230,19 @@ class DataAdaptor(metaclass=ABCMeta): # all labels must have a name, which must be unique and not used in obs column names if not labels_df.columns.is_unique: - raise KeyError(f"All column names specified in user annotations must be unique.") + raise KeyError("All column names specified in user annotations must be unique.") # the label index must be unique, and must have same values the anndata obs index if not labels_df.index.is_unique: - raise KeyError(f"All row index values specified in user annotations must be unique.") + raise KeyError("All row index values specified in user annotations must be unique.") obs_columns = self.get_obs_columns() duplicate_columns = list(set(labels_df.columns) & set(obs_columns)) if len(duplicate_columns) > 0: raise KeyError( - f"Labels file may not contain column names which overlap " f"with h5ad obs columns {duplicate_columns}" + "Labels file may not contain column names which overlap " + f"with h5ad obs columns {duplicate_columns}" ) # labels must have same count as obs annotations @@ -351,13 +352,13 @@ class DataAdaptor(metaclass=ABCMeta): """ embeddings = self.get_embedding_names() if fields is None or len(fields) == 0 else fields layout_data = [] - with ServerTiming.time(f"layout.query"): + with ServerTiming.time("layout.query"): for ename in embeddings: embedding = self.get_embedding_array(ename, 2) normalized_layout = DataAdaptor.normalize_embedding(embedding) layout_data.append(pd.DataFrame(normalized_layout, columns=[f"{ename}_0", f"{ename}_1"])) - with ServerTiming.time(f"layout.encode"): + with ServerTiming.time("layout.encode"): if layout_data: df = pd.concat(layout_data, axis=1, copy=False) else: diff --git a/server/data_common/matrix_loader.py b/server/data_common/matrix_loader.py index 7ee0184a..fae95b5a 100644 --- a/server/data_common/matrix_loader.py +++ b/server/data_common/matrix_loader.py @@ -228,7 +228,7 @@ class MatrixDataLoader(object): self.matrix_data_type = self.__matrix_data_type() if not self.__matrix_data_type_allowed(app_config): - raise DatasetAccessError(f"Dataset does not have an allowed type.") + raise DatasetAccessError("Dataset does not have an allowed type.") if self.matrix_data_type == MatrixDataType.H5AD: from server.data_anndata.anndata_adaptor import AnndataAdaptor @@ -272,7 +272,7 @@ class MatrixDataLoader(object): def pre_load_validation(self): if self.matrix_data_type == MatrixDataType.UNKNOWN: - raise DatasetAccessError(f"Dataset does not have a recognized type: .h5ad or .cxg") + raise DatasetAccessError("Dataset does not have a recognized type: .h5ad or .cxg") self.matrix_type.pre_load_validation(self.location) def file_size(self): diff --git a/server/data_cxg/cxg_adaptor.py b/server/data_cxg/cxg_adaptor.py index 3bab6987..accab96a 100644 --- a/server/data_cxg/cxg_adaptor.py +++ b/server/data_cxg/cxg_adaptor.py @@ -264,7 +264,7 @@ class CxgAdaptor(DataAdaptor): # function to get the embedding # this function to iterate through embeddings. def get_embedding_names(self): - with ServerTiming.time(f"layout.lsuri"): + with ServerTiming.time("layout.lsuri"): pemb = self.get_path("emb") embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"] if len(embeddings) == 0: @@ -311,7 +311,7 @@ class CxgAdaptor(DataAdaptor): A = self.open_array(ax) schema_hints = json.loads(A.meta["cxg_schema"]) if "cxg_schema" in A.meta else {} if type(schema_hints) is not dict: - raise TypeError(f"Array schema was malformed.") + raise TypeError("Array schema was malformed.") cols = [] for attr in A.schema: diff --git a/server/eb/app.py b/server/eb/app.py index c493a9b9..2b6f3951 100644 --- a/server/eb/app.py +++ b/server/eb/app.py @@ -156,7 +156,7 @@ try: dataroot = os.getenv("CXG_DATAROOT") if dataroot: - logging.info(f"Configuration from CXG_DATAROOT") + logging.info("Configuration from CXG_DATAROOT") app_config.update(multi_dataset__dataroot=dataroot) secret_name = os.getenv("CXG_AWS_SECRET_NAME") @@ -171,7 +171,7 @@ try: if not secret_region_name: secret_region_name = discover_s3_region_name(config_file) if not secret_region_name: - logging.error(f"Could not determine the AWS Secret Manager region") + logging.error("Could not determine the AWS Secret Manager region") sys.exit(1) flask_secret_key = get_flask_secret_key(secret_region_name, secret_name) @@ -188,7 +188,7 @@ try: if not app_config.server__flask_secret_key: logging.critical( - f"flask_secret_key is not provided. Either set in config file, CXG_SECRET_KEY environment variable, " + "flask_secret_key is not provided. Either set in config file, CXG_SECRET_KEY environment variable, " "or in AWS Secret Manager" ) sys.exit(1) diff --git a/server/eb/check_requirements.py b/server/eb/check_requirements.py index 67d7ab8b..008afede 100644 --- a/server/eb/check_requirements.py +++ b/server/eb/check_requirements.py @@ -29,16 +29,12 @@ def check(expected, custom): # cdict must only have exact requirements (==) for cname, cspecs in cdict.items(): if len(cspecs) != 1 or cspecs[0][0] != "==": - print( - f"Error, spec must be an exact requirement {custom}: {cname} {str(cspecs)}" - ) + print(f"Error, spec must be an exact requirement {custom}: {cname} {str(cspecs)}") okay = False for ename, especs in edict.items(): if ename not in cdict: - print( - f"Error, missing requirement from {custom}: {ename} {str(especs)}" - ) + print(f"Error, missing requirement from {custom}: {ename} {str(especs)}") okay = False continue @@ -46,9 +42,7 @@ def check(expected, custom): for espec in especs: rokay = check_version(cver, espec[0], Version(espec[1])) if not rokay: - print( - f"Error, failed requirement from {custom}: {ename} {espec}, {cver}" - ) + print(f"Error, failed requirement from {custom}: {ename} {espec}, {cver}") okay = False if okay: diff --git a/server/test/test_api.py b/server/test/test_api.py index cd9a4329..319a5847 100644 --- a/server/test/test_api.py +++ b/server/test/test_api.py @@ -185,14 +185,14 @@ class EndPoints(object): self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST) def test_data_mimetype_error(self): - endpoint = f"data/var" + endpoint = "data/var" header = {"Accept": "xxx"} url = f"{self.URL_BASE}{endpoint}" result = self.session.put(url, headers=header) self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE) def test_fbs_default(self): - endpoint = f"data/var" + endpoint = "data/var" url = f"{self.URL_BASE}{endpoint}" result = self.session.put(url) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST) @@ -202,21 +202,21 @@ class EndPoints(object): self.assertEqual(result.headers["Content-Type"], "application/octet-stream") def test_data_put_fbs(self): - endpoint = f"data/var" + endpoint = "data/var" url = f"{self.URL_BASE}{endpoint}" header = {"Accept": "application/octet-stream"} result = self.session.put(url, headers=header) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST) def test_data_get_fbs(self): - endpoint = f"data/var" + endpoint = "data/var" url = f"{self.URL_BASE}{endpoint}" header = {"Accept": "application/octet-stream"} result = self.session.get(url, headers=header) self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST) def test_data_put_filter_fbs(self): - endpoint = f"data/var" + endpoint = "data/var" url = f"{self.URL_BASE}{endpoint}" header = {"Accept": "application/octet-stream"} filter = {"filter": {"var": {"index": [0, 1, 4]}}} @@ -233,8 +233,8 @@ class EndPoints(object): def test_data_get_filter_fbs(self): index_col_name = self.schema["schema"]["annotations"]["var"]["index"] + endpoint = "data/var" query = f"var:{index_col_name}=SIK1" - endpoint = f"data/var" url = f"{self.URL_BASE}{endpoint}?{query}" header = {"Accept": "application/octet-stream"} result = self.session.get(url, headers=header) @@ -245,7 +245,7 @@ class EndPoints(object): self.assertEqual(df["n_cols"], 1) def test_data_put_single_var(self): - endpoint = f"data/var" + endpoint = "data/var" url = f"{self.URL_BASE}{endpoint}" header = {"Accept": "application/octet-stream"} index_col_name = self.schema["schema"]["annotations"]["var"]["index"] @@ -306,7 +306,7 @@ class EndPointsAnnotations(EndPoints): query = "annotation-collection-name=test_annotations" url = f"{self.URL_BASE}{endpoint}?{query}" n_rows = self.data.get_shape()[0] - fbs = make_fbs({"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category")}) + fbs = make_fbs({"cat_A": pd.Series(["label_A"] * n_rows, dtype="category")}) result = self.session.put(url, data=fbs) self.assertEqual(result.status_code, HTTPStatus.OK) self.assertEqual(result.headers["Content-Type"], "application/json") diff --git a/server/test/test_writable_annotation.py b/server/test/test_writable_annotation.py index 693bc373..d4cf15b4 100644 --- a/server/test/test_writable_annotation.py +++ b/server/test/test_writable_annotation.py @@ -27,7 +27,7 @@ class WritableAnnotationTest(unittest.TestCase): def test_error_checks(self): # verify that the expected errors are generated n_rows = self.data.get_shape()[0] - fbs_bad = make_fbs({"louvain": pd.Series(["undefined" for l in range(0, n_rows)], dtype="category")}) + fbs_bad = make_fbs({"louvain": pd.Series(["undefined"] * n_rows, dtype="category")}) # ensure we catch attempt to overwrite non-writable data with self.assertRaises(KeyError): @@ -38,8 +38,8 @@ class WritableAnnotationTest(unittest.TestCase): n_rows = self.data.get_shape()[0] fbs = make_fbs( { - "cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"), - "cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"), + "cat_A": pd.Series(["label_A"] * n_rows, dtype="category"), + "cat_B": pd.Series(["label_B"] * n_rows, dtype="category"), } ) res = self.annotation_put_fbs(fbs) @@ -49,14 +49,14 @@ class WritableAnnotationTest(unittest.TestCase): self.assertEqual(df.shape, (n_rows, 2)) self.assertEqual(set(df.columns), {"cat_A", "cat_B"}) self.assertTrue(self.data.original_obs_index.equals(df.index)) - self.assertTrue(np.all(df["cat_A"] == ["label_A" for l in range(0, n_rows)])) - self.assertTrue(np.all(df["cat_B"] == ["label_B" for l in range(0, n_rows)])) + self.assertTrue(np.all(df["cat_A"] == ["label_A"] * n_rows)) + self.assertTrue(np.all(df["cat_B"] == ["label_B"] * n_rows)) # verify complete overwrite on second attempt, AND rotation occurs fbs = make_fbs( { - "cat_A": pd.Series(["label_A1" for l in range(0, n_rows)], dtype="category"), - "cat_C": pd.Series(["label_C" for l in range(0, n_rows)], dtype="category"), + "cat_A": pd.Series(["label_A1"] * n_rows, dtype="category"), + "cat_C": pd.Series(["label_C"] * n_rows, dtype="category"), } ) res = self.annotation_put_fbs(fbs) @@ -64,8 +64,8 @@ class WritableAnnotationTest(unittest.TestCase): self.assertTrue(path.exists(self.annotations.output_file)) df = pd.read_csv(self.annotations.output_file, index_col=0, header=0, comment="#") self.assertEqual(set(df.columns), {"cat_A", "cat_C"}) - self.assertTrue(np.all(df["cat_A"] == ["label_A1" for l in range(0, n_rows)])) - self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)])) + self.assertTrue(np.all(df["cat_A"] == ["label_A1"] * n_rows)) + self.assertTrue(np.all(df["cat_C"] == ["label_C"] * n_rows)) # rotation name, ext = path.splitext(self.annotations.output_file) @@ -79,8 +79,8 @@ class WritableAnnotationTest(unittest.TestCase): n_rows = self.data.get_shape()[0] fbs = make_fbs( { - "cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"), - "cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"), + "cat_A": pd.Series(["label_A"] * n_rows, dtype="category"), + "cat_B": pd.Series(["label_B"] * n_rows, dtype="category"), } ) for i in range(0, 11): @@ -100,8 +100,8 @@ class WritableAnnotationTest(unittest.TestCase): n_rows = self.data.get_shape()[0] fbs = make_fbs( { - "cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"), - "cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"), + "cat_A": pd.Series(["label_A"] * n_rows, dtype="category"), + "cat_B": pd.Series(["label_B"] * n_rows, dtype="category"), } ) @@ -123,8 +123,8 @@ class WritableAnnotationTest(unittest.TestCase): [obs_index_col_name, "n_genes", "percent_mito", "n_counts", "louvain", "cat_A", "cat_B"], ) col_idx = annotations["col_idx"] - self.assertEqual(annotations["columns"][col_idx.index("cat_A")], ["label_A" for l in range(0, n_rows)]) - self.assertEqual(annotations["columns"][col_idx.index("cat_B")], ["label_B" for l in range(0, n_rows)]) + self.assertEqual(annotations["columns"][col_idx.index("cat_A")], ["label_A"] * n_rows) + self.assertEqual(annotations["columns"][col_idx.index("cat_B")], ["label_B"] * n_rows) # verify the schema was updated all_col_schema = {c["name"]: c for c in schema["annotations"]["obs"]["columns"]} From e21997799c4f6e4cd6e1dbb26cf44f55ef96dd17 Mon Sep 17 00:00:00 2001 From: Matt Weiden <538456+mweiden@users.noreply.github.com> Date: Thu, 14 May 2020 10:04:28 -0700 Subject: [PATCH 3/5] Upgrade python requirements to click>=7.1.2 (#1472) 6.7 does not have the `hidden` flag used in the code. Users building the app with an older version of click within the current range specified by requirements.txt may fail. --- server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/requirements.txt b/server/requirements.txt index e538353c..61ead7b5 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,6 +1,6 @@ anndata>=0.6.20 boto3>=1.12.18 -click>=6.7 +click>=7.1.2 fastobo>=0.6.1 Flask>=1.0.2 Flask-Compress>=1.4.0 From 5fc76edf2d8760ff0811055aeb2c8f045a918902 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Tue, 19 May 2020 11:41:49 -0400 Subject: [PATCH 4/5] Factor adding genes into own component (#1480) * factor out add genes to own component * correct import --- .../src/components/geneExpression/addGenes.js | 318 ++++++++++++++++++ client/src/components/geneExpression/index.js | 297 +--------------- 2 files changed, 321 insertions(+), 294 deletions(-) create mode 100644 client/src/components/geneExpression/addGenes.js diff --git a/client/src/components/geneExpression/addGenes.js b/client/src/components/geneExpression/addGenes.js new file mode 100644 index 00000000..aeb3001e --- /dev/null +++ b/client/src/components/geneExpression/addGenes.js @@ -0,0 +1,318 @@ +// jshint esversion: 6 +/* rc slider https://www.npmjs.com/package/rc-slider */ + +import React from "react"; +import _ from "lodash"; +import fuzzysort from "fuzzysort"; +import { connect } from "react-redux"; +import { Suggest } from "@blueprintjs/select"; +import { + MenuItem, + Button, + FormGroup, + InputGroup, + ControlGroup, +} from "@blueprintjs/core"; +import * as globals from "../../globals"; +import actions from "../../actions"; +import { + postUserErrorToast, + keepAroundErrorToast, +} from "../framework/toasters"; + +import { memoize } from "../../util/dataframe/util"; + +const renderGene = (fuzzySortResult, { handleClick, modifiers }) => { + if (!modifiers.matchesPredicate) { + return null; + } + /* the fuzzysort wraps the object with other properties, like a score */ + const geneName = fuzzySortResult.target; + + return ( + + /* this fires when user clicks a menu item */ + handleClick(g) + } + text={geneName} + /> + ); +}; + +const filterGenes = (query, genes) => + /* fires on load, once, and then for each character typed into the input */ + fuzzysort.go(query, genes, { + limit: 5, + threshold: -10000, // don't return bad results + }); + +@connect((state) => { + return { + obsAnnotations: state.world?.obsAnnotations, + userDefinedGenes: state.controls.userDefinedGenes, + userDefinedGenesLoading: state.controls.userDefinedGenesLoading, + world: state.world, + colorAccessor: state.colors.colorAccessor, + differential: state.differential, + }; +}) +class AddGenes extends React.Component { + constructor(props) { + super(props); + this.state = { + bulkAdd: "", + tab: "autosuggest", + activeItem: null, + }; + } + + _genesToUpper = (listGenes) => { + // Has to be a Map to preserve index + const upperGenes = new Map(); + for (let i = 0, { length } = listGenes; i < length; i += 1) { + upperGenes.set(listGenes[i].toUpperCase(), i); + } + + return upperGenes; + }; + + // eslint-disable-next-line react/sort-comp + _memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr); + + handleBulkAddClick = () => { + const { world, dispatch, userDefinedGenes } = this.props; + const varIndexName = world.schema.annotations.var.index; + const { bulkAdd } = this.state; + + /* + test: + Apod,,, Cd74,, ,,, Foo, Bar-2,, + */ + if (bulkAdd !== "") { + const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), ""); + console.log("geneExpression genes", genes); + if (genes.length === 0) { + return keepAroundErrorToast("Must enter a gene name."); + } + const worldGenes = + world.varAnnotations?.col(varIndexName)?.asArray() || []; + + // These gene lists are unique enough where memoization is useless + const upperGenes = this._genesToUpper(genes); + const upperUserDefinedGenes = this._genesToUpper(userDefinedGenes); + + const upperWorldGenes = this._memoGenesToUpper(worldGenes); + + dispatch({ type: "bulk user defined gene start" }); + + Promise.all( + [...upperGenes.keys()].map((upperGene) => { + if (upperUserDefinedGenes.get(upperGene) !== undefined) { + return keepAroundErrorToast("That gene already exists"); + } + + const indexOfGene = upperWorldGenes.get(upperGene); + + if (indexOfGene === undefined) { + return keepAroundErrorToast( + `${ + genes[upperGenes.get(upperGene)] + } doesn't appear to be a valid gene name.` + ); + } + return dispatch( + actions.requestUserDefinedGene(worldGenes[indexOfGene]) + ); + }) + ).then( + () => dispatch({ type: "bulk user defined gene complete" }), + () => dispatch({ type: "bulk user defined gene error" }) + ); + } + + this.setState({ bulkAdd: "" }); + return undefined; + }; + + placeholderGeneNames() { + /* + return a string containing gene name suggestions for use as a user hint. + Eg., Apod, Cd74, ... + Will return a max of 3 genes, totalling 15 characters in length. + Randomly selects gene names. + + NOTE: the random selection means it will re-render constantly. + */ + const { world } = this.props; + const { varAnnotations } = world; + const varIndexName = world.schema.annotations.var.index; + const geneNames = varAnnotations.col(varIndexName).asArray(); + if (geneNames.length > 0) { + const placeholder = []; + let len = geneNames.length; + const maxGeneNameCount = 3; + const maxStrLength = 15; + len = len < maxGeneNameCount ? len : maxGeneNameCount; + for (let i = 0, strLen = 0; i < len && strLen < maxStrLength; i += 1) { + const deal = Math.floor(Math.random() * geneNames.length); + const geneName = geneNames[deal]; + placeholder.push(geneName); + strLen += geneName.length + 2; // '2' is the length of a comma and space + } + placeholder.push("..."); + return placeholder.join(", "); + } + // default - should never happen. + return "Apod, Cd74, ..."; + } + + handleClick(g) { + const { world, dispatch, userDefinedGenes } = this.props; + const varIndexName = world.schema.annotations.var.index; + if (!g) return; + const gene = g.target; + if (userDefinedGenes.indexOf(gene) !== -1) { + postUserErrorToast("That gene already exists"); + } else if (userDefinedGenes.length > globals.maxUserDefinedGenes) { + postUserErrorToast( + `That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes` + ); + } else if ( + world.varAnnotations.col(varIndexName).indexOf(gene) === undefined + ) { + postUserErrorToast("That doesn't appear to be a valid gene name."); + } else { + dispatch({ type: "single user defined gene start" }); + dispatch(actions.requestUserDefinedGene(gene)).then( + () => dispatch({ type: "single user defined gene complete" }), + () => dispatch({ type: "single user defined gene error" }) + ); + } + } + + render() { + const { world, userDefinedGenesLoading } = this.props; + const varIndexName = world?.schema?.annotations?.var?.index; + const varIndex = world?.varAnnotations?.col(varIndexName)?.asArray(); + const { tab, bulkAdd, activeItem } = this.state; + + // may still be loading! + if (!varIndex) return null; + + return ( +
+
+ + +
+ {tab === "autosuggest" ? ( + + true : () => false} + noResults={} + onItemSelect={(g) => { + /* this happens on 'enter' */ + this.handleClick(g); + }} + initialContent={} + inputProps={{ "data-testid": "gene-search" }} + inputValueRenderer={() => { + return ""; + }} + itemListPredicate={filterGenes} + onActiveItemChange={(item) => this.setState({ activeItem: item })} + itemRenderer={renderGene.bind(this)} + items={varIndex || ["No genes"]} + popoverProps={{ minimal: true }} + /> + + + ) : null} + {tab === "bulkadd" ? ( +
+
{ + e.preventDefault(); + this.handleBulkAddClick(); + }} + > + + + { + this.setState({ bulkAdd: e.target.value }); + }} + id="text-input-bulk-add" + data-testid="input-bulk-add" + placeholder={this.placeholderGeneNames()} + value={bulkAdd} + /> + + + +
+
+ ) : null} +
+ ); + } +} + +export default AddGenes; diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js index 16a91184..a7f7a058 100644 --- a/client/src/components/geneExpression/index.js +++ b/client/src/components/geneExpression/index.js @@ -3,58 +3,10 @@ import React from "react"; import _ from "lodash"; -import fuzzysort from "fuzzysort"; - import { connect } from "react-redux"; -import { - MenuItem, - Button, - FormGroup, - InputGroup, - ControlGroup, -} from "@blueprintjs/core"; -import { Suggest } from "@blueprintjs/select"; import HistogramBrush from "../brushableHistogram"; import * as globals from "../../globals"; -import actions from "../../actions"; -import { - postUserErrorToast, - keepAroundErrorToast, -} from "../framework/toasters"; - -import { memoize } from "../../util/dataframe/util"; - -const renderGene = (fuzzySortResult, { handleClick, modifiers }) => { - if (!modifiers.matchesPredicate) { - return null; - } - /* the fuzzysort wraps the object with other properties, like a score */ - const geneName = fuzzySortResult.target; - - return ( - - /* this fires when user clicks a menu item */ - handleClick(g) - } - text={geneName} - /> - ); -}; - -const filterGenes = (query, genes) => - /* fires on load, once, and then for each character typed into the input */ - fuzzysort.go(query, genes, { - limit: 5, - threshold: -10000, // don't return bad results - }); +import AddGenes from "./addGenes"; @connect((state) => { return { @@ -67,148 +19,10 @@ const filterGenes = (query, genes) => }; }) class GeneExpression extends React.Component { - constructor(props) { - super(props); - this.state = { - bulkAdd: "", - tab: "autosuggest", - activeItem: null, - }; - } - - _genesToUpper = (listGenes) => { - // Has to be a Map to preserve index - const upperGenes = new Map(); - for (let i = 0, { length } = listGenes; i < length; i += 1) { - upperGenes.set(listGenes[i].toUpperCase(), i); - } - - return upperGenes; - }; - - // eslint-disable-next-line react/sort-comp - _memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr); - - handleBulkAddClick = () => { - const { world, dispatch, userDefinedGenes } = this.props; - const varIndexName = world.schema.annotations.var.index; - const { bulkAdd } = this.state; - - /* - test: - Apod,,, Cd74,, ,,, Foo, Bar-2,, - */ - if (bulkAdd !== "") { - const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), ""); - if (genes.length === 0) { - return keepAroundErrorToast("Must enter a gene name."); - } - const worldGenes = - world.varAnnotations?.col(varIndexName)?.asArray() || []; - - // These gene lists are unique enough where memoization is useless - const upperGenes = this._genesToUpper(genes); - const upperUserDefinedGenes = this._genesToUpper(userDefinedGenes); - - const upperWorldGenes = this._memoGenesToUpper(worldGenes); - - dispatch({ type: "bulk user defined gene start" }); - - Promise.all( - [...upperGenes.keys()].map((upperGene) => { - if (upperUserDefinedGenes.get(upperGene) !== undefined) { - return keepAroundErrorToast("That gene already exists"); - } - - const indexOfGene = upperWorldGenes.get(upperGene); - - if (indexOfGene === undefined) { - return keepAroundErrorToast( - `${ - genes[upperGenes.get(upperGene)] - } doesn't appear to be a valid gene name.` - ); - } - return dispatch( - actions.requestUserDefinedGene(worldGenes[indexOfGene]) - ); - }) - ).then( - () => dispatch({ type: "bulk user defined gene complete" }), - () => dispatch({ type: "bulk user defined gene error" }) - ); - } - - this.setState({ bulkAdd: "" }); - return undefined; - }; - - placeholderGeneNames() { - /* - return a string containing gene name suggestions for use as a user hint. - Eg., Apod, Cd74, ... - Will return a max of 3 genes, totalling 15 characters in length. - Randomly selects gene names. - - NOTE: the random selection means it will re-render constantly. - */ - const { world } = this.props; - const { varAnnotations } = world; - const varIndexName = world.schema.annotations.var.index; - const geneNames = varAnnotations.col(varIndexName).asArray(); - if (geneNames.length > 0) { - const placeholder = []; - let len = geneNames.length; - const maxGeneNameCount = 3; - const maxStrLength = 15; - len = len < maxGeneNameCount ? len : maxGeneNameCount; - for (let i = 0, strLen = 0; i < len && strLen < maxStrLength; i += 1) { - const deal = Math.floor(Math.random() * geneNames.length); - const geneName = geneNames[deal]; - placeholder.push(geneName); - strLen += geneName.length + 2; // '2' is the length of a comma and space - } - placeholder.push("..."); - return placeholder.join(", "); - } - // default - should never happen. - return "Apod, Cd74, ..."; - } - - handleClick(g) { - const { world, dispatch, userDefinedGenes } = this.props; - const varIndexName = world.schema.annotations.var.index; - if (!g) return; - const gene = g.target; - if (userDefinedGenes.indexOf(gene) !== -1) { - postUserErrorToast("That gene already exists"); - } else if (userDefinedGenes.length > globals.maxUserDefinedGenes) { - postUserErrorToast( - `That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes` - ); - } else if ( - world.varAnnotations.col(varIndexName).indexOf(gene) === undefined - ) { - postUserErrorToast("That doesn't appear to be a valid gene name."); - } else { - dispatch({ type: "single user defined gene start" }); - dispatch(actions.requestUserDefinedGene(gene)).then( - () => dispatch({ type: "single user defined gene complete" }), - () => dispatch({ type: "single user defined gene error" }) - ); - } - } - render() { - const { - world, - userDefinedGenes, - userDefinedGenesLoading, - differential, - } = this.props; + const { world, userDefinedGenes, differential } = this.props; const varIndexName = world?.schema?.annotations?.var?.index; const varIndex = world?.varAnnotations?.col(varIndexName)?.asArray(); - const { tab, bulkAdd, activeItem } = this.state; // may still be loading! if (!varIndex) return null; @@ -220,112 +34,7 @@ class GeneExpression extends React.Component { }} >
-
- - -
- - {tab === "autosuggest" ? ( - - true : () => false - } - noResults={} - onItemSelect={(g) => { - /* this happens on 'enter' */ - this.handleClick(g); - }} - initialContent={} - inputProps={{ "data-testid": "gene-search" }} - inputValueRenderer={() => { - return ""; - }} - itemListPredicate={filterGenes} - onActiveItemChange={(item) => - this.setState({ activeItem: item }) - } - itemRenderer={renderGene.bind(this)} - items={varIndex || ["No genes"]} - popoverProps={{ minimal: true }} - /> - - - ) : null} - {tab === "bulkadd" ? ( -
-
{ - e.preventDefault(); - this.handleBulkAddClick(); - }} - > - - - { - this.setState({ bulkAdd: e.target.value }); - }} - id="text-input-bulk-add" - data-testid="input-bulk-add" - placeholder={this.placeholderGeneNames()} - value={bulkAdd} - /> - - - -
-
- ) : null} + {world && userDefinedGenes.length > 0 ? _.map(userDefinedGenes, (geneName, index) => { const values = world.varData.col(geneName); From c34a68304ec1c59d8adb55ff12403a020c1bf8d3 Mon Sep 17 00:00:00 2001 From: Severiano Badajoz Date: Tue, 19 May 2020 12:38:29 -0700 Subject: [PATCH 5/5] remove all linting errors on client/src (#1463) * run eslint --fix * camelcase * camelCase config part 1 * part 2 * part 3 - removing subscripts * fix "class-methods-use-this" * fix "class-methods-use-this" * fix eslint ignores * add eslint ignore for set state in update * reformat comments to appease eslint * add a11y features * sort-comp fix * a11y fix * add ignore for set state in update * add a11y htmlFor * remove unused toast * remove unnecessary bind * add ignore for set state in update * add rel="noopener noreferrer" Using target="_blank" without rel="noopener noreferrer" is a security risk: see https://mathiasbynens.github.io/rel-noopener * use arrow function to bind * remove unused definitions/declarations * prettier * remove unused state * add comments to empty catch blocks remove curly brackets * escape ' * use eqeqeq * switch from default export * remove ignore log * remove static * fix import * revert subscripting config * clean-up * remove unnecessary subscript * fix new errors from master * change category click handler to a class property * fix camelcase changes that slipped by * unused import * Fix newly introduced ESLint errors from addGenes --- client/__tests__/e2e/config.js | 6 +- client/__tests__/e2e/e2e.test.js | 13 +- client/__tests__/e2e/e2eAnnotations.test.js | 5 +- client/__tests__/e2e/feature.test.js | 8 +- client/__tests__/util/promiseLimit.test.js | 2 +- client/configuration/webpack/cspHashPlugin.js | 2 +- client/src/actions/index.js | 2 +- .../src/components/autosave/filenameDialog.js | 2 +- client/src/components/autosave/index.js | 3 +- .../components/categorical/category/index.js | 57 ++++--- client/src/components/categorical/index.js | 2 +- .../src/components/categorical/labelInput.js | 6 +- .../src/components/categorical/value/index.js | 11 +- client/src/components/framework/toasters.js | 5 - .../src/components/geneExpression/addGenes.js | 2 +- client/src/components/graph/graph.js | 53 +++--- .../graph/overlays/centroidLabels.js | 7 +- .../graph/overlays/graphOverlayLayer.js | 6 +- .../leftSidebar/topLeftLogoAndTitle.js | 4 +- .../src/components/menubar/cellSetButtons.js | 4 +- .../src/components/menubar/diffexpButtons.js | 27 +-- client/src/components/menubar/embedding.js | 4 +- client/src/components/menubar/index.js | 155 +++++++++--------- .../src/components/scatterplot/scatterplot.js | 28 ++-- client/src/components/termsPrompt/index.js | 29 ++-- client/src/index.js | 1 - client/src/reducers/colors.js | 2 +- client/src/reducers/layoutChoice.js | 2 +- client/src/reducers/ontology.js | 9 +- client/src/reducers/world.js | 6 +- client/src/util/dataframe/labelIndex.js | 10 +- client/src/util/promiseLimit.js | 2 +- client/src/util/significantDigits.js | 2 +- .../src/util/typedCrossfilter/crossfilter.js | 9 +- 34 files changed, 248 insertions(+), 238 deletions(-) diff --git a/client/__tests__/e2e/config.js b/client/__tests__/e2e/config.js index d4748edd..80078f8b 100644 --- a/client/__tests__/e2e/config.js +++ b/client/__tests__/e2e/config.js @@ -1,9 +1,9 @@ -export const jest_env = process.env.JEST_ENV; +export const jestEnv = process.env.JEST_ENV; export const appPort = process.env.CXG_SERVER_PORT; export const appUrlBase = process.env.CXG_URL_BASE || `http://localhost:${appPort}`; -export const DEV = jest_env === "dev"; -export const DEBUG = jest_env === "debug"; +export const DEV = jestEnv === "dev"; +export const DEBUG = jestEnv === "debug"; export const DATASET = "pbmc3k"; if (DEBUG) jest.setTimeout(100000); diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 226d37f1..76e1b4d0 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -7,7 +7,10 @@ import { appUrlBase, DATASET } from "./config"; import { setupTestBrowser } from "./testBrowser"; import { datasets } from "./data"; -let browser, page, utils, cxgActions; +let browser; +let page; +let utils; +let cxgActions; const data = datasets[DATASET]; beforeAll(async () => { @@ -34,9 +37,9 @@ describe("did launch", () => { try { await utils.clickOn("tos-cookies-accept", { timeout: 500 }); } catch { - console.warn("No terms of service footer detected.") + console.warn("No terms of service footer detected."); } - page.waitFor(50); // give the footer a chance to disappear + page.waitFor(50); // give the footer a chance to disappear const result = await page.$("[data-testid='tos-cookies-accept']"); expect(result).toBeNull(); }); @@ -130,7 +133,7 @@ describe("gene entry", () => { testGenes ); expect(allHistograms).toEqual(expect.arrayContaining(testGenes)); - expect(allHistograms.length).toEqual(testGenes.length); + expect(allHistograms).toHaveLength(testGenes.length); }); }); @@ -144,7 +147,7 @@ describe("differential expression", () => { expect(allHistograms).toEqual( expect.arrayContaining(data.diffexp["gene-results"]) ); - expect(allHistograms.length).toEqual(data.diffexp["gene-results"].length); + expect(allHistograms).toHaveLength(data.diffexp["gene-results"].length); }); }); diff --git a/client/__tests__/e2e/e2eAnnotations.test.js b/client/__tests__/e2e/e2eAnnotations.test.js index 6da0c42e..f0041529 100644 --- a/client/__tests__/e2e/e2eAnnotations.test.js +++ b/client/__tests__/e2e/e2eAnnotations.test.js @@ -5,7 +5,10 @@ import { appUrlBase, DATASET } from "./config"; import { setupTestBrowser } from "./testBrowser"; import { datasets } from "./data"; -let browser, page, utils, actions; +let browser; +let page; +let utils; +let actions; const data = datasets[DATASET]; beforeAll(async () => { diff --git a/client/__tests__/e2e/feature.test.js b/client/__tests__/e2e/feature.test.js index 0fed000a..9e7adc11 100644 --- a/client/__tests__/e2e/feature.test.js +++ b/client/__tests__/e2e/feature.test.js @@ -12,9 +12,13 @@ import { appUrlBase, DEBUG, DEV, DATASET } from "./config"; import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils"; import { datasets } from "./data"; -let browser, page, utils, cxgActions, spy; +let browser; +let page; +let utils; +let cxgActions; +let spy; const browserViewport = { width: 1280, height: 960 }; -let data = datasets[DATASET].features; +const data = datasets[DATASET].features; if (DEBUG) jest.setTimeout(100000); if (DEV) jest.setTimeout(10000); diff --git a/client/__tests__/util/promiseLimit.test.js b/client/__tests__/util/promiseLimit.test.js index fc6832f6..659e2f23 100644 --- a/client/__tests__/util/promiseLimit.test.js +++ b/client/__tests__/util/promiseLimit.test.js @@ -1,4 +1,4 @@ -import { PromiseLimit } from "../../src/util/promiseLimit"; +import PromiseLimit from "../../src/util/promiseLimit"; import { range } from "../../src/util/range"; const delay = (t) => new Promise((resolve, reject) => setTimeout(resolve, t)); diff --git a/client/configuration/webpack/cspHashPlugin.js b/client/configuration/webpack/cspHashPlugin.js index d56d2b32..99afd46f 100644 --- a/client/configuration/webpack/cspHashPlugin.js +++ b/client/configuration/webpack/cspHashPlugin.js @@ -54,7 +54,7 @@ class CspHashPlugin { .createHash("sha256") .update(str, "utf8") .digest("base64"); - return "sha256-" + hash; + return `sha256-${hash}`; } } diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 19ff498d..0bfffd51 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -7,7 +7,7 @@ import { doBinaryRequest, dispatchNetworkErrorMessageToUser, } from "../util/actionHelpers"; -import { PromiseLimit } from "../util/promiseLimit"; +import PromiseLimit from "../util/promiseLimit"; import { requestReembed, reembedResetWorldToUniverse } from "./reembed"; import { loadUserColorConfig } from "../util/stateManager/colorHelpers"; diff --git a/client/src/components/autosave/filenameDialog.js b/client/src/components/autosave/filenameDialog.js index e25792d1..c2f02e64 100644 --- a/client/src/components/autosave/filenameDialog.js +++ b/client/src/components/autosave/filenameDialog.js @@ -18,7 +18,7 @@ import { saveInProgress: state.autosave?.saveInProgress ?? false, lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations, error: state.autosave?.error, - writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false, + writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, })) class FilenameDialog extends React.Component { constructor(props) { diff --git a/client/src/components/autosave/index.js b/client/src/components/autosave/index.js index bffd1740..d77dcc65 100644 --- a/client/src/components/autosave/index.js +++ b/client/src/components/autosave/index.js @@ -1,6 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import * as globals from "../../globals"; import actions from "../../actions"; import FilenameDialog from "./filenameDialog"; @@ -11,7 +10,7 @@ import FilenameDialog from "./filenameDialog"; saveInProgress: state.autosave?.saveInProgress ?? false, lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations, error: state.autosave?.error, - writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false, + writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, initialDataLoadComplete: state.autosave?.initialDataLoadComplete, })) class Autosave extends React.Component { diff --git a/client/src/components/categorical/category/index.js b/client/src/components/categorical/category/index.js index e3c6b239..dfd5e296 100644 --- a/client/src/components/categorical/category/index.js +++ b/client/src/components/categorical/category/index.js @@ -64,7 +64,7 @@ class Category extends React.Component { } else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) { /* to be explicit... */ this.checkbox.indeterminate = true; - this.setState({ isChecked: false }); + this.setState({ isChecked: false }); // eslint-disable-line react/no-did-update-set-state } } } @@ -77,14 +77,15 @@ class Category extends React.Component { }); }; - toggleAll() { - const { dispatch, metadataField } = this.props; - dispatch({ - type: "categorical metadata filter all of these", - metadataField, - }); - this.setState({ isChecked: true }); - } + handleCategoryClick = () => { + const { annotations, metadataField, onExpansionChange } = this.props; + const editingCategory = + annotations.isEditingCategoryName && + annotations.categoryBeingEdited === metadataField; + if (!editingCategory) { + onExpansionChange(metadataField); + } + }; toggleNone() { const { dispatch, metadataField } = this.props; @@ -95,6 +96,15 @@ class Category extends React.Component { this.setState({ isChecked: false }); } + toggleAll() { + const { dispatch, metadataField } = this.props; + dispatch({ + type: "categorical metadata filter all of these", + metadataField, + }); + this.setState({ isChecked: true }); + } + handleToggleAllClick() { const { isChecked } = this.state; // || this.checkbox.indeterminate === false @@ -115,6 +125,8 @@ class Category extends React.Component { globals.categoryDisplayStringMaxLength ); + const checkboxID = `category-select-${metadataField}`; + return (
-
diff --git a/client/src/index.js b/client/src/index.js index b4655afa..7f2de221 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,5 +1,4 @@ // jshint esversion: 6 -/* eslint-disable no-console */ import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; diff --git a/client/src/reducers/colors.js b/client/src/reducers/colors.js index aa10d077..a7c566fe 100644 --- a/client/src/reducers/colors.js +++ b/client/src/reducers/colors.js @@ -150,7 +150,7 @@ const ColorsReducer = ( case "annotation: delete label": { const { world } = nextSharedState; const { colorMode, colorAccessor } = state; - const { metadataField, colors } = action; + const { metadataField } = action; if ( colorMode !== "color by categorical metadata" || colorAccessor !== metadataField diff --git a/client/src/reducers/layoutChoice.js b/client/src/reducers/layoutChoice.js index 12486bc0..fa04f6f7 100644 --- a/client/src/reducers/layoutChoice.js +++ b/client/src/reducers/layoutChoice.js @@ -49,7 +49,7 @@ const LayoutChoice = ( } case "reembed: add reembedding": { - const name = action.schema.name; + const { name } = action.schema; const available = Array.from(new Set(state.available).add(name)); return { ...state, diff --git a/client/src/reducers/ontology.js b/client/src/reducers/ontology.js index 504d26ff..828bc88a 100644 --- a/client/src/reducers/ontology.js +++ b/client/src/reducers/ontology.js @@ -9,11 +9,12 @@ const Ontology = ( ) => { switch (action.type) { case "configuration load complete": { - /* eslint-disable camelcase */ const enabled = - action.config?.parameters?.annotations_cell_ontology_enabled ?? false; - const terms = action.config?.parameters?.annotations_cell_ontology_terms; - /* eslint-enable camelcase */ + action.config?.parameters?.["annotations_cell_ontology_enabled"] ?? + false; + const terms = + action.config?.parameters?.["annotations_cell_ontology_terms"]; + const termSet = new Set(terms); return { ...state, diff --git a/client/src/reducers/world.js b/client/src/reducers/world.js index 0e9685ad..6a62df26 100644 --- a/client/src/reducers/world.js +++ b/client/src/reducers/world.js @@ -41,8 +41,8 @@ const WorldReducer = ( const { dim } = action; // we don't clip anything except for varData and obsAnnotations - let unclipped = state.unclipped; - if (dim == "varData" || dim == "obsAnnotations") { + let { unclipped } = state; + if (dim === "varData" || dim === "obsAnnotations") { unclipped = { ...unclipped, [dim]: universe[dim].clone(), @@ -298,7 +298,7 @@ const WorldReducer = ( const { obsLayout: origObsLayout, schema: origSchema } = state; const { embedding, schema: embeddingSchema } = action; - const { dims, name } = embeddingSchema; + const { dims } = embeddingSchema; let obsLayout = origObsLayout; let schema = origSchema; diff --git a/client/src/util/dataframe/labelIndex.js b/client/src/util/dataframe/labelIndex.js index 0b01adda..79f69802 100644 --- a/client/src/util/dataframe/labelIndex.js +++ b/client/src/util/dataframe/labelIndex.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ /** Label indexing - map a label to & from an integer offset. See Dataframe for how this is used. @@ -23,7 +24,6 @@ function extent(tarr) { return [min, max]; } -/* eslint-disable class-methods-use-this */ class IdentityInt32Index { /* identity/noop index, with small assumptions that labels are int32 @@ -41,11 +41,13 @@ class IdentityInt32Index { return k; } + // eslint-disable-next-line class-methods-use-this getOffset(i) { // label to offset return i; } + // eslint-disable-next-line class-methods-use-this getLabel(i) { // offset to label return i; @@ -93,9 +95,6 @@ class IdentityInt32Index { return this.__promote(labelArray); } } -/* eslint-enable class-methods-use-this */ - -/* eslint-disable class-methods-use-this */ class DenseInt32Index { /* DenseInt32Index indexes integer labels, and uses Int32Array typed arrays @@ -177,9 +176,7 @@ class DenseInt32Index { return this.__promote(labelArray); } } -/* eslint-enable class-methods-use-this */ -/* eslint-disable class-methods-use-this */ class KeyIndex { /* KeyIndex indexes arbitrary JS primitive types, and uses a Map() @@ -223,6 +220,7 @@ class KeyIndex { return this.rindex.length; } + // eslint-disable-next-line class-methods-use-this subsetLabels(labelArray) { return new KeyIndex(labelArray); } diff --git a/client/src/util/promiseLimit.js b/client/src/util/promiseLimit.js index ad502fbc..1b1731b3 100644 --- a/client/src/util/promiseLimit.js +++ b/client/src/util/promiseLimit.js @@ -13,7 +13,7 @@ return Promise.all([ plimit.add(() => fetch('/baz')) ]) */ -export class PromiseLimit { +export default class PromiseLimit { constructor(maxConcurrency) { this.queue = new Set(); this.maxConcurrency = maxConcurrency; diff --git a/client/src/util/significantDigits.js b/client/src/util/significantDigits.js index 6d3e005e..21fbf83f 100644 --- a/client/src/util/significantDigits.js +++ b/client/src/util/significantDigits.js @@ -5,6 +5,6 @@ export default (n) => { return n .toExponential() - .replace(/e[\+\-0-9]*$/, "") + .replace(/e[+\-0-9]*$/, "") .replace(/^0\.?0*|\./, "").length; }; diff --git a/client/src/util/typedCrossfilter/crossfilter.js b/client/src/util/typedCrossfilter/crossfilter.js index f328d3a8..3ee99386 100644 --- a/client/src/util/typedCrossfilter/crossfilter.js +++ b/client/src/util/typedCrossfilter/crossfilter.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line max-classes-per-file import PositiveIntervals from "./positiveIntervals"; import BitArray from "./bitArray"; import { @@ -409,7 +410,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension { this.index = makeSortIndex(array); } - /* eslint-disable class-methods-use-this */ + // eslint-disable-next-line class-methods-use-this _createValueArray(data, mapf, array) { // create dimension value array const len = data.length; @@ -419,7 +420,6 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension { } return larray; } - /* eslint-enable class-methods-use-this */ select(spec) { const { mode } = spec; @@ -466,7 +466,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension { const ranges = []; const r = [ lowerBoundIndirect(value, index, lo, 0, value.length), - !!inclusive + inclusive ? upperBoundIndirect(value, index, hi, 0, value.length) : lowerBoundIndirect(value, index, hi, 0, value.length), ]; @@ -514,11 +514,10 @@ class ImmutableEnumDimension extends ImmutableScalarDimension { }); } - /* eslint-disable class-methods-use-this */ + // eslint-disable-next-line class-methods-use-this selectRange() { throw new Error("range selection unsupported on Enumerated dimension"); } - /* eslint-enable class-methods-use-this */ } class ImmutableSpatialDimension extends _ImmutableBaseDimension {