Files
cellxgene/client/src/components/categorical/index.js
T
bmccandlessandColin Megill 298924fef5 Separate userinfo from the config endpoint (#1728)
* Separate userinfo from the config endpoint

previously information about if the user was logged in and their username
was part of the config endpoint.
However, the config endpoint was previously static, and has a cache control.
Rather than not caching the config, a new endpoint called "userinfo"
is created to handle that information.

The config endpoint still has the non-changing part of the authentication:

  config:
    authentication:
        requires_client_login:  True/False
        login: <uri to login endoint if requires_client_login is True>
        logout: <uri to logout endoint if requires_client_login is True>

The userinfo endpoint returns this information:

  userinfo:
    is_authenticated:  True/False
    username: <string if is_authenticated>

if authentication is not enabled then the config does not have an authentication key,
and userinfo returns None.

Also in the PR are a few minor code improvements and bug fixes

Co-authored-by: Colin Megill <colinmegill@gmail.com>
2020-08-17 13:41:03 -07:00

243 lines
6.9 KiB
JavaScript

// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip, Position } from "@blueprintjs/core";
import { connect } from "react-redux";
import * as globals from "../../globals";
import Category from "./category";
import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager";
import AnnoDialog from "./annoDialog";
import AnnoSelect from "./annoSelect";
import LabelInput from "./labelInput";
import { labelPrompt } from "./labelUtil";
import actions from "../../actions";
@connect((state) => ({
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
schema: state.annoMatrix?.schema,
ontology: state.ontology,
userinfo: state.userinfo,
}))
class Categories extends React.Component {
constructor(props) {
super(props);
this.state = {
createAnnoModeActive: false,
newCategoryText: "",
categoryToDuplicate: null,
expandedCats: new Set(),
};
}
handleCreateUserAnno = (e) => {
const { dispatch } = this.props;
const { newCategoryText, categoryToDuplicate } = this.state;
dispatch(
actions.annotationCreateCategoryAction(
newCategoryText,
categoryToDuplicate
)
);
this.setState({
createAnnoModeActive: false,
categoryToDuplicate: null,
newCategoryText: "",
});
e.preventDefault();
};
handleEnableAnnoMode = () => {
this.setState({ createAnnoModeActive: true });
};
handleDisableAnnoMode = () => {
this.setState({
createAnnoModeActive: false,
categoryToDuplicate: null,
newCategoryText: "",
});
};
handleModalDuplicateCategorySelection = (d) => {
this.setState({ categoryToDuplicate: d });
};
categoryNameError = (name) => {
/*
return false if this is a LEGAL/acceptable category name or NULL/empty string,
or return an error type.
*/
/* allow empty string */
if (name === "") return false;
/*
test for uniqueness against *all* annotation names, not just the subset
we render as categorical.
*/
const { schema } = this.props;
const allCategoryNames = schema.annotations.obs.columns.map((c) => c.name);
/* check category name syntax */
const error = AnnotationsHelpers.annotationNameIsErroneous(name);
if (error) {
return error;
}
/* disallow duplicates */
if (allCategoryNames.indexOf(name) !== -1) {
return "duplicate";
}
/* otherwise, no error */
return false;
};
handleChange = (name) => {
this.setState({ newCategoryText: name });
};
handleSelect = (name) => {
this.setState({ newCategoryText: name });
};
instruction = (name) => {
return labelPrompt(
this.categoryNameError(name),
"New, unique category name",
":"
);
};
onExpansionChange = (catName) => {
const { expandedCats } = this.state;
if (expandedCats.has(catName)) {
const _expandedCats = new Set(expandedCats);
_expandedCats.delete(catName);
this.setState({ expandedCats: _expandedCats });
} else {
const _expandedCats = new Set(expandedCats);
_expandedCats.add(catName);
this.setState({ expandedCats: _expandedCats });
}
};
render() {
const {
createAnnoModeActive,
categoryToDuplicate,
newCategoryText,
expandedCats,
} = this.state;
const {
writableCategoriesEnabled,
schema,
ontology,
userinfo,
} = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
/* all names, sorted in display order. Will be rendered in this order */
const allCategoryNames = ControlsHelpers.selectableCategoryNames(
schema
).sort();
return (
<div
style={{
padding: globals.leftSidebarSectionPadding,
}}
>
<AnnoDialog
isActive={createAnnoModeActive}
title="Create new category"
instruction={this.instruction(newCategoryText)}
cancelTooltipContent="Close this dialog without creating a category."
primaryButtonText="Create new category"
primaryButtonProps={{ "data-testid": "submit-category" }}
text={newCategoryText}
validationError={this.categoryNameError(newCategoryText)}
handleSubmit={this.handleCreateUserAnno}
handleCancel={this.handleDisableAnnoMode}
annoInput={
<LabelInput
labelSuggestions={ontologyEnabled ? ontology.terms : null}
onChange={this.handleChange}
onSelect={this.handleSelect}
inputProps={{
"data-testid": "new-category-name",
leftIcon: "tag",
intent: "none",
autoFocus: true,
}}
newLabelMessage="New category"
/>
}
annoSelect={
<AnnoSelect
handleModalDuplicateCategorySelection={
this.handleModalDuplicateCategorySelection
}
categoryToDuplicate={categoryToDuplicate}
allCategoryNames={allCategoryNames}
/>
}
/>
{/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */}
{allCategoryNames.map((catName) =>
!schema.annotations.obsByName[catName].writable ? (
<Category
key={catName}
metadataField={catName}
onExpansionChange={this.onExpansionChange}
isExpanded={expandedCats.has(catName)}
createAnnoModeActive={createAnnoModeActive}
/>
) : null
)}
{/* WRITEABLE FIELDS */}
{allCategoryNames.map((catName) =>
schema.annotations.obsByName[catName].writable ? (
<Category
key={catName}
metadataField={catName}
onExpansionChange={this.onExpansionChange}
isExpanded={expandedCats.has(catName)}
createAnnoModeActive={createAnnoModeActive}
/>
) : null
)}
{writableCategoriesEnabled ? (
<Tooltip
content={
userinfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userinfo.is_authenticated}
>
Create new category
</AnchorButton>
</Tooltip>
) : null}
</div>
);
}
}
export default Categories;