mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 13:58:12 +08:00
Merge branch 'main' into colinmegill/geneset-prototype
This commit is contained in:
@@ -491,19 +491,7 @@ const CategoryRender = React.memo(
|
||||
/*
|
||||
Entire category has a single value, special case.
|
||||
*/
|
||||
const theOneValue = categorySummary.categoryValues[0];
|
||||
return (
|
||||
<div style={{ marginBottom: 10, marginTop: 4 }}>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 150, fontWeight: 700 }}>
|
||||
{metadataField}
|
||||
</span>
|
||||
</Truncate>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 150 }}>{`: ${theOneValue}`}</span>
|
||||
</Truncate>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -81,7 +81,7 @@ class CategoryValue extends React.Component {
|
||||
get shouldRenderStackedBarOrHistogram() {
|
||||
const { colorAccessor, isColorBy, annotations } = this.props;
|
||||
|
||||
return colorAccessor && !isColorBy && !annotations.isEditingLabelName;
|
||||
return !!colorAccessor && !isColorBy && !annotations.isEditingLabelName;
|
||||
}
|
||||
|
||||
handleDeleteValue = () => {
|
||||
@@ -439,7 +439,9 @@ class CategoryValue extends React.Component {
|
||||
|
||||
if (
|
||||
!this.shouldRenderStackedBarOrHistogram ||
|
||||
!AnnotationsHelpers.isContinuousAnnotation(schema, colorAccessor)
|
||||
// This function returns true on categorical annotations(when stacked bar should not render),
|
||||
// in cases where the colorAccessor is a gene this function will return undefined since genes do not live on the schema
|
||||
AnnotationsHelpers.isCategoricalAnnotation(schema, colorAccessor) === true
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { PureComponent } from "react";
|
||||
import { connect, shallowEqual } from "react-redux";
|
||||
import { Drawer } from "@blueprintjs/core";
|
||||
import Async from "react-async";
|
||||
|
||||
import InfoFormat from "./infoFormat";
|
||||
import {
|
||||
selectableCategoryNames,
|
||||
createCategorySummaryFromDfCol,
|
||||
} from "../../util/stateManager/controlsHelpers";
|
||||
|
||||
@connect((state) => {
|
||||
return {
|
||||
annoMatrix: state.annoMatrix,
|
||||
schema: state.annoMatrix.schema,
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
aboutURL: state.config?.links?.["about-dataset"],
|
||||
isOpen: state.controls.datasetDrawer,
|
||||
dataPortalProps: state.config?.["corpora_props"] ?? {},
|
||||
};
|
||||
})
|
||||
class InfoDrawer extends PureComponent {
|
||||
static watchAsync(props, prevProps) {
|
||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||
}
|
||||
|
||||
fetchAsyncProps = async (props) => {
|
||||
const { schema } = props.watchProps;
|
||||
const { annoMatrix } = this.props;
|
||||
|
||||
const allCategoryNames = selectableCategoryNames(schema).sort();
|
||||
|
||||
const nonUserAnnoCategories = allCategoryNames.map((catName) => {
|
||||
const isUserAnno = schema?.annotations?.obsByName[catName]?.writable;
|
||||
if (!isUserAnno) return annoMatrix.fetch("obs", catName);
|
||||
return null;
|
||||
});
|
||||
const singleValueCategories = (
|
||||
await Promise.all(nonUserAnnoCategories)
|
||||
).reduce((acc, categoryData, i) => {
|
||||
const catName = allCategoryNames[i];
|
||||
|
||||
const column = categoryData.icol(0);
|
||||
const colSchema = schema.annotations.obsByName[catName];
|
||||
|
||||
const categorySummary = createCategorySummaryFromDfCol(column, colSchema);
|
||||
|
||||
const { numCategoryValues } = categorySummary;
|
||||
// Add to the array if the category has only one value
|
||||
if (numCategoryValues === 1) {
|
||||
acc.set(catName, categorySummary.allCategoryValues[0]);
|
||||
}
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
return { singleValueCategories };
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
position,
|
||||
aboutURL,
|
||||
datasetTitle,
|
||||
schema,
|
||||
isOpen,
|
||||
dataPortalProps,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="Dataset Overview"
|
||||
onClose={this.handleClose}
|
||||
{...{ isOpen, position }}
|
||||
>
|
||||
<Async
|
||||
watchFn={InfoDrawer.watchAsync}
|
||||
promiseFn={this.fetchAsyncProps}
|
||||
watchProps={{ schema }}
|
||||
>
|
||||
<Async.Pending>
|
||||
<InfoFormat
|
||||
skeleton
|
||||
{...{ datasetTitle, aboutURL, dataPortalProps }}
|
||||
/>
|
||||
</Async.Pending>
|
||||
<Async.Rejected>
|
||||
{(error) => {
|
||||
console.error(error);
|
||||
return <span>Failed to load info</span>;
|
||||
}}
|
||||
</Async.Rejected>
|
||||
<Async.Fulfilled>
|
||||
{(asyncProps) => {
|
||||
const { singleValueCategories } = asyncProps;
|
||||
return (
|
||||
<InfoFormat
|
||||
{...{
|
||||
datasetTitle,
|
||||
aboutURL,
|
||||
singleValueCategories,
|
||||
dataPortalProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Async.Fulfilled>
|
||||
</Async>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default InfoDrawer;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { H3, H1, UL, Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
const renderContributors = (contributors, affiliations, skeleton) => {
|
||||
// eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII
|
||||
if (!contributors || contributors.length === 0 || true) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Contributors</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>
|
||||
{contributors.map((contributor) => {
|
||||
const { email, name, institution } = contributor;
|
||||
|
||||
return (
|
||||
<span key={name}>
|
||||
{name}
|
||||
{email && `(${email})`}
|
||||
<sup>{affiliations.indexOf(institution) + 1}</sup>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
{renderAffiliations(affiliations, skeleton)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// generates a list of unique institutions by order of appearance in contributors
|
||||
const buildAffiliations = (contributors = []) => {
|
||||
const affiliations = [];
|
||||
contributors.forEach((contributor) => {
|
||||
const { institution } = contributor;
|
||||
if (affiliations.indexOf(institution) === -1) {
|
||||
affiliations.push(institution);
|
||||
}
|
||||
});
|
||||
return affiliations;
|
||||
};
|
||||
|
||||
const renderAffiliations = (affiliations, skeleton) => {
|
||||
if (affiliations.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Affiliations</H3>
|
||||
<UL>
|
||||
{affiliations.map((item, index) => (
|
||||
<div key={item} className={skeleton ? Classes.SKELETON : null}>
|
||||
<sup>{index + 1}</sup>
|
||||
{" "}
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDOILink = (type, doi, skeleton) => {
|
||||
if (!doi) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>{type}</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>
|
||||
<a href={doi} target="_blank" rel="noopener">
|
||||
{doi}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOrganism = (organism, skeleton) => {
|
||||
if (!organism) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Organism</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>{organism}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Render list of metadata attributes found in categorical field
|
||||
// Ignores categories with empty or null values
|
||||
const renderSingleValueCategories = (singleValueCategories, skeleton) => {
|
||||
if (singleValueCategories.size === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Dataset Metadata</H3>
|
||||
<UL>
|
||||
{Array.from(singleValueCategories).map((pair) => {
|
||||
if (!pair[1] || pair[1] === "") return null;
|
||||
return (
|
||||
<li
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
key={pair[0]}
|
||||
>{`${pair[0]}: ${pair[1]}`}</li>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders any links found in the config where link_type is not "SUMMARY"
|
||||
// If there are no links in the config, render the aboutURL
|
||||
const renderLinks = (projectLinks, aboutURL, skeleton) => {
|
||||
if (!projectLinks && !aboutURL) return null;
|
||||
if (projectLinks)
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Project Links</H3>
|
||||
<UL>
|
||||
{projectLinks.map((link) => {
|
||||
if (link.link_type === "SUMMARY") return null;
|
||||
return (
|
||||
<li
|
||||
key={link.link_name}
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
>
|
||||
<a href={link.link_url} target="_blank" rel="noopener">
|
||||
{link.link_name}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>More Info</H3>
|
||||
<p>
|
||||
<a
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
href={aboutURL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{aboutURL}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NUM_CATEGORIES = 8;
|
||||
|
||||
// Generates arbitrary placeholder array for singleValueCategories skeleton shape
|
||||
const singleValueCategoriesPlaceholder = Array.from(Array(NUM_CATEGORIES)).map(
|
||||
(_, index) => {
|
||||
return [index, index];
|
||||
}
|
||||
);
|
||||
|
||||
const InfoFormat = React.memo(
|
||||
({
|
||||
datasetTitle,
|
||||
singleValueCategories = new Map(singleValueCategoriesPlaceholder),
|
||||
aboutURL = "thisisabouthtelengthofaurl",
|
||||
dataPortalProps = {},
|
||||
skeleton = false,
|
||||
}) => {
|
||||
if (dataPortalProps.corpora_schema_version === "1.0.0") {
|
||||
dataPortalProps = {};
|
||||
}
|
||||
const {
|
||||
title,
|
||||
publication_doi: doi,
|
||||
preprint_doi: preprintDOI,
|
||||
organism,
|
||||
contributors,
|
||||
project_links: projectLinks,
|
||||
} = dataPortalProps;
|
||||
|
||||
const affiliations = buildAffiliations(contributors);
|
||||
|
||||
return (
|
||||
<div style={{ margin: 24, overflow: "auto" }}>
|
||||
<H1 className={skeleton ? Classes.SKELETON : null}>
|
||||
{title ?? datasetTitle}
|
||||
</H1>
|
||||
{renderContributors(contributors, affiliations, skeleton)}
|
||||
{renderDOILink("DOI", doi, skeleton)}
|
||||
{renderDOILink("Preprint DOI", preprintDOI, skeleton)}
|
||||
{renderOrganism(organism, skeleton)}
|
||||
{renderSingleValueCategories(singleValueCategories, skeleton)}
|
||||
{renderLinks(projectLinks, aboutURL, skeleton)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default InfoFormat;
|
||||
@@ -1,22 +1,27 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Button } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Logo from "../framework/logo";
|
||||
import Truncate from "../util/truncate";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
|
||||
const DATASET_TITLE_WIDTH = 190;
|
||||
const DATASET_TITLE_FONT_SIZE = 14;
|
||||
|
||||
@connect((state) => ({
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
aboutURL: state.config?.links?.["about-dataset"],
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
}))
|
||||
class LeftSideBar extends React.Component {
|
||||
handleClick = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasetTitle, aboutURL } = this.props;
|
||||
const { datasetTitle } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -53,39 +58,23 @@ class LeftSideBar extends React.Component {
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
<div
|
||||
<Button
|
||||
minimal
|
||||
icon={IconNames.BOOK}
|
||||
style={{
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
display: "inline-block",
|
||||
width: DATASET_TITLE_WIDTH,
|
||||
marginLeft: "7px",
|
||||
height: "1.2em",
|
||||
overflow: "hidden",
|
||||
wordBreak: "break-all",
|
||||
position: "absolute",
|
||||
right: 10,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{aboutURL ? (
|
||||
<Truncate>
|
||||
<a
|
||||
style={{ width: 185 }}
|
||||
href={aboutURL}
|
||||
data-testid="header"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{datasetTitle}
|
||||
</a>
|
||||
</Truncate>
|
||||
) : (
|
||||
<Truncate>
|
||||
<span style={{ width: 185 }} data-testid="header">
|
||||
{datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
)}
|
||||
</div>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||
{datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
</Button>
|
||||
<InfoDrawer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,10 +250,7 @@ class MenuBar extends React.PureComponent {
|
||||
>
|
||||
<AuthButtons auth={auth} userinfo={userinfo} />
|
||||
<InformationMenu
|
||||
libraryVersions={libraryVersions}
|
||||
aboutLink={aboutLink}
|
||||
tosURL={tosURL}
|
||||
privacyURL={privacyURL}
|
||||
{...{ libraryVersions, aboutLink, tosURL, privacyURL, dispatch }}
|
||||
/>
|
||||
<UndoRedoReset
|
||||
dispatch={dispatch}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
const handleClick = (dispatch) => {
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
const InformationMenu = React.memo((props) => {
|
||||
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
|
||||
const { libraryVersions, tosURL, privacyURL, dispatch } = props;
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
{aboutLink ? (
|
||||
<MenuItem
|
||||
href={aboutLink}
|
||||
target="_blank"
|
||||
icon="document-open"
|
||||
text="About this dataset"
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => handleClick(dispatch)}
|
||||
icon={IconNames.BOOK}
|
||||
text="Dataset Overview"
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||
|
||||
@@ -84,7 +84,7 @@ class TermsPrompt extends React.PureComponent {
|
||||
}}
|
||||
href={tosURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
terms of service
|
||||
</a>
|
||||
@@ -106,7 +106,7 @@ class TermsPrompt extends React.PureComponent {
|
||||
}}
|
||||
href={privacyURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
privacy policy
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user