mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 17:18:11 +08:00
Create dataset info drawer (#1805)
* create infoDrawer * create read/writes to redux store * reimplement reducer that vanished * remove aboutURL stuff from title * add formatting and style * s/length/size and make metadata items list items * remove comment * remove empty singletons * refactor into async react component * Clean up skeleton * swap out for loop for map * add comment * replace placeholder * switch ternary for `&&` * event handling fixes and PR feedback * add button and move click handler to button * ditch empty categories * move drawer button handling to redux * remove categorical move note * PR feedback from colin * update snapshot * remove hover state
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
|
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
|
||||||
|
|
||||||
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
|
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
|
||||||
|
|||||||
@@ -491,19 +491,7 @@ const CategoryRender = React.memo(
|
|||||||
/*
|
/*
|
||||||
Entire category has a single value, special case.
|
Entire category has a single value, special case.
|
||||||
*/
|
*/
|
||||||
const theOneValue = categorySummary.categoryValues[0];
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { PureComponent } from "react";
|
||||||
|
import { connect, shallowEqual } from "react-redux";
|
||||||
|
import { Drawer, H3, H1, UL, Classes } from "@blueprintjs/core";
|
||||||
|
import Async from "react-async";
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
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 } = 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 }} />
|
||||||
|
</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 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Async.Fulfilled>
|
||||||
|
</Async>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_CATEGORIES = 8;
|
||||||
|
|
||||||
|
const singleValueCategoriesPlaceholder = Array.from(Array(NUM_CATEGORIES)).map(
|
||||||
|
(_, index) => {
|
||||||
|
return [index, index];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const InfoFormat = ({
|
||||||
|
datasetTitle,
|
||||||
|
singleValueCategories = new Map(singleValueCategoriesPlaceholder),
|
||||||
|
aboutURL = "thisisabouthtelengthofaurl",
|
||||||
|
skeleton = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ margin: 24 }}>
|
||||||
|
<H1 className={skeleton ? Classes.SKELETON : null}>{datasetTitle}</H1>
|
||||||
|
{singleValueCategories.size > 0 && (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{aboutURL && (
|
||||||
|
<>
|
||||||
|
<H3 className={skeleton ? Classes.SKELETON : null}>More Info</H3>
|
||||||
|
<a
|
||||||
|
className={skeleton ? Classes.SKELETON : null}
|
||||||
|
href={aboutURL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{aboutURL}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default InfoDrawer;
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { Button } from "@blueprintjs/core";
|
||||||
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Logo from "../framework/logo";
|
import Logo from "../framework/logo";
|
||||||
import Truncate from "../util/truncate";
|
import Truncate from "../util/truncate";
|
||||||
|
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||||
|
|
||||||
const DATASET_TITLE_WIDTH = 190;
|
|
||||||
const DATASET_TITLE_FONT_SIZE = 14;
|
const DATASET_TITLE_FONT_SIZE = 14;
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||||
aboutURL: state.config?.links?.["about-dataset"],
|
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
|
||||||
}))
|
}))
|
||||||
class LeftSideBar extends React.Component {
|
class LeftSideBar extends React.Component {
|
||||||
|
handleClick = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch({ type: "toggle dataset drawer" });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasetTitle, aboutURL } = this.props;
|
const { datasetTitle } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -53,39 +58,23 @@ class LeftSideBar extends React.Component {
|
|||||||
</span>
|
</span>
|
||||||
gene
|
gene
|
||||||
</span>
|
</span>
|
||||||
<div
|
<Button
|
||||||
|
minimal
|
||||||
|
icon={IconNames.BOOK}
|
||||||
style={{
|
style={{
|
||||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||||
position: "relative",
|
position: "absolute",
|
||||||
top: -6,
|
right: 10,
|
||||||
display: "inline-block",
|
|
||||||
width: DATASET_TITLE_WIDTH,
|
|
||||||
marginLeft: "7px",
|
|
||||||
height: "1.2em",
|
|
||||||
overflow: "hidden",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
}}
|
||||||
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
{aboutURL ? (
|
<Truncate>
|
||||||
<Truncate>
|
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||||
<a
|
{datasetTitle}
|
||||||
style={{ width: 185 }}
|
</span>
|
||||||
href={aboutURL}
|
</Truncate>
|
||||||
data-testid="header"
|
</Button>
|
||||||
target="_blank"
|
<InfoDrawer />
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{datasetTitle}
|
|
||||||
</a>
|
|
||||||
</Truncate>
|
|
||||||
) : (
|
|
||||||
<Truncate>
|
|
||||||
<span style={{ width: 185 }} data-testid="header">
|
|
||||||
{datasetTitle}
|
|
||||||
</span>
|
|
||||||
</Truncate>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,10 +250,7 @@ class MenuBar extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<AuthButtons auth={auth} userinfo={userinfo} />
|
<AuthButtons auth={auth} userinfo={userinfo} />
|
||||||
<InformationMenu
|
<InformationMenu
|
||||||
libraryVersions={libraryVersions}
|
{...{ libraryVersions, aboutLink, tosURL, privacyURL, dispatch }}
|
||||||
aboutLink={aboutLink}
|
|
||||||
tosURL={tosURL}
|
|
||||||
privacyURL={privacyURL}
|
|
||||||
/>
|
/>
|
||||||
<UndoRedoReset
|
<UndoRedoReset
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
||||||
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
import styles from "./menubar.css";
|
import styles from "./menubar.css";
|
||||||
|
|
||||||
|
const handleClick = (dispatch) => {
|
||||||
|
dispatch({ type: "toggle dataset drawer" });
|
||||||
|
};
|
||||||
|
|
||||||
const InformationMenu = React.memo((props) => {
|
const InformationMenu = React.memo((props) => {
|
||||||
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
|
const { libraryVersions, tosURL, privacyURL, dispatch } = props;
|
||||||
return (
|
return (
|
||||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
{aboutLink ? (
|
<MenuItem
|
||||||
<MenuItem
|
onClick={() => handleClick(dispatch)}
|
||||||
href={aboutLink}
|
icon={IconNames.BOOK}
|
||||||
target="_blank"
|
text="Dataset Overview"
|
||||||
icon="document-open"
|
/>
|
||||||
text="About this dataset"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||||
|
|||||||
Vendored
+9
@@ -20,6 +20,9 @@ const Controls = (
|
|||||||
scatterplotXXaccessor: null, // just easier to read
|
scatterplotXXaccessor: null, // just easier to read
|
||||||
scatterplotYYaccessor: null,
|
scatterplotYYaccessor: null,
|
||||||
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
|
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
|
||||||
|
|
||||||
|
singletonHover: false,
|
||||||
|
datasetDrawer: false,
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
) => {
|
) => {
|
||||||
@@ -162,6 +165,12 @@ const Controls = (
|
|||||||
scatterplotYYaccessor: null,
|
scatterplotYYaccessor: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**************************
|
||||||
|
Dataset Drawer
|
||||||
|
**************************/
|
||||||
|
case "toggle dataset drawer":
|
||||||
|
return { ...state, datasetDrawer: !state.datasetDrawer };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user