mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-25 08:28:11 +08:00
* 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>
243 lines
6.9 KiB
JavaScript
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;
|