mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 07:28:11 +08:00
refactor: drop non-session auth from frontend (#2419)
This commit is contained in:
+1
-2
@@ -7,7 +7,6 @@ GENE_SETS_FILENAME := $(shell basename $(GENE_SETS))
|
|||||||
|
|
||||||
CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml)
|
CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml)
|
||||||
|
|
||||||
CXG_AUTH_TYPE := $(if $(CXG_AUTH_TYPE),$(CXG_AUTH_TYPE),"test")
|
|
||||||
|
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
@@ -40,7 +39,7 @@ smoke-test:
|
|||||||
start_server_and_test \
|
start_server_and_test \
|
||||||
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
||||||
$(CXG_SERVER_PORT) \
|
$(CXG_SERVER_PORT) \
|
||||||
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE=$(CXG_AUTH_TYPE) npm run e2e -- --verbose false'
|
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false'
|
||||||
|
|
||||||
# start an instance of cellxgene and run the end-to-end annotations tests
|
# start an instance of cellxgene and run the end-to-end annotations tests
|
||||||
.PHONY: smoke-test-annotations
|
.PHONY: smoke-test-annotations
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ import {
|
|||||||
getTestClass,
|
getTestClass,
|
||||||
getTestId,
|
getTestId,
|
||||||
isElementPresent,
|
isElementPresent,
|
||||||
goToPage,
|
|
||||||
} from "./puppeteerUtils";
|
} from "./puppeteerUtils";
|
||||||
|
|
||||||
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
|
|
||||||
|
|
||||||
export async function drag(testId, start, end, lasso = false) {
|
export async function drag(testId, start, end, lasso = false) {
|
||||||
const layout = await waitByID(testId);
|
const layout = await waitByID(testId);
|
||||||
const elBox = await layout.boxModel();
|
const elBox = await layout.boxModel();
|
||||||
@@ -61,7 +58,9 @@ export async function getAllHistograms(testclass, testIds) {
|
|||||||
const allHistograms = await getAllByClass(testclass);
|
const allHistograms = await getAllByClass(testclass);
|
||||||
|
|
||||||
const testIDs = await Promise.all(
|
const testIDs = await Promise.all(
|
||||||
allHistograms.map((hist) => page.evaluate((elem) => elem.dataset.testid, hist))
|
allHistograms.map((hist) =>
|
||||||
|
page.evaluate((elem) => elem.dataset.testid, hist)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return testIDs.map((id) => id.replace(/^histogram-/, ""));
|
return testIDs.map((id) => id.replace(/^histogram-/, ""));
|
||||||
@@ -172,9 +171,9 @@ export async function createCategory(categoryName) {
|
|||||||
await clickOn("submit-category");
|
await clickOn("submit-category");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
GENESET
|
GENESET
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -189,7 +188,9 @@ export async function colorByGene(gene) {
|
|||||||
export async function assertColorLegendLabel(label) {
|
export async function assertColorLegendLabel(label) {
|
||||||
const handle = await waitByID("continuous_legend_color_by_label");
|
const handle = await waitByID("continuous_legend_color_by_label");
|
||||||
|
|
||||||
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
const result = await handle.evaluate((node) =>
|
||||||
|
node.getAttribute("aria-label")
|
||||||
|
);
|
||||||
|
|
||||||
return expect(result).toBe(label);
|
return expect(result).toBe(label);
|
||||||
}
|
}
|
||||||
@@ -246,12 +247,14 @@ export async function assertGenesetDoesNotExist(genesetName) {
|
|||||||
export async function assertGenesetExists(genesetName) {
|
export async function assertGenesetExists(genesetName) {
|
||||||
const handle = await waitByID(`${genesetName}:geneset-name`);
|
const handle = await waitByID(`${genesetName}:geneset-name`);
|
||||||
|
|
||||||
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
const result = await handle.evaluate((node) =>
|
||||||
|
node.getAttribute("aria-label")
|
||||||
|
);
|
||||||
|
|
||||||
return expect(result).toBe(genesetName);
|
return expect(result).toBe(genesetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
GENE
|
GENE
|
||||||
|
|
||||||
@@ -276,7 +279,9 @@ export async function removeGene(geneSymbol) {
|
|||||||
export async function assertGeneExistsInGeneset(geneSymbol) {
|
export async function assertGeneExistsInGeneset(geneSymbol) {
|
||||||
const handle = await waitByID(`${geneSymbol}:gene-label`);
|
const handle = await waitByID(`${geneSymbol}:gene-label`);
|
||||||
|
|
||||||
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
const result = await handle.evaluate((node) =>
|
||||||
|
node.getAttribute("aria-label")
|
||||||
|
);
|
||||||
|
|
||||||
return expect(result).toBe(geneSymbol);
|
return expect(result).toBe(geneSymbol);
|
||||||
}
|
}
|
||||||
@@ -291,9 +296,9 @@ export async function expandGene(geneSymbol) {
|
|||||||
await clickOn(`maximize-${geneSymbol}`);
|
await clickOn(`maximize-${geneSymbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
CATEGORY
|
CATEGORY
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -437,70 +442,4 @@ export async function assertCategoryDoesNotExist(categoryName) {
|
|||||||
await expect(result).toBe(false);
|
await expect(result).toBe(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login() {
|
|
||||||
await goToPage(appUrlBase);
|
|
||||||
|
|
||||||
await clickOn("log-in");
|
|
||||||
|
|
||||||
// (thuang): Auth0 form is unstable and unsafe for input until verified
|
|
||||||
await waitUntilFormFieldStable('[name="email"]');
|
|
||||||
|
|
||||||
await expect(page).toFillForm("form", {
|
|
||||||
email: TEST_EMAIL,
|
|
||||||
password: TEST_PASSWORD,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
|
||||||
expect(page).toClick('[name="submit"]'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(page.url()).toContain(appUrlBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
await clickOnUntil("user-info", async () => {
|
|
||||||
await waitByID("log-out");
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
|
||||||
clickOn("log-out"),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitByID("log-in");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitUntilFormFieldStable(selector) {
|
|
||||||
const MAX_RETRY = 10;
|
|
||||||
const WAIT_FOR_MS = 200;
|
|
||||||
|
|
||||||
const EXPECTED_VALUE = "aaa";
|
|
||||||
|
|
||||||
let retry = 0;
|
|
||||||
|
|
||||||
while (retry < MAX_RETRY) {
|
|
||||||
try {
|
|
||||||
await expect(page).toFill(selector, EXPECTED_VALUE);
|
|
||||||
|
|
||||||
const fieldHandle = await expect(page).toMatchElement(selector);
|
|
||||||
|
|
||||||
const fieldValue = await page.evaluate(
|
|
||||||
(input) => input.value,
|
|
||||||
fieldHandle
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(fieldValue).toBe(EXPECTED_VALUE);
|
|
||||||
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
retry += 1;
|
|
||||||
|
|
||||||
await page.waitForTimeout(WAIT_FOR_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry === MAX_RETRY) {
|
|
||||||
throw Error("clickOnUntil() assertion failed!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ server:
|
|||||||
# none: No authentication support, features like user_annotations must not be enabled.
|
# none: No authentication support, features like user_annotations must not be enabled.
|
||||||
# session: A session based userid is automatically generated. (no params needed)
|
# session: A session based userid is automatically generated. (no params needed)
|
||||||
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
|
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
|
||||||
type: test
|
type: session
|
||||||
insecure_test_environment: true
|
insecure_test_environment: true
|
||||||
|
|
||||||
dataset:
|
dataset:
|
||||||
|
|||||||
@@ -1,26 +1,5 @@
|
|||||||
const {
|
|
||||||
SecretsManagerClient,
|
|
||||||
GetSecretValueCommand,
|
|
||||||
} = require("@aws-sdk/client-secrets-manager");
|
|
||||||
|
|
||||||
const { setup } = require("jest-environment-puppeteer");
|
const { setup } = require("jest-environment-puppeteer");
|
||||||
|
|
||||||
const client = new SecretsManagerClient({ region: "us-west-2" });
|
|
||||||
|
|
||||||
const deploymentStage = process.env.DEPLOYMENT_STAGE || "test";
|
|
||||||
|
|
||||||
const secretValueRequest = {
|
|
||||||
SecretId: `corpora/backend/${deploymentStage}/auth0-secret`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = new GetSecretValueCommand(secretValueRequest);
|
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
await setup();
|
await setup();
|
||||||
try {
|
|
||||||
const secret = JSON.parse((await client.send(command)).SecretString);
|
|
||||||
process.env.TEST_ACCOUNT_PASS = secret.test_account_password;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+12
-1447
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,6 @@
|
|||||||
"not Safari > 0"
|
"not Safari > 0"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.13.0",
|
|
||||||
"@babel/eslint-parser": "^7.15.0",
|
"@babel/eslint-parser": "^7.15.0",
|
||||||
"@blueprintjs/core": "^3.44.0",
|
"@blueprintjs/core": "^3.44.0",
|
||||||
"@blueprintjs/icons": "^3.19.0",
|
"@blueprintjs/icons": "^3.19.0",
|
||||||
|
|||||||
@@ -39,14 +39,6 @@ app.use(
|
|||||||
|
|
||||||
app.use(favicon("./favicon.png"));
|
app.use(favicon("./favicon.png"));
|
||||||
|
|
||||||
app.get("/login", async (req, res) => {
|
|
||||||
try {
|
|
||||||
res.redirect(`${API.prefix}login?dataset=http://localhost:${CLIENT_PORT}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/logout", async (req, res) => {
|
app.get("/logout", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`);
|
res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`);
|
||||||
|
|||||||
@@ -52,17 +52,6 @@ async function configFetch(dispatch) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function userInfoFetch(dispatch) {
|
|
||||||
return fetchJson("userinfo").then((response) => {
|
|
||||||
const { userinfo: userInfo } = response || {};
|
|
||||||
dispatch({
|
|
||||||
type: "userInfo load complete",
|
|
||||||
userInfo,
|
|
||||||
});
|
|
||||||
return userInfo;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function genesetsFetch(dispatch, config) {
|
async function genesetsFetch(dispatch, config) {
|
||||||
/* request genesets ONLY if the backend supports the feature */
|
/* request genesets ONLY if the backend supports the feature */
|
||||||
const defaultResponse = {
|
const defaultResponse = {
|
||||||
@@ -105,7 +94,6 @@ const doInitialDataLoad = () =>
|
|||||||
configFetch(dispatch),
|
configFetch(dispatch),
|
||||||
schemaFetch(dispatch),
|
schemaFetch(dispatch),
|
||||||
userColorsFetchAndLoad(dispatch),
|
userColorsFetchAndLoad(dispatch),
|
||||||
userInfoFetch(dispatch),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
genesetsFetch(dispatch, config);
|
genesetsFetch(dispatch, config);
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
auth: state.config?.authentication,
|
|
||||||
userInfo: state.userInfo,
|
|
||||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
writableGenesetsEnabled: !(
|
writableGenesetsEnabled: !(
|
||||||
state.config?.parameters?.annotations_genesets_readonly ?? true
|
state.config?.parameters?.annotations_genesets_readonly ?? true
|
||||||
@@ -101,15 +99,13 @@ class FilenameDialog extends React.Component {
|
|||||||
writableGenesetsEnabled,
|
writableGenesetsEnabled,
|
||||||
annotations,
|
annotations,
|
||||||
idhash,
|
idhash,
|
||||||
userInfo,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { filenameText } = this.state;
|
const { filenameText } = this.state;
|
||||||
|
|
||||||
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
|
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
|
||||||
annotations.promptForFilename &&
|
annotations.promptForFilename &&
|
||||||
!annotations.dataCollectionNameIsReadOnly &&
|
!annotations.dataCollectionNameIsReadOnly &&
|
||||||
!annotations.dataCollectionName &&
|
!annotations.dataCollectionName ? (
|
||||||
userInfo.is_authenticated ? (
|
|
||||||
<Dialog
|
<Dialog
|
||||||
icon="tag"
|
icon="tag"
|
||||||
title="User Generated Data Directory"
|
title="User Generated Data Directory"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import actions from "../../actions";
|
|||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
schema: state.annoMatrix?.schema,
|
schema: state.annoMatrix?.schema,
|
||||||
userInfo: state.userInfo,
|
|
||||||
}))
|
}))
|
||||||
class Categories extends React.Component {
|
class Categories extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -98,11 +97,8 @@ class Categories extends React.Component {
|
|||||||
this.setState({ newCategoryText: name });
|
this.setState({ newCategoryText: name });
|
||||||
};
|
};
|
||||||
|
|
||||||
instruction = (name) => labelPrompt(
|
instruction = (name) =>
|
||||||
this.categoryNameError(name),
|
labelPrompt(this.categoryNameError(name), "New, unique category name", ":");
|
||||||
"New, unique category name",
|
|
||||||
":"
|
|
||||||
);
|
|
||||||
|
|
||||||
onExpansionChange = (catName) => {
|
onExpansionChange = (catName) => {
|
||||||
const { expandedCats } = this.state;
|
const { expandedCats } = this.state;
|
||||||
@@ -124,7 +120,7 @@ class Categories extends React.Component {
|
|||||||
newCategoryText,
|
newCategoryText,
|
||||||
expandedCats,
|
expandedCats,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { writableCategoriesEnabled, schema, userInfo } = this.props;
|
const { writableCategoriesEnabled, schema } = this.props;
|
||||||
/* all names, sorted in display order. Will be rendered in this order */
|
/* all names, sorted in display order. Will be rendered in this order */
|
||||||
const allCategoryNames =
|
const allCategoryNames =
|
||||||
ControlsHelpers.selectableCategoryNames(schema).sort();
|
ControlsHelpers.selectableCategoryNames(schema).sort();
|
||||||
@@ -174,11 +170,7 @@ class Categories extends React.Component {
|
|||||||
{writableCategoriesEnabled ? (
|
{writableCategoriesEnabled ? (
|
||||||
<div style={{ marginBottom: 10 }}>
|
<div style={{ marginBottom: 10 }}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content="Create a new category"
|
||||||
userInfo.is_authenticated
|
|
||||||
? "Create a new category"
|
|
||||||
: "You must be logged in to create new categorical fields"
|
|
||||||
}
|
|
||||||
position={Position.RIGHT}
|
position={Position.RIGHT}
|
||||||
boundary="viewport"
|
boundary="viewport"
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
@@ -192,7 +184,6 @@ class Categories extends React.Component {
|
|||||||
data-testid="open-annotation-dialog"
|
data-testid="open-annotation-dialog"
|
||||||
onClick={this.handleEnableAnnoMode}
|
onClick={this.handleEnableAnnoMode}
|
||||||
intent="primary"
|
intent="primary"
|
||||||
disabled={!userInfo.is_authenticated}
|
|
||||||
>
|
>
|
||||||
Create new <strong>category</strong>
|
Create new <strong>category</strong>
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
AnchorButton,
|
|
||||||
Button,
|
|
||||||
MenuItem,
|
|
||||||
Tooltip,
|
|
||||||
Popover,
|
|
||||||
Menu,
|
|
||||||
Elevation,
|
|
||||||
PopoverPosition,
|
|
||||||
Checkbox,
|
|
||||||
Card,
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
|
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
import styles from "./menubar.css";
|
|
||||||
|
|
||||||
import { storageGet, storageSet, KEYS } from "../util/localStorage";
|
|
||||||
|
|
||||||
const BASE_EMOJI = [0x1f9d1, 0x1f468, 0x1f469];
|
|
||||||
const SKIN_TONES = [0x1f3fb, 0x1f3fc, 0x1f3fd, 0x1f3fe, 0x1f3ff];
|
|
||||||
const MICROSCOPE = 0x1f52c;
|
|
||||||
const ZERO_WIDTH_JOINER = 0x0200d;
|
|
||||||
|
|
||||||
const LOGIN_PROMPT_OFF = "off";
|
|
||||||
|
|
||||||
const Auth = React.memo((props) => {
|
|
||||||
const [isPromptOpen, setIsPromptOpen] = useState(shouldShowPrompt());
|
|
||||||
|
|
||||||
const { auth, userInfo } = props;
|
|
||||||
|
|
||||||
const isAuthenticated = userInfo && userInfo.is_authenticated;
|
|
||||||
|
|
||||||
window.userInfo = userInfo;
|
|
||||||
|
|
||||||
const randomInt = Math.random() * 15;
|
|
||||||
const sexIndex = Math.floor(randomInt / 5);
|
|
||||||
const skinToneIndex = Math.floor(randomInt % 5);
|
|
||||||
|
|
||||||
const scientist = String.fromCodePoint(
|
|
||||||
BASE_EMOJI[sexIndex],
|
|
||||||
SKIN_TONES[skinToneIndex],
|
|
||||||
ZERO_WIDTH_JOINER,
|
|
||||||
MICROSCOPE
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldShowAuth()) return null;
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
const PopoverContent = (
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
data-testid="user-email"
|
|
||||||
text={`Logged in as: ${userInfo.email}`}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
data-testid="log-out"
|
|
||||||
text="Log Out"
|
|
||||||
href={auth.logout}
|
|
||||||
icon={IconNames.LOG_OUT}
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover content={PopoverContent}>
|
|
||||||
<Button
|
|
||||||
data-testid="user-info"
|
|
||||||
className={styles.menubarButton}
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line no-constant-condition -- disable profile picture until CSP is tweaked */}
|
|
||||||
{userInfo?.picture && false ? (
|
|
||||||
<img alt="profile" size="21px" src={userInfo?.picture} />
|
|
||||||
) : (
|
|
||||||
<span style={{ fontSize: "18px" }}>{scientist}</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoginButton = (
|
|
||||||
<Tooltip
|
|
||||||
content="Log in to cellxgene"
|
|
||||||
position="bottom"
|
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
data-testid="log-in"
|
|
||||||
href={auth.login}
|
|
||||||
className={styles.menubarButton}
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isPromptOpen) {
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
position={PopoverPosition.AUTO_END}
|
|
||||||
isOpen
|
|
||||||
content={<PromptContent setIsPromptOpen={setIsPromptOpen} />}
|
|
||||||
onInteraction={setIsPromptOpen}
|
|
||||||
>
|
|
||||||
{LoginButton}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoginButton;
|
|
||||||
|
|
||||||
function shouldShowAuth() {
|
|
||||||
return auth && auth.requires_client_login;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldShowPrompt() {
|
|
||||||
if (storageGet(KEYS.LOGIN_PROMPT) === LOGIN_PROMPT_OFF) return false;
|
|
||||||
|
|
||||||
return shouldShowAuth && !isAuthenticated;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function PromptContent({ setIsPromptOpen }) {
|
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
|
||||||
|
|
||||||
function handleOKClick() {
|
|
||||||
if (isChecked) {
|
|
||||||
storageSet(KEYS.LOGIN_PROMPT, LOGIN_PROMPT_OFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPromptOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCheckboxChange() {
|
|
||||||
setIsChecked(!isChecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card style={{ width: "500px" }} elevation={Elevation.TWO}>
|
|
||||||
<p>
|
|
||||||
Logging in will enable you to create your own categories and labels.
|
|
||||||
Logging in later will reset cellxgene to the default view and cause you
|
|
||||||
to lose progress.
|
|
||||||
</p>
|
|
||||||
<Checkbox
|
|
||||||
style={{ width: "230px" }}
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={handleCheckboxChange}
|
|
||||||
data-testid="login-hint-do-not-show-again"
|
|
||||||
>
|
|
||||||
Do not show me this message again
|
|
||||||
</Checkbox>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleOKClick}
|
|
||||||
intent="primary"
|
|
||||||
data-testid="login-hint-yes"
|
|
||||||
>
|
|
||||||
Acknowledge
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Auth;
|
|
||||||
@@ -7,7 +7,6 @@ import styles from "./menubar.css";
|
|||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import Clip from "./clip";
|
import Clip from "./clip";
|
||||||
|
|
||||||
import AuthButtons from "./authButtons";
|
|
||||||
import Subset from "./subset";
|
import Subset from "./subset";
|
||||||
import UndoRedoReset from "./undoRedo";
|
import UndoRedoReset from "./undoRedo";
|
||||||
import DiffexpButtons from "./diffexpButtons";
|
import DiffexpButtons from "./diffexpButtons";
|
||||||
@@ -36,8 +35,6 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
|
|||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||||
libraryVersions: state.config?.library_versions,
|
libraryVersions: state.config?.library_versions,
|
||||||
auth: state.config?.authentication,
|
|
||||||
userInfo: state.userInfo,
|
|
||||||
undoDisabled: state["@@undoable/past"].length === 0,
|
undoDisabled: state["@@undoable/past"].length === 0,
|
||||||
redoDisabled: state["@@undoable/future"].length === 0,
|
redoDisabled: state["@@undoable/future"].length === 0,
|
||||||
aboutLink: state.config?.links?.["about-dataset"],
|
aboutLink: state.config?.links?.["about-dataset"],
|
||||||
@@ -209,8 +206,6 @@ class MenuBar extends React.PureComponent {
|
|||||||
colorAccessor,
|
colorAccessor,
|
||||||
subsetPossible,
|
subsetPossible,
|
||||||
subsetResetPossible,
|
subsetResetPossible,
|
||||||
userInfo,
|
|
||||||
auth,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { pendingClipPercentiles } = this.state;
|
const { pendingClipPercentiles } = this.state;
|
||||||
|
|
||||||
@@ -236,7 +231,6 @@ class MenuBar extends React.PureComponent {
|
|||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AuthButtons {...{ auth, userInfo }} />
|
|
||||||
<UndoRedoReset
|
<UndoRedoReset
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
undoDisabled={undoDisabled}
|
undoDisabled={undoDisabled}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
COOKIE_DECISION: "cxg.cookieDecision",
|
COOKIE_DECISION: "cxg.cookieDecision",
|
||||||
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function storageGet(key, defaultValue = null) {
|
export function storageGet(key, defaultValue = null) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import thunk from "redux-thunk";
|
|||||||
import cascadeReducers from "./cascade";
|
import cascadeReducers from "./cascade";
|
||||||
import undoable from "./undoable";
|
import undoable from "./undoable";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import userInfo from "./userInfo";
|
|
||||||
import annoMatrix from "./annoMatrix";
|
import annoMatrix from "./annoMatrix";
|
||||||
import obsCrossfilter from "./obsCrossfilter";
|
import obsCrossfilter from "./obsCrossfilter";
|
||||||
import categoricalSelection from "./categoricalSelection";
|
import categoricalSelection from "./categoricalSelection";
|
||||||
@@ -42,7 +41,6 @@ const Reducer = undoable(
|
|||||||
["centroidLabels", centroidLabels],
|
["centroidLabels", centroidLabels],
|
||||||
["pointDilation", pointDialation],
|
["pointDilation", pointDialation],
|
||||||
["autosave", autosave],
|
["autosave", autosave],
|
||||||
["userInfo", userInfo],
|
|
||||||
]),
|
]),
|
||||||
[
|
[
|
||||||
"annoMatrix",
|
"annoMatrix",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
const UserInfo = (state = {}, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load start":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
case "userInfo load complete":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
...action.userInfo,
|
|
||||||
};
|
|
||||||
case "initial data load error":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: action.error,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserInfo;
|
|
||||||
Reference in New Issue
Block a user