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:
Severiano Badajoz
2020-09-09 17:55:43 -07:00
committed by GitHub
parent 5781879da5
commit 89b68723cc
7 changed files with 198 additions and 63 deletions
@@ -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;
}
/*
@@ -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
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>
);
}
+1 -4
View File
@@ -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}
+11 -11
View File
@@ -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/"