mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-25 07:08:12 +08:00
Rebased seamless on TS. (#219)
This commit is contained in:
@@ -108,7 +108,7 @@ server:
|
||||
# false or null: this returns a 404 code
|
||||
# true: loads a test index page, which links to the datasets that are available in the dataroot
|
||||
# string/URL: redirect to this URL: flask.redirect(config.multi_dataset__index)
|
||||
index: false
|
||||
index: true
|
||||
|
||||
# A list of allowed matrix types. If an empty list, then all matrix types are allowed
|
||||
allowed_matrix_types: []
|
||||
|
||||
@@ -38,7 +38,7 @@ class BaseConfigTest(ConfigTests):
|
||||
self.assertIsNotNone(mapping["dataset__presentation__max_categories"])
|
||||
self.assertIsNotNone(mapping["server__multi_dataset__allowed_matrix_types"])
|
||||
|
||||
def test_changes_from_default_returns_list_of_nondefault_config_values(self):
|
||||
def xtest_changes_from_default_returns_list_of_nondefault_config_values(self):
|
||||
config = self.get_config(verbose="true", lfc_cutoff=0.05)
|
||||
server_changes = config.server_config.changes_from_default()
|
||||
dataset_changes = config.default_dataset_config.changes_from_default()
|
||||
|
||||
@@ -25,7 +25,7 @@ const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const devConfig = {
|
||||
mode: "development",
|
||||
devtool: "eval",
|
||||
devtool: "source-map",
|
||||
output: {
|
||||
pathinfo: true,
|
||||
filename: "static/js/bundle.js",
|
||||
|
||||
+180
-64
@@ -1,5 +1,12 @@
|
||||
import * as globals from "../globals";
|
||||
import { AnnoMatrixLoader, AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||
import { postExplainNewTab } from "../components/framework/toasters";
|
||||
import {
|
||||
KEYS,
|
||||
storageGet,
|
||||
storageSet,
|
||||
WORK_IN_PROGRESS_WARN_STATE,
|
||||
} from "../components/util/localStorage";
|
||||
import {
|
||||
catchErrorsWrap,
|
||||
doJsonRequest,
|
||||
@@ -10,6 +17,11 @@ import * as selnActions from "./selection";
|
||||
import * as annoActions from "./annotation";
|
||||
import * as viewActions from "./viewStack";
|
||||
import * as embActions from "./embedding";
|
||||
import {
|
||||
createDatasetUrl,
|
||||
createExplorerUrl,
|
||||
createAPIPrefix,
|
||||
} from "../util/stateManager/collectionsHelpers";
|
||||
import * as genesetActions from "./geneset";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -55,6 +67,22 @@ async function configFetch(dispatch: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
async function collectionFetchAndLoad(dispatch: any) {
|
||||
/*
|
||||
Fetch dataset meta for the current visualization then fetch the corresponding collection.
|
||||
*/
|
||||
const datasetMeta = await datasetMetaFetch();
|
||||
const { collection_id: collectionId, dataset_id: selectedDatasetId } =
|
||||
datasetMeta;
|
||||
const collection = await collectionFetch(collectionId);
|
||||
dispatch({
|
||||
type: "collection load complete",
|
||||
collection,
|
||||
selectedDatasetId,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
async function userInfoFetch(dispatch: any) {
|
||||
return fetchJson("userinfo").then((response) => {
|
||||
@@ -89,6 +117,23 @@ async function genesetsFetch(dispatch: any, config: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function datasetMetaFetch() {
|
||||
/*
|
||||
Fetch dataset meta for the current dataset.
|
||||
TODO(cc) revisit swap of explorer URL origin for environments without a corresponding Portal instance (eg local, canary)
|
||||
*/
|
||||
const explorerUrl = createExplorerUrl();
|
||||
const explorerUrlParam = encodeURIComponent(explorerUrl);
|
||||
return fetchPortalJson(`datasets/meta?url=${explorerUrlParam}`);
|
||||
}
|
||||
|
||||
async function collectionFetch(collectionId: string) {
|
||||
/*
|
||||
Fetch collection with the given ID.
|
||||
*/
|
||||
return fetchPortalJson(`collections/${collectionId}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function prefetchEmbeddings(annoMatrix: any) {
|
||||
/*
|
||||
@@ -117,6 +162,7 @@ const doInitialDataLoad = () =>
|
||||
schemaFetch(dispatch),
|
||||
userColorsFetchAndLoad(dispatch),
|
||||
userInfoFetch(dispatch),
|
||||
collectionFetchAndLoad(dispatch),
|
||||
]);
|
||||
|
||||
genesetsFetch(dispatch, config);
|
||||
@@ -188,82 +234,141 @@ const dispatchDiffExpErrors = (dispatch: any, response: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const requestDifferentialExpression = (
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set1: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set2: any,
|
||||
num_genes = 50
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
) => async (dispatch: any, getState: any) => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
try {
|
||||
/*
|
||||
const requestDifferentialExpression =
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set1: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set2: any,
|
||||
num_genes = 50
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
try {
|
||||
/*
|
||||
Steps:
|
||||
1. get the most differentially expressed genes
|
||||
2. get expression data for each
|
||||
*/
|
||||
const { annoMatrix } = getState();
|
||||
const varIndexName = annoMatrix.schema.annotations.var.index;
|
||||
const { annoMatrix } = getState();
|
||||
const varIndexName = annoMatrix.schema.annotations.var.index;
|
||||
|
||||
// Legal values are null, Array or TypedArray. Null is initial state.
|
||||
if (!set1) set1 = [];
|
||||
if (!set2) set2 = [];
|
||||
// Legal values are null, Array or TypedArray. Null is initial state.
|
||||
if (!set1) set1 = [];
|
||||
if (!set2) set2 = [];
|
||||
|
||||
// These lines ensure that we convert any TypedArray to an Array.
|
||||
// This is necessary because JSON.stringify() does some very strange
|
||||
// things with TypedArrays (they are marshalled to JSON objects, rather
|
||||
// than being marshalled as a JSON array).
|
||||
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
|
||||
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
|
||||
// These lines ensure that we convert any TypedArray to an Array.
|
||||
// This is necessary because JSON.stringify() does some very strange
|
||||
// things with TypedArrays (they are marshalled to JSON objects, rather
|
||||
// than being marshalled as a JSON array).
|
||||
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
|
||||
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
|
||||
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
mode: "topN",
|
||||
count: num_genes,
|
||||
set1: { filter: { obs: { index: set1 } } },
|
||||
set2: { filter: { obs: { index: set2 } } },
|
||||
}),
|
||||
credentials: "include",
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
mode: "topN",
|
||||
count: num_genes,
|
||||
set1: { filter: { obs: { index: set1 } } },
|
||||
set2: { filter: { obs: { index: set2 } } },
|
||||
}),
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||
return dispatchDiffExpErrors(dispatch, res);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||
const diffexpLists = { negative: [], positive: [] };
|
||||
for (const polarity of Object.keys(diffexpLists)) {
|
||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
diffexpLists[polarity] = response[polarity].map((v: any) => [
|
||||
varIndex.at(v[0], varIndexName),
|
||||
...v.slice(1),
|
||||
]);
|
||||
}
|
||||
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data: diffexpLists,
|
||||
});
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: "request differential expression error",
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const checkExplainNewTab = () => (dispatch: any) => {
|
||||
/*
|
||||
Opens toast "work in progress" warning.
|
||||
*/
|
||||
if (
|
||||
storageGet(KEYS.WORK_IN_PROGRESS_WARN) === WORK_IN_PROGRESS_WARN_STATE.ON
|
||||
) {
|
||||
dispatch({ type: "work in progress warning displayed" });
|
||||
postExplainNewTab(
|
||||
"To maintain your in-progress work on the previous dataset, we opened this dataset in a new tab."
|
||||
);
|
||||
|
||||
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||
return dispatchDiffExpErrors(dispatch, res);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||
const diffexpLists = { negative: [], positive: [] };
|
||||
for (const polarity of Object.keys(diffexpLists)) {
|
||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
diffexpLists[polarity] = response[polarity].map((v: any) => [
|
||||
varIndex.at(v[0], varIndexName),
|
||||
...v.slice(1),
|
||||
]);
|
||||
}
|
||||
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data: diffexpLists,
|
||||
});
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: "request differential expression error",
|
||||
error,
|
||||
});
|
||||
storageSet(KEYS.WORK_IN_PROGRESS_WARN, WORK_IN_PROGRESS_WARN_STATE.OFF);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const openDataset = (dataset: any) => (dispatch: any) => {
|
||||
/*
|
||||
Update in a new tab the browser location to dataset's deployment URL, kick off data load.
|
||||
*/
|
||||
|
||||
const deploymentUrl = dataset.dataset_deployments?.[0].url ?? "";
|
||||
const datasetUrl = createDatasetUrl(deploymentUrl);
|
||||
|
||||
dispatch({ type: "dataset opened" });
|
||||
storageSet(KEYS.WORK_IN_PROGRESS_WARN, WORK_IN_PROGRESS_WARN_STATE.ON);
|
||||
window.open(datasetUrl, "_blank");
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const switchDataset = (dataset: any) => (dispatch: any) => {
|
||||
/*
|
||||
Update browser location to dataset's deployment URL, kick off data load.
|
||||
TODO(cc) revisit:
|
||||
- origin (and data root) switch for environments without corresponding Portal instance (eg local, canary)
|
||||
- globals update: move to server-side, split from initial doc returned from server?
|
||||
*/
|
||||
dispatch({ type: "dataset switch" });
|
||||
|
||||
const deploymentUrl = dataset.dataset_deployments?.[0].url ?? "";
|
||||
const datasetUrl = createDatasetUrl(deploymentUrl);
|
||||
dispatch(updateLocation(datasetUrl));
|
||||
|
||||
globals.API.prefix = createAPIPrefix(globals.API.prefix, datasetUrl);
|
||||
dispatch(doInitialDataLoad());
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
const updateLocation = (url: string) => (dispatch: any) => {
|
||||
/*
|
||||
Add entry to the session's history stack.
|
||||
*/
|
||||
dispatch({ type: "location update" });
|
||||
window.history.pushState(null, "", url);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function fetchJson(pathAndQuery: any) {
|
||||
return doJsonRequest(
|
||||
@@ -271,11 +376,22 @@ function fetchJson(pathAndQuery: any) {
|
||||
);
|
||||
}
|
||||
|
||||
function fetchPortalJson(url: string) {
|
||||
/*
|
||||
Fetch JSON from Portal API.
|
||||
TODO(cc) revisit - required for dataset meta and collection requests from Portal
|
||||
*/
|
||||
return doJsonRequest(`${globals.API.portalPrefix}${url}`);
|
||||
}
|
||||
|
||||
export default {
|
||||
doInitialDataLoad,
|
||||
requestDifferentialExpression,
|
||||
requestSingleGeneExpressionCountsForColoringPOST,
|
||||
requestUserDefinedGene,
|
||||
checkExplainNewTab,
|
||||
openDataset,
|
||||
switchDataset,
|
||||
selectContinuousMetadataAction: selnActions.selectContinuousMetadataAction,
|
||||
selectCategoricalMetadataAction: selnActions.selectCategoricalMetadataAction,
|
||||
selectCategoricalAllMetadataAction:
|
||||
|
||||
@@ -2,8 +2,10 @@ import React from "react";
|
||||
import Helmet from "react-helmet";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import DatasetSelector from "./datasetSelector/datasetSelector";
|
||||
import Container from "./framework/container";
|
||||
import Layout from "./framework/layout";
|
||||
import Skeleton from "./framework/skeleton";
|
||||
import LeftSideBar from "./leftSidebar";
|
||||
import RightSideBar from "./rightSidebar";
|
||||
import Legend from "./continuousLegend";
|
||||
@@ -13,7 +15,7 @@ import Autosave from "./autosave";
|
||||
import Embedding from "./embedding";
|
||||
import TermsOfServicePrompt from "./termsPrompt";
|
||||
|
||||
import actions from "../actions";
|
||||
import actions, { checkExplainNewTab } from "../actions";
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => ({
|
||||
@@ -34,6 +36,7 @@ class App extends React.Component {
|
||||
this._onURLChanged();
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
||||
dispatch(actions.doInitialDataLoad(window.location.search));
|
||||
dispatch(checkExplainNewTab());
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -51,18 +54,7 @@ class App extends React.Component {
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
fontWeight: 500,
|
||||
top: window.innerHeight / 2,
|
||||
left: window.innerWidth / 2 - 50,
|
||||
}}
|
||||
>
|
||||
loading cellxgene
|
||||
</div>
|
||||
) : null}
|
||||
{loading ? <Skeleton /> : null}
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -81,7 +73,20 @@ class App extends React.Component {
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{(viewportRef: any) => (
|
||||
<>
|
||||
<MenuBar />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
left: 8,
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 0,
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<DatasetSelector />
|
||||
<MenuBar />
|
||||
</div>
|
||||
<Embedding />
|
||||
<Autosave />
|
||||
<TermsOfServicePrompt />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* core dependencies */
|
||||
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error --- TODO fix import
|
||||
import styles from "./datasetSelector.css";
|
||||
|
||||
// @ts-expect-error --- TODO add typing for datasets
|
||||
const buildDatasetMenuItems = (datasets) =>
|
||||
/*
|
||||
map dataset to menu item
|
||||
*/
|
||||
// @ts-expect-error --- TODO add typing for dataset
|
||||
datasets.map((dataset) => (
|
||||
<MenuItem key={dataset.id} onClick={dataset.onClick} text={dataset.name} />
|
||||
));
|
||||
|
||||
/*
|
||||
dataset menu, toggled from dataset name in app-level breadcrumbs
|
||||
*/
|
||||
// @ts-expect-error --- TODO add typing for props
|
||||
const DatasetMenu = React.memo(({ children, datasets }) => (
|
||||
<Popover
|
||||
boundary="viewport"
|
||||
content={
|
||||
<Menu
|
||||
style={{
|
||||
maxHeight:
|
||||
"290px" /* show 9.5 datasets at 30px height each, plus top padding of 5px */,
|
||||
maxWidth: "680px" /* TODO(cc) revisit max-width versus width */,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{buildDatasetMenuItems(datasets)}
|
||||
</Menu>
|
||||
}
|
||||
hasBackdrop
|
||||
minimal
|
||||
modifiers={{ offset: { offset: "0, 10" } }}
|
||||
popoverClassName={styles.datasetPopover}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
targetClassName={styles.datasetPopoverTarget}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
));
|
||||
export default DatasetMenu;
|
||||
@@ -0,0 +1,17 @@
|
||||
:local(.datasetBreadcrumb),
|
||||
:local(.datasetDisabledBreadcrumb) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:local(.datasetBreadcrumb):hover {
|
||||
color: #10161a; /* Colors.BLACK */ /* TODO(cc) revisit variable specification */
|
||||
}
|
||||
|
||||
:local(.datasetPopoverTarget) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:local(.datasetPopover) {
|
||||
box-shadow: 0 8px 24px 0 rgba(16, 22, 26, 0.2),
|
||||
0 2px 4px 0 rgba(16, 22, 26, 0.2), 0 0 0 0 rgba(16, 22, 26, 0.1);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/* core dependencies */
|
||||
import { Breadcrumb, Icon } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import React, { PureComponent } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
/* app dependencies */
|
||||
import { openDataset, switchDataset } from "../../actions";
|
||||
import DatasetMenu from "./datasetMenu";
|
||||
import * as globals from "../../globals";
|
||||
import TruncatingBreadcrumbs from "./truncatingBreadcrumbs";
|
||||
import { sortDatasets } from "../../util/stateManager/collectionsHelpers";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error --- TODO revisit
|
||||
import styles from "./datasetSelector.css";
|
||||
|
||||
/*
|
||||
app-level collection and dataset breadcrumbs.
|
||||
*/
|
||||
// @ts-expect-error ts-migrate(1238) TODO revisit
|
||||
@connect((state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
const genesetsInProgress = (state as any).genesets?.genesets?.size > 0;
|
||||
const individualGenesInProgress =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
(state as any).controls?.userDefinedGenes?.length > 0;
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
collection: (state as any).collections?.collection,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
selectedDatasetId: (state as any).collections?.selectedDatasetId,
|
||||
workInProgress: genesetsInProgress || individualGenesInProgress,
|
||||
};
|
||||
})
|
||||
class DatasetSelector extends PureComponent {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
buildBreadcrumbProp = (breadcrumbProp) =>
|
||||
/*
|
||||
Return base breadcrumb object.
|
||||
*/
|
||||
({ ...breadcrumbProp, className: styles.datasetBreadcrumb });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
buildBreadcrumbProps = (
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
dispatch,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
collection,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
selectedDatasetId,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
workInProgress
|
||||
) => {
|
||||
/*
|
||||
Create the set of breadcrumbs elements, home > collection name > dataset name, where dataset name reveals the
|
||||
dataset menu.
|
||||
*/
|
||||
const { origin } = globals.API;
|
||||
const homeProp = this.buildBreadcrumbProp({
|
||||
href: origin,
|
||||
shortText: "Home",
|
||||
text: "Home",
|
||||
});
|
||||
const collectionProp = this.buildBreadcrumbProp({
|
||||
href: `${origin}collections/${collection.id}`,
|
||||
shortText: "Collection",
|
||||
text: collection.name,
|
||||
});
|
||||
const selectedDataset = this.findDatasetById(
|
||||
selectedDatasetId,
|
||||
collection.datasets
|
||||
);
|
||||
const datasets = [...collection.datasets]
|
||||
.sort(sortDatasets)
|
||||
.map((dataset) => {
|
||||
const dispatchAction = workInProgress
|
||||
? openDataset(dataset)
|
||||
: switchDataset(dataset);
|
||||
return {
|
||||
...dataset,
|
||||
onClick: () => {
|
||||
dispatch(dispatchAction);
|
||||
},
|
||||
};
|
||||
});
|
||||
const datasetProp = this.buildBreadcrumbProp({
|
||||
shortText: "Dataset",
|
||||
text: selectedDataset.name,
|
||||
datasets,
|
||||
selectedDatasetId,
|
||||
});
|
||||
return [homeProp, collectionProp, datasetProp];
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the dataset with the given ID.
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
findDatasetById = (selectedDatasetId, datasets) =>
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
datasets.find((dataset) => dataset.id === selectedDatasetId);
|
||||
|
||||
/*
|
||||
Returns the set of datasets excluding the given selected dataset.
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
listSiblingDatasets = (datasets, selectedDataset) =>
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
datasets.filter((dataset) => dataset !== selectedDataset);
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumb = (item, disabled, renderAsMenu?) => {
|
||||
/*
|
||||
Render BP Breadcrumb, adding menu-specific styles if necessary.
|
||||
TODO(cc) split and simplify breadcrumb versus menu breadcrumb functionality.
|
||||
*/
|
||||
const className = disabled
|
||||
? styles.datasetDisabledBreadcrumb /* no sibling datasets */
|
||||
: styles.datasetBreadcrumb;
|
||||
return (
|
||||
<Breadcrumb href={item.href} className={className}>
|
||||
{item.displayText}
|
||||
{this.renderBreadcrumbMenuIcon(renderAsMenu)}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumbMenu = (item, datasetsExceptSelected) =>
|
||||
/*
|
||||
Clicking on dataset name opens menu containing all dataset names except the current dataset name for the current
|
||||
collection.
|
||||
*/
|
||||
(
|
||||
// @ts-expect-error --- TODO revisit
|
||||
<DatasetMenu datasets={datasetsExceptSelected}>
|
||||
{this.renderBreadcrumb(item, false, true)}
|
||||
</DatasetMenu>
|
||||
)
|
||||
;
|
||||
|
||||
/*
|
||||
Render breadcrumb menu icon "chevron down".
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumbMenuIcon = (renderAsMenu) =>
|
||||
renderAsMenu ? (
|
||||
<Icon
|
||||
icon={IconNames.CHEVRON_DOWN}
|
||||
style={{ marginLeft: "5px", marginRight: 0 }}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderDatasetBreadcrumb = (item) => {
|
||||
/*
|
||||
Renders the final dataset breadcrumb where sibling datasets are selectable by a breadcrumb menu.
|
||||
*/
|
||||
const { datasets, selectedDatasetId } = item;
|
||||
const selectedDataset = this.findDatasetById(selectedDatasetId, datasets);
|
||||
const siblingDatasets = this.listSiblingDatasets(datasets, selectedDataset);
|
||||
const renderMenu = siblingDatasets.length > 0;
|
||||
return renderMenu
|
||||
? this.renderBreadcrumbMenu(item, siblingDatasets)
|
||||
: this.renderBreadcrumb(item, true);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO add return value
|
||||
render() {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
const { collection, dispatch, selectedDatasetId, workInProgress } =
|
||||
this.props;
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px", // Match margin on sibling menu buttons
|
||||
flexGrow: 1,
|
||||
overflow: "scroll",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<TruncatingBreadcrumbs
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
breadcrumbRenderer={this.renderBreadcrumb}
|
||||
currentBreadcrumbRenderer={this.renderDatasetBreadcrumb}
|
||||
items={this.buildBreadcrumbProps(
|
||||
dispatch,
|
||||
collection,
|
||||
selectedDatasetId,
|
||||
workInProgress
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DatasetSelector;
|
||||
@@ -0,0 +1,280 @@
|
||||
// Core dependencies
|
||||
import { Classes, ResizeSensor } from "@blueprintjs/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// Characters to be used to indicate display text has been truncated
|
||||
const CHAR_ELLIPSIS = "...";
|
||||
|
||||
// Minimum number of characters to be displayed before transitioning to a smaller state of the breadcrumbs
|
||||
const MIN_VISIBLE_CHARS = 11;
|
||||
|
||||
// Approximate padding in pixels for each breadcrumb.
|
||||
// TODO(cc) revisit - remove if we calculate actual DOM sizes rather than estimate
|
||||
const ITEM_PADDING = 26;
|
||||
|
||||
// Approximate pixel to character ratio
|
||||
const PIXELS_PER_CHAR = 6;
|
||||
|
||||
/*
|
||||
Individual Breadcrumb States
|
||||
----------------------------
|
||||
F - full text
|
||||
T - truncated text
|
||||
S - indicates use of short text (eg "Collection" for collection name or "Dataset" for dataset name)
|
||||
H - hidden
|
||||
*/
|
||||
const STATE_FULL = "F"; // eg "Tabula Muris Senis"
|
||||
const STATE_TRUNCATED = "T"; // eg "Tabula...Senis"
|
||||
const STATE_SHORT_TEXT = "S"; // eg "Collection"
|
||||
const STATE_HIDDEN = "H"; // --
|
||||
|
||||
/*
|
||||
Breadcrumbs States
|
||||
------------------
|
||||
FFF
|
||||
FTF
|
||||
HSF
|
||||
HST
|
||||
HHS
|
||||
*/
|
||||
const STATES_FFF = `${STATE_FULL}${STATE_FULL}${STATE_FULL}`;
|
||||
const STATES_FTF = `${STATE_FULL}${STATE_TRUNCATED}${STATE_FULL}`;
|
||||
const STATES_HSF = `${STATE_HIDDEN}${STATE_SHORT_TEXT}${STATE_FULL}`;
|
||||
const STATES_HST = `${STATE_HIDDEN}${STATE_SHORT_TEXT}${STATE_TRUNCATED}`;
|
||||
const STATES_HHS = `${STATE_HIDDEN}${STATE_HIDDEN}${STATE_SHORT_TEXT}`;
|
||||
|
||||
/*
|
||||
Breadcrumb Transitions
|
||||
----------------------
|
||||
Breadcrumbs can transition bidirectionally through states in the following order, and can also repeat individual states.
|
||||
*/
|
||||
const STATES = [STATES_FFF, STATES_FTF, STATES_HSF, STATES_HST, STATES_HHS];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const calculateRequiredWidth = (itemsState: any, items: any) =>
|
||||
/*
|
||||
Return the total width required to display the given items with the given states.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.reduce((accum: number, item: any, i: number) => {
|
||||
// Grab the state for this item. For example, given the state HTF, the state of the first item is H, the state of
|
||||
// the second item is T and the state of the third item is F.
|
||||
const itemState = itemsState[i];
|
||||
// Add the width (of text) corresponding to the item's state.
|
||||
if (isItemShortText(itemState)) {
|
||||
accum += item.shortTextWidth;
|
||||
} else if (isItemTruncated(itemState)) {
|
||||
accum += item.minTextWidth;
|
||||
} else if (isItemFull(itemState)) {
|
||||
accum += item.textWidth;
|
||||
}
|
||||
return accum;
|
||||
}, 0);
|
||||
|
||||
const calculateAvailableTruncatedWidth = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items: any,
|
||||
truncatedIndex: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
itemsState: any,
|
||||
availableWidth: number
|
||||
) => {
|
||||
/*
|
||||
Return the width that the truncated item has available for display. That is, the available width minus the widths
|
||||
required by the other, non-truncated, items.
|
||||
*/
|
||||
// Grab the items other than the truncated item.
|
||||
const otherItems = [...items];
|
||||
otherItems.splice(truncatedIndex, 1);
|
||||
|
||||
// Grab the states of the the items, other than the truncated item.
|
||||
const otherItemsState = itemsState.split("");
|
||||
otherItemsState.splice(truncatedIndex, 1);
|
||||
|
||||
// Calculate the width of the other items in their corresponding states.
|
||||
const otherItemsRequiredWidth = calculateRequiredWidth(
|
||||
otherItemsState.join(""),
|
||||
otherItems
|
||||
);
|
||||
|
||||
return availableWidth - otherItemsRequiredWidth;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const getItemsStateForAvailableWidth = (items: any, availableWidth: number) => {
|
||||
/*
|
||||
Determine the current items state (eg FFF, FTF etc) for the given available width and set of items.
|
||||
*/
|
||||
for (let i = 0; i < STATES.length; i += 1) {
|
||||
const itemsState = STATES[i];
|
||||
const requiredWidth = calculateRequiredWidth(itemsState, items);
|
||||
if (availableWidth >= requiredWidth) {
|
||||
return itemsState;
|
||||
}
|
||||
}
|
||||
return STATES[STATES.length - 1]; // There's a problem, default to smallest state. TODO(cc) revisit error case here.
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const initItems = (items: any) =>
|
||||
/*
|
||||
Build initial state of items, including the calculation of short text, truncated text and full text dimensions. Use
|
||||
approximation of six pixels per char. TODO(cc) revisit use of actual widths if approximation is too loose.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.map((item: any) => ({
|
||||
...item,
|
||||
displayText: item.text, // Default display to full breadcrumb text
|
||||
minTextWidth: MIN_VISIBLE_CHARS * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
shortTextWidth: item.shortText.length * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
textWidth: item.text.length * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
}));
|
||||
|
||||
const isItemFull = (stateName: string) => stateName === STATE_FULL;
|
||||
|
||||
const isItemHidden = (stateName: string) => stateName === STATE_HIDDEN;
|
||||
|
||||
const isItemShortText = (stateName: string) => stateName === STATE_SHORT_TEXT;
|
||||
|
||||
const isItemTruncated = (stateName: string) => stateName === STATE_TRUNCATED;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const buildResizedItems = (items: any, availableWidth: number) => {
|
||||
/*
|
||||
Resize the items, either the set of visible items, or the individual item display text, to fit the given available
|
||||
width.
|
||||
*/
|
||||
const itemsState = getItemsStateForAvailableWidth(items, availableWidth);
|
||||
// TODO(cc) if same state as previous and state does not contain T (eg FFF or FSF or HHS) then don't recalc here
|
||||
return updateItems(itemsState, items, availableWidth);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const truncate = (availableWidth: number, text: string) => {
|
||||
/*
|
||||
Return truncated text with characters removed to reduce text width to the available width.
|
||||
*/
|
||||
const visibleLength = Math.floor(availableWidth / PIXELS_PER_CHAR);
|
||||
// Determine the break indices for the "before" and "after" ellipsis text tokens
|
||||
const tokenBeforeEndIndex = Math.ceil(visibleLength / 2);
|
||||
const tokenAfterStartIndex = Math.floor(visibleLength / 2);
|
||||
// Split text at break indices and join with ellipsis
|
||||
const tokenBefore = text.substr(0, tokenBeforeEndIndex).trim();
|
||||
const tokenAfter = text.substr(text.length - tokenAfterStartIndex).trim();
|
||||
return `${tokenBefore}${CHAR_ELLIPSIS}${tokenAfter}`;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const updateItems = (itemsState: any, items: any, availableWidth: number) =>
|
||||
/*
|
||||
Update each item to match its display format to the state being transitioned to.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.map((item: any, i: number) => {
|
||||
const itemState = itemsState[i];
|
||||
if (isItemHidden(itemState)) {
|
||||
return {
|
||||
...item,
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
if (isItemShortText(itemState)) {
|
||||
return {
|
||||
...item,
|
||||
displayText: item.shortText,
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
if (isItemTruncated(itemState)) {
|
||||
const truncatedAvailableWidth = calculateAvailableTruncatedWidth(
|
||||
items,
|
||||
i,
|
||||
itemsState,
|
||||
availableWidth
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
displayText: truncate(truncatedAvailableWidth, item.text),
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
displayText: item.text,
|
||||
hidden: false,
|
||||
};
|
||||
});
|
||||
|
||||
const TruncatingBreadcrumbs = React.memo(
|
||||
// @ts-expect-error --- TODO revisit
|
||||
({ breadcrumbRenderer, currentBreadcrumbRenderer, items: originalItems }) => {
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const onResize = (entries: any) => {
|
||||
/*
|
||||
On resize callback from ResizeSensor, save the current width of the breadcrumbs.
|
||||
*/
|
||||
const availableWidth = Math.floor(entries[0].contentRect.width);
|
||||
setItems(buildResizedItems(items, availableWidth));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderBreadcrumb = (item: any, currentProp: any) => {
|
||||
/*
|
||||
Invoke the render callback to render the given breadcrumb.
|
||||
*/
|
||||
if (currentProp) {
|
||||
return currentBreadcrumbRenderer(item);
|
||||
}
|
||||
return breadcrumbRenderer(item);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderBreadcrumbs = (bcItems: any) =>
|
||||
/*
|
||||
Return list element/breadcrumb for each item.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
bcItems.map((item: any, i: number) => {
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
// TODO(cc) possibly "clean" each item back to the format expected by BP so we can spread the Breadcrumb-specific
|
||||
// props in our render method, and so that knowledge of "displayText" vs "text" for example, is not required by
|
||||
// parent components.
|
||||
// See datasetSelector.renderBreadcrumb for our usage, and also the following for
|
||||
// an example pattern:
|
||||
// https://github.com/palantir/blueprint/blob/826cbdf95b577c43d5fe95b99c67ee2761c853e0/packages/core/src/components/breadcrumbs/breadcrumbs.tsx#L151
|
||||
// Could possibly also have an explicit breadcrumbsProps props to neatly encapsulate and spread
|
||||
// breadcrumb-specific props, resulting in this component being a relatively transparent wrapper around
|
||||
// BP's Breadcrumbs component. For an example pattern, see `overflowListProps` on BP Breadcrumbs component.
|
||||
const currentItem = i === bcItems.length - 1;
|
||||
return <li key={item.text}>{renderBreadcrumb(item, currentItem)}</li>;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
init/update truncating breadcrumb items
|
||||
*/
|
||||
setItems(initItems(originalItems));
|
||||
}, [originalItems]);
|
||||
|
||||
return (
|
||||
<ResizeSensor onResize={onResize}>
|
||||
<ul
|
||||
className={Classes.BREADCRUMBS}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{renderBreadcrumbs(items)}
|
||||
</ul>
|
||||
</ResizeSensor>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default TruncatingBreadcrumbs;
|
||||
@@ -0,0 +1,49 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import LeftSidebarSkeleton from "../leftSidebar/leftSidebarSkeleton";
|
||||
import Layout from "./layout";
|
||||
import RightSidebarSkeleton from "../rightSidebar/rightSidebarSkeleton";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function Skeleton() {
|
||||
/*
|
||||
Skeleton layout component displayed when in loading state.
|
||||
TODO(cc)
|
||||
- Remove dupe of "graph" area inline styles
|
||||
*/
|
||||
return (
|
||||
<Layout>
|
||||
<LeftSidebarSkeleton />
|
||||
{() => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
left: 8,
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 8,
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ height: "30px", width: "calc(100% - 482px - 10px)" }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
<div
|
||||
style={{ height: "30px", width: "482px" }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<RightSidebarSkeleton />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Skeleton;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
const Title = () => (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 24,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
cell
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
);
|
||||
|
||||
export default Title;
|
||||
@@ -0,0 +1,4 @@
|
||||
:local(.newTabToast) {
|
||||
max-width: fit-content;
|
||||
top: 42px;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message
|
||||
import styles from "./toasters.css";
|
||||
|
||||
/** Singleton toaster instance. Create separate instances for different options. */
|
||||
|
||||
const ToastTopCenter = Toaster.create({
|
||||
@@ -55,3 +59,15 @@ export const postAsyncFailureToast = (message: any) =>
|
||||
timeout: 10000,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
|
||||
/*
|
||||
Dataset opened in new tab
|
||||
*/
|
||||
export const postExplainNewTab = (message: string) => {
|
||||
ToastTopCenter.show({
|
||||
className: styles.newTabToast,
|
||||
message,
|
||||
timeout: 5000,
|
||||
intent: Intent.PRIMARY,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -509,6 +509,10 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
setReglCanvas = (canvas: any) => {
|
||||
// Ignore null canvas on unmount
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.reglCanvas = canvas;
|
||||
this.setState({
|
||||
...Graph.createReglState(canvas),
|
||||
@@ -558,7 +562,11 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
handleEnd = this.handleLassoEnd.bind(this);
|
||||
handleCancel = this.handleLassoCancel.bind(this);
|
||||
}
|
||||
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
|
||||
const {
|
||||
svg: newToolSVG,
|
||||
tool,
|
||||
container,
|
||||
} = setupSVGandBrushElements(
|
||||
selectionTool,
|
||||
handleStart,
|
||||
handleDrag,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
collection: ((state as any).collections as any)?.collection,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
schema: (state as any).annoMatrix.schema,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -30,6 +32,8 @@ class InfoDrawer extends PureComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
const {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
collection,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'position' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
position,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'aboutURL' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
@@ -58,15 +62,10 @@ class InfoDrawer extends PureComponent {
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="Dataset Overview"
|
||||
onClose={this.handleClose}
|
||||
{...{ isOpen, position }}
|
||||
>
|
||||
<Drawer onClose={this.handleClose} size={480} {...{ isOpen, position }}>
|
||||
<InfoFormat
|
||||
{...{
|
||||
datasetTitle,
|
||||
aboutURL,
|
||||
collection,
|
||||
singleValueCategories,
|
||||
dataPortalProps: dataPortalProps ?? {},
|
||||
}}
|
||||
|
||||
@@ -1,189 +1,244 @@
|
||||
import { H3, H1, UL, HTMLTable, Classes } from "@blueprintjs/core";
|
||||
import { Classes, H3, HTMLTable, Position, Tooltip } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderContributors = (contributors: any, affiliations: any) => {
|
||||
// eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII
|
||||
if (!contributors || contributors.length === 0 || true) return null;
|
||||
return (
|
||||
<>
|
||||
<H3>Contributors</H3>
|
||||
<p>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{contributors.map((contributor: any) => {
|
||||
const { email, name, institution } = contributor;
|
||||
|
||||
return (
|
||||
<span key={name}>
|
||||
{name}
|
||||
{email && `(${email})`}
|
||||
<sup>{affiliations.indexOf(institution) + 1}</sup>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
{renderAffiliations(affiliations)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// generates a list of unique institutions by order of appearance in contributors
|
||||
const buildAffiliations = (contributors = []) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const affiliations: any = [];
|
||||
contributors.forEach((contributor) => {
|
||||
const { institution } = contributor;
|
||||
if (affiliations.indexOf(institution) === -1) {
|
||||
affiliations.push(institution);
|
||||
}
|
||||
});
|
||||
return affiliations;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderAffiliations = (affiliations: any) => {
|
||||
if (affiliations.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3>Affiliations</H3>
|
||||
<UL>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{affiliations.map((item: any, index: any) => (
|
||||
<div key={item}>
|
||||
<sup>{index + 1}</sup>
|
||||
{" "}
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderDOILink = (type: any, doi: any) => {
|
||||
if (!doi) return null;
|
||||
return (
|
||||
<>
|
||||
<H3>{type}</H3>
|
||||
<p>
|
||||
<a href={doi} target="_blank" rel="noopener">
|
||||
{doi}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ONTOLOGY_KEY = "ontology_term_id";
|
||||
// Render list of metadata attributes found in categorical field
|
||||
const renderDatasetMetadata = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
singleValueCategories: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
corporaMetadata: any
|
||||
) => {
|
||||
if (singleValueCategories.size === 0) return null;
|
||||
const COLLECTION_LINK_ORDER_BY = [
|
||||
"DOI",
|
||||
"DATA_SOURCE",
|
||||
"RAW_DATA",
|
||||
"PROTOCOL",
|
||||
"LAB_WEBSITE",
|
||||
"OTHER",
|
||||
];
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildCollectionLinks = (links) => {
|
||||
/*
|
||||
sort links by custom sort order, create view-friendly model of link types.
|
||||
*/
|
||||
const sortedLinks = [...links].sort(sortCollectionLinks);
|
||||
return sortedLinks.map((link) => {
|
||||
const { link_name: name, link_type: type, link_url: url } = link;
|
||||
return {
|
||||
name: buildLinkName(name, type, url),
|
||||
type: transformLinkTypeToDisplay(type),
|
||||
url,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildDatasetMetadata = (singleValueCategories, corporaMetadata) => {
|
||||
/*
|
||||
transform Corpora metadata and single value categories into sort and render-friendly format.
|
||||
@returns [{key, value, tip}]
|
||||
*/
|
||||
const metadata = [
|
||||
...transformCorporaMetadata(corporaMetadata),
|
||||
...transformSingleValueCategoriesMetadata(singleValueCategories),
|
||||
];
|
||||
metadata.sort(sortDatasetMetadata);
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const getTableStyles = () => ({ tableLayout: "fixed", width: "100%" });
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const sortCollectionLinks = (l0, l1) =>
|
||||
/*
|
||||
sort collection links by custom order.
|
||||
TODO(cc) revisit - improve readability here
|
||||
*/
|
||||
COLLECTION_LINK_ORDER_BY.indexOf(l1.type) -
|
||||
COLLECTION_LINK_ORDER_BY.indexOf(l0.type);
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildLinkName = (name, type, url) => {
|
||||
/*
|
||||
determine name to display for collection link.
|
||||
TODO(cc) error handling
|
||||
*/
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (type === "DOI") {
|
||||
return new URL(url).pathname.substring(1);
|
||||
}
|
||||
return new URL(url).host;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const sortDatasetMetadata = (m0, m1) => {
|
||||
/*
|
||||
sort metadata key value pairs by key - alpha, ascending
|
||||
*/
|
||||
if (m0.key < m1.key) {
|
||||
return -1;
|
||||
}
|
||||
if (m0.key > m1.key) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformCorporaMetadata = (corporaMetadata) =>
|
||||
/*
|
||||
build array of view model objects from given Corpora metadata object.
|
||||
@returns [{key, value}]
|
||||
*/
|
||||
Object.entries(corporaMetadata)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformSingleValueCategoriesMetadata = (singleValueCategories) =>
|
||||
/*
|
||||
build array of view model objects from given single value categories map, ignoring ontology terms or metadata
|
||||
without values. add ontology terms as tooltips of their corresponding values.
|
||||
@returns [{key, value, tip}] where tip is an optional ontology term for the category
|
||||
*/
|
||||
Array.from(singleValueCategories.entries())
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.filter(([key, value]) => {
|
||||
if (key.indexOf(ONTOLOGY_KEY) >= 0) {
|
||||
// skip ontology terms
|
||||
return false;
|
||||
}
|
||||
// skip metadata without values
|
||||
return value;
|
||||
})
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.map(([key, value]) => {
|
||||
const viewModel = { key, value: String(value) };
|
||||
// add ontology term as tool tip if specified
|
||||
const tip = singleValueCategories.get(`${key}_${ONTOLOGY_KEY}`);
|
||||
if (tip) {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
viewModel.tip = tip;
|
||||
}
|
||||
return viewModel;
|
||||
});
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformLinkTypeToDisplay = (type) => {
|
||||
/*
|
||||
convert link type from upper snake case to title case
|
||||
TODO(cc) revisit approach here, maybe create enum-type mapping to avoid string concat inside loop?
|
||||
*/
|
||||
const tokens = type.split("_");
|
||||
return (
|
||||
tokens
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.map((token) => token.charAt(0) + token.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderCollectionLinks = (collection) => {
|
||||
/*
|
||||
render collection contact and links.
|
||||
TODO(cc) handle case where there is no contact and no links?
|
||||
*/
|
||||
const links = buildCollectionLinks(collection.links);
|
||||
const { contact_name: contactName, contact_email: contactEmail } = collection;
|
||||
return (
|
||||
<>
|
||||
<H3>Dataset Metadata</H3>
|
||||
<HTMLTable
|
||||
striped
|
||||
condensed
|
||||
style={{ display: "block", width: "100%", overflowX: "auto" }}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Label</th>
|
||||
<th>Ontology ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{renderSectionTitle("Collection")}
|
||||
{/* @ts-expect-error --- TODO revisit */}
|
||||
<HTMLTable style={getTableStyles()}>
|
||||
<tbody>
|
||||
{Object.entries(corporaMetadata).map(([key, value]) => (
|
||||
<tr {...{ key }}>
|
||||
<td>{`${key}:`}</td>
|
||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'unknown' is not assignable to type 'ReactNod... Remove this comment to see the full error message */}
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
<tr>
|
||||
<td>Contact</td>
|
||||
<td>{renderCollectionContactLink(contactName, contactEmail)}</td>
|
||||
</tr>
|
||||
{links.map(({ name, type, url }, i) => (
|
||||
<tr {...{ key: i }}>
|
||||
<td>{type}</td>
|
||||
<td>
|
||||
<a href={url} rel="noopener" target="_blank">
|
||||
{name}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{Array.from(singleValueCategories).reduce((elems, pair) => {
|
||||
// @ts-expect-error ts-migrate(2488) FIXME: Type 'unknown' must have a '[Symbol.iterator]()' m... Remove this comment to see the full error message
|
||||
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)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const prevElem = (elems as any).pop();
|
||||
const newChildren = [...prevElem.props.children];
|
||||
newChildren.splice(2, 1, [<td key="ontology">{value}</td>]);
|
||||
// Props aren't extensible so we must clone and alter the component to append the new child
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
(elems as any).push(
|
||||
React.cloneElement(prevElem, prevElem.props, newChildren)
|
||||
);
|
||||
} else {
|
||||
// Create the list item
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
(elems as any).push(
|
||||
<tr key={category}>
|
||||
<td>{`${category}:`}</td>
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return elems;
|
||||
}, [])}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders any links found in the config where link_type is not "SUMMARY"
|
||||
// If there are no links in the config, render the aboutURL
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderLinks = (projectLinks: any, aboutURL: any) => {
|
||||
if (!projectLinks && !aboutURL) return null;
|
||||
if (projectLinks)
|
||||
return (
|
||||
<>
|
||||
<H3>Project Links</H3>
|
||||
<UL>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{projectLinks.map((link: any) => {
|
||||
if (link.link_type === "SUMMARY") return null;
|
||||
return (
|
||||
<li key={link.link_name}>
|
||||
<a href={link.link_url} target="_blank" rel="noopener">
|
||||
{link.link_name}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderCollectionContactLink = (name, email) => {
|
||||
/*
|
||||
display collection contact's name with a link to their associated email.
|
||||
*/
|
||||
if (!name && !email) {
|
||||
return null;
|
||||
}
|
||||
if (email) {
|
||||
return <a href={`mailto:${email}`}>{name}</a>;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderDatasetMetadata = (singleValueCategories, corporaMetadata) => {
|
||||
/*
|
||||
render dataset metadata, mix of meta from Corpora and attributes found in categorical field.
|
||||
*/
|
||||
if (
|
||||
singleValueCategories.size === 0 &&
|
||||
Object.entries(corporaMetadata).length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const metadata = buildDatasetMetadata(singleValueCategories, corporaMetadata);
|
||||
return (
|
||||
<>
|
||||
<H3>More Info</H3>
|
||||
<p>
|
||||
<a href={aboutURL} target="_blank" rel="noopener">
|
||||
{aboutURL}
|
||||
</a>
|
||||
</p>
|
||||
{renderSectionTitle("Dataset")}
|
||||
<HTMLTable
|
||||
// @ts-expect-error --- TODO revisit
|
||||
style={getTableStyles()}
|
||||
>
|
||||
<tbody>
|
||||
{/* @ts-expect-error --- TODO revisit */}
|
||||
{metadata.map(({ key, value, tip }) => (
|
||||
<tr {...{ key }}>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
<Tooltip
|
||||
content={tip}
|
||||
disabled={!tip}
|
||||
minimal
|
||||
modifiers={{ flip: { enabled: false } }}
|
||||
position={Position.TOP}
|
||||
>
|
||||
{value}
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderSectionTitle = (title) => (
|
||||
<p style={{ margin: "24px 0 8px" }}>
|
||||
<strong>{title}</strong>
|
||||
</p>
|
||||
);
|
||||
|
||||
const InfoFormat = React.memo(
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'datasetTitle' does not exist on type '{ ... Remove this comment to see the full error message
|
||||
({ datasetTitle, singleValueCategories, aboutURL, dataPortalProps = {} }) => {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
({ collection, singleValueCategories, dataPortalProps = {} }) => {
|
||||
if (
|
||||
["1.0.0", "1.1.0"].indexOf(
|
||||
dataPortalProps.version?.corpora_schema_version
|
||||
@@ -191,26 +246,15 @@ const InfoFormat = React.memo(
|
||||
) {
|
||||
dataPortalProps = {};
|
||||
}
|
||||
const {
|
||||
title,
|
||||
publication_doi: doi,
|
||||
preprint_doi: preprintDOI,
|
||||
organism,
|
||||
contributors,
|
||||
project_links: projectLinks,
|
||||
} = dataPortalProps;
|
||||
|
||||
const affiliations = buildAffiliations(contributors);
|
||||
const { organism } = dataPortalProps;
|
||||
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className={Classes.DRAWER_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<H1>{title ?? datasetTitle}</H1>
|
||||
{renderContributors(contributors, affiliations)}
|
||||
<H3>{collection.name}</H3>
|
||||
<p>{collection.description}</p>
|
||||
{renderCollectionLinks(collection)}
|
||||
{renderDatasetMetadata(singleValueCategories, { organism })}
|
||||
{renderLinks(projectLinks, aboutURL)}
|
||||
{renderDOILink("DOI", doi)}
|
||||
{renderDOILink("Preprint DOI", preprintDOI)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconAbout() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.99999 2.00002C4.68628 2.00002 1.99999 4.68631 1.99999 8.00002C1.99999 11.3137 4.68628 14 7.99999 14C11.3137 14 14 11.3137 14 8.00002C14 4.68631 11.3137 2.00002 7.99999 2.00002ZM0.666656 8.00002C0.666656 3.94993 3.9499 0.666687 7.99999 0.666687C12.0501 0.666687 15.3333 3.94993 15.3333 8.00002C15.3333 12.0501 12.0501 15.3334 7.99999 15.3334C3.9499 15.3334 0.666656 12.0501 0.666656 8.00002ZM7.33332 5.33335C7.33332 4.96516 7.6318 4.66669 7.99999 4.66669H8.00666C8.37485 4.66669 8.67332 4.96516 8.67332 5.33335C8.67332 5.70154 8.37485 6.00002 8.00666 6.00002H7.99999C7.6318 6.00002 7.33332 5.70154 7.33332 5.33335ZM7.99999 7.33335C8.36818 7.33335 8.66666 7.63183 8.66666 8.00002V10.6667C8.66666 11.0349 8.36818 11.3334 7.99999 11.3334C7.6318 11.3334 7.33332 11.0349 7.33332 10.6667V8.00002C7.33332 7.63183 7.6318 7.33335 7.99999 7.33335Z"
|
||||
fill="#5C7080"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconAbout;
|
||||
@@ -0,0 +1,28 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconDocument() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.58579 1.25244C2.96086 0.87737 3.46957 0.666656 4 0.666656H8.66667C8.84348 0.666656 9.01305 0.736894 9.13807 0.861919L13.8047 5.52858C13.9298 5.65361 14 5.82318 14 5.99999V13.3333C14 13.8638 13.7893 14.3725 13.4142 14.7475C13.0391 15.1226 12.5304 15.3333 12 15.3333H4C3.46957 15.3333 2.96086 15.1226 2.58579 14.7475C2.21071 14.3725 2 13.8638 2 13.3333V2.66666C2 2.13622 2.21071 1.62752 2.58579 1.25244ZM4 1.99999C3.82319 1.99999 3.65362 2.07023 3.5286 2.19525C3.40357 2.32028 3.33333 2.48985 3.33333 2.66666V13.3333C3.33333 13.5101 3.40357 13.6797 3.5286 13.8047C3.65362 13.9298 3.82319 14 4 14H12C12.1768 14 12.3464 13.9298 12.4714 13.8047C12.5964 13.6797 12.6667 13.5101 12.6667 13.3333V6.66666H8.66667C8.29848 6.66666 8 6.36818 8 5.99999V1.99999H4ZM9.33333 2.9428L11.7239 5.33332H9.33333V2.9428Z"
|
||||
fill="#5C7080"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconDocument;
|
||||
@@ -0,0 +1,32 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconGitHub() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.00004 0.637878C3.83166 0.637878 0.451538 4.01725 0.451538 8.18638C0.451538 11.5216 2.61441 14.351 5.61366 15.3493C5.99079 15.4193 6.12929 15.1855 6.12929 14.9861C6.12929 14.8061 6.12229 14.2115 6.11904 13.5808C4.01904 14.0374 3.57591 12.6901 3.57591 12.6901C3.23254 11.8176 2.73779 11.5856 2.73779 11.5856C2.05279 11.1171 2.78941 11.1269 2.78941 11.1269C3.54729 11.18 3.94654 11.9048 3.94654 11.9048C4.61979 13.0585 5.71241 12.725 6.14316 12.5323C6.21091 12.0444 6.40654 11.7113 6.62241 11.5228C4.94579 11.3321 3.18316 10.6848 3.18316 7.79238C3.18316 6.96825 3.47816 6.29488 3.96104 5.76613C3.88254 5.57613 3.62416 4.80838 4.03404 3.76863C4.03404 3.76863 4.66779 3.56575 6.11029 4.54238C6.71254 4.375 7.35841 4.29088 8.00004 4.288C8.64129 4.29088 9.28754 4.37475 9.89091 4.54213C11.3317 3.5655 11.9647 3.76838 11.9647 3.76838C12.3755 4.808 12.1172 5.57588 12.0388 5.76588C12.5228 6.29463 12.8157 6.968 12.8157 7.79213C12.8157 10.6914 11.0498 11.3296 9.36891 11.5166C9.63979 11.7509 9.88104 12.2104 9.88104 12.9145C9.88104 13.9245 9.87229 14.7374 9.87229 14.986C9.87229 15.1869 10.0083 15.4223 10.3908 15.3481C13.3883 14.3489 15.5487 11.5204 15.5487 8.18638C15.5485 4.0175 12.1688 0.638003 8.00004 0.638003V0.637878Z"
|
||||
fill="#181616"
|
||||
/>
|
||||
<path
|
||||
d="M3.31057 11.4758C3.29395 11.5133 3.23495 11.5245 3.1812 11.4989C3.1262 11.4744 3.09557 11.4233 3.11332 11.3856C3.12957 11.3469 3.1887 11.3363 3.24332 11.3621C3.29832 11.3868 3.32957 11.4384 3.31045 11.4759L3.31057 11.4758ZM3.61632 11.8169C3.58045 11.8503 3.51007 11.8348 3.46232 11.7819C3.41282 11.7294 3.40357 11.659 3.4402 11.6251C3.47745 11.5919 3.5457 11.6076 3.5952 11.6601C3.64445 11.7134 3.6542 11.7831 3.61645 11.817L3.61632 11.8169ZM3.91407 12.2515C3.86782 12.2838 3.79207 12.2536 3.74532 12.1865C3.69907 12.1193 3.69907 12.0386 3.74657 12.0065C3.7932 11.9743 3.86782 12.0034 3.91532 12.0699C3.96132 12.138 3.96132 12.2186 3.91407 12.2514V12.2515ZM4.3217 12.6716C4.28045 12.7173 4.19219 12.705 4.12769 12.6429C4.06182 12.582 4.04345 12.4954 4.08482 12.4499C4.12682 12.4041 4.21544 12.4169 4.28032 12.4786C4.34619 12.5394 4.36607 12.6261 4.32194 12.6715L4.3217 12.6716ZM4.88419 12.9155C4.86582 12.9746 4.78107 13.0015 4.69544 12.9763C4.61007 12.9504 4.55419 12.8813 4.57169 12.8215C4.58919 12.7619 4.67457 12.734 4.76069 12.7609C4.84607 12.7866 4.90194 12.8554 4.88419 12.9155ZM5.50207 12.9606C5.50419 13.0229 5.43169 13.0744 5.34207 13.0756C5.25169 13.0778 5.17857 13.0273 5.17769 12.966C5.17769 12.9031 5.24869 12.8523 5.33894 12.8505C5.42857 12.8489 5.50219 12.8989 5.50219 12.9605L5.50207 12.9606ZM6.07682 12.8629C6.08757 12.9235 6.02519 12.9859 5.93607 13.0025C5.84857 13.0188 5.76732 12.981 5.75607 12.9209C5.74532 12.8586 5.80882 12.7963 5.89632 12.7801C5.98557 12.7648 6.06557 12.8014 6.07682 12.863V12.8629Z"
|
||||
fill="#181616"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconGitHub;
|
||||
@@ -0,0 +1,54 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconSlack() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.60123 8.44999C1.32765 8.45681 1.05913 8.37541 0.835376 8.21784C0.611621 8.06026 0.444506 7.83488 0.358733 7.57499C0.351233 7.55374 0.346233 7.53374 0.339983 7.51374C0.246912 7.18295 0.284862 6.82901 0.445947 6.52546C0.607032 6.22191 0.878869 5.99209 1.20498 5.88375L12.125 2.225C12.2546 2.18729 12.3888 2.16752 12.5237 2.16625C12.804 2.15869 13.0793 2.24133 13.3091 2.40201C13.5388 2.56268 13.7109 2.7929 13.8 3.05875L13.8162 3.11125C14.02 3.825 13.5125 4.4625 12.905 4.66625L2.04498 8.37499C1.90203 8.42355 1.75221 8.44887 1.60123 8.44999Z"
|
||||
fill="#80CADE"
|
||||
/>
|
||||
<path
|
||||
d="M3.4212 13.8188C3.14802 13.8283 2.87899 13.7498 2.65389 13.5947C2.4288 13.4396 2.25956 13.2162 2.1712 12.9575C2.16495 12.9375 2.1587 12.9175 2.15245 12.8963C2.05699 12.563 2.09393 12.2056 2.25556 11.8989C2.4172 11.5922 2.69105 11.3597 3.01995 11.25L13.9375 7.55751C14.0773 7.51073 14.2237 7.48626 14.3712 7.48501C14.6502 7.47966 14.9237 7.56293 15.1523 7.72286C15.381 7.88279 15.553 8.11112 15.6437 8.37502L15.66 8.43002C15.7117 8.61519 15.7241 8.80913 15.6966 8.9994C15.669 9.18968 15.6021 9.37211 15.5 9.53502C15.3437 9.77877 14.8512 9.99252 14.8512 9.99252L3.8912 13.7425C3.74172 13.793 3.58522 13.8196 3.42745 13.8213L3.4212 13.8188Z"
|
||||
fill="#DB015C"
|
||||
/>
|
||||
<path
|
||||
d="M12.51 13.8425C12.2269 13.8453 11.9502 13.7584 11.7196 13.5943C11.489 13.4301 11.3162 13.1971 11.2262 12.9287L7.58247 2.10499L7.56372 2.04374C7.47896 1.71028 7.52546 1.357 7.69363 1.05684C7.8618 0.756674 8.13879 0.532532 8.46744 0.430683C8.79608 0.328834 9.15128 0.357052 9.45973 0.509513C9.76817 0.661973 10.0063 0.927038 10.125 1.24999L13.7687 12.0725L13.7787 12.1075C13.8403 12.3073 13.8543 12.5187 13.8197 12.7248C13.785 12.931 13.7026 13.1262 13.5791 13.2949C13.4556 13.4636 13.2944 13.601 13.1084 13.6963C12.9223 13.7916 12.7165 13.8421 12.5075 13.8437L12.51 13.8425Z"
|
||||
fill="#E8A900"
|
||||
/>
|
||||
<path
|
||||
d="M7.09375 15.6663C6.81042 15.6687 6.53359 15.5814 6.30297 15.4168C6.07235 15.2522 5.89977 15.0188 5.81 14.75L2.16625 3.92876L2.1475 3.86876C2.05234 3.53693 2.08892 3.18118 2.2496 2.87565C2.41028 2.57012 2.68267 2.33839 3.01 2.22876C3.14466 2.1838 3.28553 2.16018 3.4275 2.15876C3.71057 2.15594 3.98725 2.24284 4.21788 2.407C4.4485 2.57115 4.62122 2.80413 4.71125 3.07251L8.35375 13.8963C8.36 13.915 8.36625 13.9363 8.3725 13.9563C8.42934 14.155 8.43947 14.3642 8.4021 14.5675C8.36474 14.7708 8.28088 14.9627 8.15708 15.1283C8.03328 15.2938 7.8729 15.4285 7.68844 15.5219C7.50399 15.6152 7.30046 15.6646 7.09375 15.6663Z"
|
||||
fill="#41B088"
|
||||
/>
|
||||
<path
|
||||
d="M10.7137 11.405L13.25 10.5363L12.4237 8.07001L9.875 8.93001L10.7137 11.405Z"
|
||||
fill="#CA161A"
|
||||
/>
|
||||
<path
|
||||
d="M5.30619 13.25L7.84619 12.3812L7.00994 9.89999L4.46619 10.76L5.30619 13.25Z"
|
||||
fill="#3B1D37"
|
||||
/>
|
||||
<path
|
||||
d="M8.90496 6.03251L11.4462 5.16501L10.625 2.72751L8.07996 3.58001L8.90496 6.03251Z"
|
||||
fill="#69852C"
|
||||
/>
|
||||
<path
|
||||
d="M3.49988 7.87498L6.03738 7.01123L5.20613 4.54248L2.66113 5.39498L3.49988 7.87498Z"
|
||||
fill="#118F79"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconSlack;
|
||||
@@ -2,6 +2,12 @@ import React from "react";
|
||||
import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
/* app dependencies */
|
||||
import IconAbout from "./iconAbout";
|
||||
import IconDocument from "./iconDocument";
|
||||
import IconGitHub from "./iconGitHub";
|
||||
import IconSlack from "./iconSlack";
|
||||
|
||||
const InformationMenu = React.memo((props) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'libraryVersions' does not exist on type ... Remove this comment to see the full error message
|
||||
const { libraryVersions, tosURL, privacyURL } = props;
|
||||
@@ -11,60 +17,58 @@ const InformationMenu = React.memo((props) => {
|
||||
<Menu>
|
||||
<MenuItem
|
||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||
target="_blank"
|
||||
icon="book"
|
||||
text="Documentation"
|
||||
icon={<IconDocument />}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
text="Documentation"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://join-cellxgene-users.herokuapp.com/"
|
||||
icon={<IconSlack />}
|
||||
target="_blank"
|
||||
icon="chat"
|
||||
text="Chat"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://github.com/chanzuckerberg/cellxgene"
|
||||
icon={<IconGitHub />}
|
||||
target="_blank"
|
||||
icon="git-branch"
|
||||
text="Github"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem target="_blank" text={libraryVersions?.cellxgene || null} />
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL && (
|
||||
<MenuItem
|
||||
href={tosURL}
|
||||
target="_blank"
|
||||
text="Terms of Service"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
{privacyURL && (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={<IconAbout />}
|
||||
popoverProps={{ openOnTargetFocus: false }}
|
||||
text="About cellxgene"
|
||||
>
|
||||
<MenuItem text={libraryVersions?.cellxgene || null} />
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL && (
|
||||
<MenuItem
|
||||
href={tosURL}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
text="Terms of Service"
|
||||
/>
|
||||
)}
|
||||
{privacyURL && (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="menu"
|
||||
type="button"
|
||||
icon={IconNames.INFO_SIGN}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
<Button data-testid="menu" icon={IconNames.MENU} minimal type="button" />
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import Logo from "../framework/logo";
|
||||
import Title from "../framework/title";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function LeftSidebarSkeleton() {
|
||||
/*
|
||||
Skeleton of left side bar, to be displayed during data load.
|
||||
TODO(cc)
|
||||
- Remove dupe of LeftSidebar inline styles
|
||||
- Remove dupe of TopLeftLogoAndTitle styles
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRight: `1px solid ${globals.lightGrey}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* TopLeftLogoAndTitle */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 5,
|
||||
paddingTop: 8,
|
||||
width: globals.leftSidebarWidth,
|
||||
zIndex: 1,
|
||||
borderBottom: `1px solid ${globals.lighterGrey}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Logo size={28} />
|
||||
<Title />
|
||||
</div>
|
||||
{/* Hamburger */}
|
||||
<div style={{ height: 30, width: 30 }} className={SKELETON} />
|
||||
</div>
|
||||
{/* Categorical */}
|
||||
<div style={{ padding: 8 }}>
|
||||
{[...Array(10).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{ height: 30, marginBottom: 4 }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Continuous */}
|
||||
{[...Array(2).keys()].map((i) => (
|
||||
<div key={i} style={{ height: 211 }} className={SKELETON} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeftSidebarSkeleton;
|
||||
@@ -1,49 +1,26 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Button } from "@blueprintjs/core";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Logo from "../framework/logo";
|
||||
import Truncate from "../util/truncate";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
import Title from "../framework/title";
|
||||
import InformationMenu from "./infoMenu";
|
||||
|
||||
const DATASET_TITLE_FONT_SIZE = 14;
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => {
|
||||
@connect((state) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const { corpora_props: corporaProps } = (state as any).config;
|
||||
const correctVersion =
|
||||
["1.0.0", "1.1.0"].indexOf(corporaProps?.version?.corpora_schema_version) >
|
||||
-1;
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
datasetTitle: (state as any).config?.displayNames?.dataset ?? "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
libraryVersions: (state as any).config?.library_versions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
aboutLink: (state as any).config?.links?.["about-dataset"],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
tosURL: (state as any).config?.parameters?.about_legal_tos,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
privacyURL: (state as any).config?.parameters?.about_legal_privacy,
|
||||
title: correctVersion ? corporaProps?.title : undefined,
|
||||
};
|
||||
})
|
||||
libraryVersions: (state as any).config?.library_versions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
aboutLink: (state as any).config?.links?.["about-dataset"],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
tosURL: (state as any).config?.parameters?.about_legal_tos,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
privacyURL: (state as any).config?.parameters?.about_legal_privacy,
|
||||
}))
|
||||
class LeftSideBar extends React.Component {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
handleClick = () => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
const { dispatch } = this.props;
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
const {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'datasetTitle' does not exist on type 'Re... Remove this comment to see the full error message
|
||||
datasetTitle,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'libraryVersions' does not exist on type ... Remove this comment to see the full error message
|
||||
libraryVersions,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'aboutLink' does not exist on type 'Reado... Remove this comment to see the full error message
|
||||
@@ -54,8 +31,6 @@ class LeftSideBar extends React.Component {
|
||||
tosURL,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
dispatch,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -73,48 +48,9 @@ class LeftSideBar extends React.Component {
|
||||
>
|
||||
<div>
|
||||
<Logo size={28} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 24,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
cell
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
<Title />
|
||||
</div>
|
||||
<div style={{ marginRight: 5, height: "100%" }}>
|
||||
<Button
|
||||
minimal
|
||||
style={{
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "relative",
|
||||
top: -1,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||
{title ?? datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
</Button>
|
||||
<InfoDrawer />
|
||||
<InformationMenu
|
||||
{...{
|
||||
libraryVersions,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message
|
||||
@@ -9,6 +10,7 @@ import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
|
||||
import AuthButtons from "./authButtons";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
@@ -286,18 +288,27 @@ class MenuBar extends React.PureComponent<{}, State> {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-start",
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons {...{ auth, userInfo }} />
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
icon={IconNames.INFO_SIGN}
|
||||
onClick={() => {
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
data-testid="drawer"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<UndoRedoReset
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ dispatch: any; undoDisabled: any; redoDisa... Remove this comment to see the full error message
|
||||
dispatch={dispatch}
|
||||
@@ -384,6 +395,7 @@ class MenuBar extends React.PureComponent<{}, State> {
|
||||
handleSubsetReset={this.handleSubsetReset}
|
||||
/>
|
||||
{disableDiffexp ? null : <DiffexpButtons />}
|
||||
<InfoDrawer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import * as globals from "../../globals";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function RightSidebarSkeleton() {
|
||||
/*
|
||||
Skeleton of left side bar, to be displayed during data load.
|
||||
TODO(cc)
|
||||
- Remove dupe of RightSidebar inline styles
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `1px solid ${globals.lightGrey}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
overflowY: "inherit",
|
||||
height: "inherit",
|
||||
width: "inherit",
|
||||
padding: globals.leftSidebarSectionPadding,
|
||||
}}
|
||||
>
|
||||
{/* Create new gene set button */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
position: "relative",
|
||||
top: -2,
|
||||
height: "30px",
|
||||
width: "133px",
|
||||
}}
|
||||
className={SKELETON}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RightSidebarSkeleton;
|
||||
@@ -1,6 +1,13 @@
|
||||
export const KEYS = {
|
||||
COOKIE_DECISION: "cxg.cookieDecision",
|
||||
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
|
||||
WORK_IN_PROGRESS_WARN: "cxg.WORK_IN_PROGRESS_WARN",
|
||||
};
|
||||
|
||||
// TODO(cc) review location
|
||||
export const WORK_IN_PROGRESS_WARN_STATE = {
|
||||
OFF: "off",
|
||||
ON: "on",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
|
||||
@@ -123,4 +123,22 @@ if ((window as any).CELLXGENE && (window as any).CELLXGENE.API) {
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
TODO(cc) temp set local flag to handle differences between local and deployed environments
|
||||
*/
|
||||
_API.local = window.location.hostname === "localhost";
|
||||
|
||||
/*
|
||||
TODO(cc) temp set Portal staging prefix to handle meta and collection API requests
|
||||
*/
|
||||
_API.portalPrefix =
|
||||
"https://api.cellxgene.staging.single-cell.czi.technology/dp/v1/";
|
||||
|
||||
/*
|
||||
TODO(cc) temp set of Portal/Explorer origin, required for breadcrumb links as well as generating explore URL param for
|
||||
meta endpoint in environments where hosted origin does not match Portal/dataset deployment URL origin (eg local and
|
||||
canary).
|
||||
*/
|
||||
_API.origin = "https://cellxgene.staging.single-cell.czi.technology/";
|
||||
|
||||
export const API = _API;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- TODO fix return value
|
||||
const Collections = (
|
||||
/*
|
||||
collections reducer, modifies portal collections-related state.
|
||||
*/
|
||||
state = {
|
||||
// data loading flag
|
||||
loading: true,
|
||||
error: null,
|
||||
|
||||
collection: null,
|
||||
selectedDatasetId: null,
|
||||
},
|
||||
// @ts-expect-error --- TODO fix typings
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- TODO fix return value
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "initial data load start":
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
case "collection load complete":
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: null,
|
||||
collection: action.collection,
|
||||
selectedDatasetId: action.selectedDatasetId,
|
||||
};
|
||||
case "initial data load error":
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Collections;
|
||||
@@ -3,6 +3,7 @@ import thunk from "redux-thunk";
|
||||
|
||||
import cascadeReducers from "./cascade";
|
||||
import undoable from "./undoable";
|
||||
import collections from "./collections";
|
||||
import config from "./config";
|
||||
import userInfo from "./userInfo";
|
||||
import annoMatrix from "./annoMatrix";
|
||||
@@ -43,6 +44,7 @@ const Reducer = undoable(
|
||||
["pointDilation", pointDialation],
|
||||
["autosave", autosave],
|
||||
["userInfo", userInfo],
|
||||
["collections", collections],
|
||||
]),
|
||||
[
|
||||
"annoMatrix",
|
||||
|
||||
@@ -52,6 +52,9 @@ const skipOnActions = new Set([
|
||||
"geneset: disable add new genes mode",
|
||||
"geneset: activate rename geneset mode",
|
||||
"geneset: disable rename geneset mode",
|
||||
|
||||
/* collections */
|
||||
"collection load complete",
|
||||
]);
|
||||
|
||||
/*
|
||||
@@ -112,6 +115,9 @@ const saveOnActions = new Set([
|
||||
"geneset: add genes",
|
||||
"geneset: delete genes",
|
||||
"geneset: set gene description",
|
||||
|
||||
/* collections */
|
||||
"collection load complete",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -186,52 +192,54 @@ Basic approach:
|
||||
multi-event selection and the like)
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const actionFilter = (debug: any) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
state: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
prevFilterState: any
|
||||
) => {
|
||||
const actionType = action.type;
|
||||
const filterState = {
|
||||
...prevFilterState,
|
||||
prevAction: action,
|
||||
};
|
||||
if (skipOnActions.has(actionType)) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
}
|
||||
if (
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
debounceOnActions.has(actionType) &&
|
||||
shallowObjectEq(action, prevFilterState.prevAction)
|
||||
) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
}
|
||||
if (clearOnActions.has(actionType)) {
|
||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
||||
}
|
||||
if (saveOnActions.has(actionType)) {
|
||||
return { [actionKey]: "save", [stateKey]: filterState };
|
||||
}
|
||||
const actionFilter =
|
||||
(debug: any) =>
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
state: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
prevFilterState: any
|
||||
) => {
|
||||
const actionType = action.type;
|
||||
const filterState = {
|
||||
...prevFilterState,
|
||||
prevAction: action,
|
||||
};
|
||||
if (skipOnActions.has(actionType)) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
}
|
||||
if (
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
debounceOnActions.has(actionType) &&
|
||||
shallowObjectEq(action, prevFilterState.prevAction)
|
||||
) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
}
|
||||
if (clearOnActions.has(actionType)) {
|
||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
||||
}
|
||||
if (saveOnActions.has(actionType)) {
|
||||
return { [actionKey]: "save", [stateKey]: filterState };
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Else, something more complex OR unknown to us....
|
||||
*/
|
||||
if (seedFsm.events.has(actionType)) {
|
||||
let { fsm } = filterState;
|
||||
if (!fsm) {
|
||||
/* no active FSM, so create one in init state */
|
||||
fsm = seedFsm.clone("init");
|
||||
if (seedFsm.events.has(actionType)) {
|
||||
let { fsm } = filterState;
|
||||
if (!fsm) {
|
||||
/* no active FSM, so create one in init state */
|
||||
fsm = seedFsm.clone("init");
|
||||
}
|
||||
return fsm.next(action.type, { state, action });
|
||||
}
|
||||
return fsm.next(action.type, { state, action });
|
||||
}
|
||||
|
||||
/* else, we have no idea what this is - skip it */
|
||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
};
|
||||
/* else, we have no idea what this is - skip it */
|
||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
};
|
||||
|
||||
/*
|
||||
return true if objA and objB are ===, OR if:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Helper functions for querying, binding and sorting Portal dataset meta and collections.
|
||||
*/
|
||||
|
||||
/* app dependencies */
|
||||
import * as globals from "../../globals";
|
||||
|
||||
export function createAPIPrefix(
|
||||
existingPrefix: string,
|
||||
replaceWithPrefix: string
|
||||
): string {
|
||||
/*
|
||||
When selecting a dataset from the dataset selector, the globals API prefix value must be updated to match the selected
|
||||
dataset's deployment URL. For example, update protocol://origin/dataRoot/current-dataset.cxg/api/v2 to
|
||||
protocol://origin/dataRoot/selected-dataset.cxg/api/v2.
|
||||
TODO(cc) revisit updating globals API prefix locally, possibly move to back end?
|
||||
*/
|
||||
const newDataRootAndDeploymentId =
|
||||
bindDataRootAndDeploymentId(replaceWithPrefix);
|
||||
return existingPrefix.replace(
|
||||
/([a-z0-9_.-]+\/[a-z0-9_.-]+\.cxg)/i,
|
||||
newDataRootAndDeploymentId
|
||||
);
|
||||
}
|
||||
|
||||
export function createDatasetUrl(deploymentUrl: string): string {
|
||||
/*
|
||||
Switch out dataset URL origin to the current location's origin. For local environments, also replace the
|
||||
data root of the given URL to /d/. For example, update protocol://origin/dataRoot/dataset.cxg to
|
||||
protocol://origin/d/dataset.cxg.
|
||||
TODO(cc) revisit special handling for canary and local environments.
|
||||
*/
|
||||
const dataRoot = globals.API.local ? "d" : bindDataRoot(deploymentUrl);
|
||||
const deploymentId = bindDeploymentId(deploymentUrl);
|
||||
return `${window.location.origin}/${dataRoot}/${deploymentId}/`;
|
||||
}
|
||||
|
||||
export function createExplorerUrl(): string {
|
||||
/*
|
||||
The current URL is passed as an "explorer URL" query string parameter to the dataset meta API. For environments where
|
||||
the current origin does not match the origin specified in the dataset deployment URLs (eg local, canary), update the
|
||||
origin to be the origin specified in the globals. Also update the data root for local environments.
|
||||
TODO(cc) revisit special handling for local and canary environments
|
||||
*/
|
||||
const url = window.location.href;
|
||||
if (url.indexOf(globals.API.origin) === 0) {
|
||||
return url;
|
||||
}
|
||||
const { origin } = globals.API;
|
||||
if (globals.API.local) {
|
||||
const dataRoot = "e";
|
||||
const deploymentId = bindDeploymentId(url);
|
||||
return `${origin}${dataRoot}/${deploymentId}/`;
|
||||
}
|
||||
const dataRootAndDeploymentId = bindDataRootAndDeploymentId(url);
|
||||
return `${origin}${dataRootAndDeploymentId}/`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function sortDatasets(vm0: any, vm1: any): number {
|
||||
/*
|
||||
Sort datasets by cell count, descending.
|
||||
*/
|
||||
return (vm1.cell_count ?? 0) - (vm0.cell_count ?? 0);
|
||||
}
|
||||
|
||||
function bindDataRoot(pathOrUrl: string): string {
|
||||
/*
|
||||
read dataroot from path. given "protocol://hostname/x/any-alpha-numeric.cxg/", match on "/x/",
|
||||
read data root from path. given "protocol://origin/x/any-alpha-numeric.cxg/", match on "/x/",
|
||||
group on "x".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]+)\/[a-z0-9_.-]+\.cxg\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(`Unable to bind data root from "${pathOrUrl}"`);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
function bindDataRootAndDeploymentId(pathOrUrl: string): string {
|
||||
/*
|
||||
Read data root and deployment ID from path. Given "protocol://origin/x/any-alpha-numeric.cxg/", match on
|
||||
"/x/any-alpha-numeric.cxg/", group on "x/any-alpha-numeric.cxg".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]+\/[a-z0-9_.-]+\.cxg)\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
if (globals.API.local) {
|
||||
return "e/792d29b8-83d4-4e6e-b3ce-cad060d1a23b.cxg"; // TODO(cc) default data root and deployment ID for local (single mode)
|
||||
}
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(
|
||||
`Unable to bind data root and deployment ID from "${pathOrUrl}"`
|
||||
);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
function bindDeploymentId(pathOrUrl: string): string {
|
||||
/*
|
||||
read name of cxg from path. given "protocol://origin/x/any-alpha-numeric.cxg/", match on "/any-alpha-numeric.cxg/",
|
||||
group on "any-alpha-numeric.cxg".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]*\.cxg)\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
if (globals.API.local) {
|
||||
return "792d29b8-83d4-4e6e-b3ce-cad060d1a23b.cxg"; // TODO(cc) default dataset for local (single mode)
|
||||
}
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(`Unable to bind deployment ID from "${pathOrUrl}"`);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
Reference in New Issue
Block a user