mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-22 17:08: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_AUTH_TYPE := $(if $(CXG_AUTH_TYPE),$(CXG_AUTH_TYPE),"test")
|
||||
|
||||
|
||||
# Packaging
|
||||
@@ -40,7 +39,7 @@ smoke-test:
|
||||
start_server_and_test \
|
||||
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
||||
$(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
|
||||
.PHONY: smoke-test-annotations
|
||||
|
||||
@@ -13,11 +13,8 @@ import {
|
||||
getTestClass,
|
||||
getTestId,
|
||||
isElementPresent,
|
||||
goToPage,
|
||||
} from "./puppeteerUtils";
|
||||
|
||||
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
|
||||
|
||||
export async function drag(testId, start, end, lasso = false) {
|
||||
const layout = await waitByID(testId);
|
||||
const elBox = await layout.boxModel();
|
||||
@@ -61,7 +58,9 @@ export async function getAllHistograms(testclass, testIds) {
|
||||
const allHistograms = await getAllByClass(testclass);
|
||||
|
||||
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-/, ""));
|
||||
@@ -172,9 +171,9 @@ export async function createCategory(categoryName) {
|
||||
await clickOn("submit-category");
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
|
||||
GENESET
|
||||
GENESET
|
||||
|
||||
*/
|
||||
|
||||
@@ -189,7 +188,9 @@ export async function colorByGene(gene) {
|
||||
export async function assertColorLegendLabel(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);
|
||||
}
|
||||
@@ -246,12 +247,14 @@ export async function assertGenesetDoesNotExist(genesetName) {
|
||||
export async function assertGenesetExists(genesetName) {
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
|
||||
GENE
|
||||
|
||||
@@ -276,7 +279,9 @@ export async function removeGene(geneSymbol) {
|
||||
export async function assertGeneExistsInGeneset(geneSymbol) {
|
||||
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);
|
||||
}
|
||||
@@ -291,9 +296,9 @@ export async function expandGene(geneSymbol) {
|
||||
await clickOn(`maximize-${geneSymbol}`);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
|
||||
CATEGORY
|
||||
CATEGORY
|
||||
|
||||
*/
|
||||
|
||||
@@ -437,70 +442,4 @@ export async function assertCategoryDoesNotExist(categoryName) {
|
||||
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 */
|
||||
|
||||
@@ -8,7 +8,7 @@ server:
|
||||
# none: No authentication support, features like user_annotations must not be enabled.
|
||||
# session: A session based userid is automatically generated. (no params needed)
|
||||
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
|
||||
type: test
|
||||
type: session
|
||||
insecure_test_environment: true
|
||||
|
||||
dataset:
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
const {
|
||||
SecretsManagerClient,
|
||||
GetSecretValueCommand,
|
||||
} = require("@aws-sdk/client-secrets-manager");
|
||||
|
||||
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 () => {
|
||||
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"
|
||||
],
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.13.0",
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@blueprintjs/core": "^3.44.0",
|
||||
"@blueprintjs/icons": "^3.19.0",
|
||||
|
||||
@@ -39,14 +39,6 @@ app.use(
|
||||
|
||||
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) => {
|
||||
try {
|
||||
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) {
|
||||
/* request genesets ONLY if the backend supports the feature */
|
||||
const defaultResponse = {
|
||||
@@ -105,7 +94,6 @@ const doInitialDataLoad = () =>
|
||||
configFetch(dispatch),
|
||||
schemaFetch(dispatch),
|
||||
userColorsFetchAndLoad(dispatch),
|
||||
userInfoFetch(dispatch),
|
||||
]);
|
||||
|
||||
genesetsFetch(dispatch, config);
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
@connect((state) => ({
|
||||
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
||||
annotations: state.annotations,
|
||||
auth: state.config?.authentication,
|
||||
userInfo: state.userInfo,
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
writableGenesetsEnabled: !(
|
||||
state.config?.parameters?.annotations_genesets_readonly ?? true
|
||||
@@ -101,15 +99,13 @@ class FilenameDialog extends React.Component {
|
||||
writableGenesetsEnabled,
|
||||
annotations,
|
||||
idhash,
|
||||
userInfo,
|
||||
} = this.props;
|
||||
const { filenameText } = this.state;
|
||||
|
||||
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
|
||||
annotations.promptForFilename &&
|
||||
!annotations.dataCollectionNameIsReadOnly &&
|
||||
!annotations.dataCollectionName &&
|
||||
userInfo.is_authenticated ? (
|
||||
!annotations.dataCollectionName ? (
|
||||
<Dialog
|
||||
icon="tag"
|
||||
title="User Generated Data Directory"
|
||||
|
||||
@@ -13,7 +13,6 @@ import actions from "../../actions";
|
||||
@connect((state) => ({
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
schema: state.annoMatrix?.schema,
|
||||
userInfo: state.userInfo,
|
||||
}))
|
||||
class Categories extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -98,11 +97,8 @@ class Categories extends React.Component {
|
||||
this.setState({ newCategoryText: name });
|
||||
};
|
||||
|
||||
instruction = (name) => labelPrompt(
|
||||
this.categoryNameError(name),
|
||||
"New, unique category name",
|
||||
":"
|
||||
);
|
||||
instruction = (name) =>
|
||||
labelPrompt(this.categoryNameError(name), "New, unique category name", ":");
|
||||
|
||||
onExpansionChange = (catName) => {
|
||||
const { expandedCats } = this.state;
|
||||
@@ -124,7 +120,7 @@ class Categories extends React.Component {
|
||||
newCategoryText,
|
||||
expandedCats,
|
||||
} = 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 */
|
||||
const allCategoryNames =
|
||||
ControlsHelpers.selectableCategoryNames(schema).sort();
|
||||
@@ -174,11 +170,7 @@ class Categories extends React.Component {
|
||||
{writableCategoriesEnabled ? (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Tooltip
|
||||
content={
|
||||
userInfo.is_authenticated
|
||||
? "Create a new category"
|
||||
: "You must be logged in to create new categorical fields"
|
||||
}
|
||||
content="Create a new category"
|
||||
position={Position.RIGHT}
|
||||
boundary="viewport"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
@@ -192,7 +184,6 @@ class Categories extends React.Component {
|
||||
data-testid="open-annotation-dialog"
|
||||
onClick={this.handleEnableAnnoMode}
|
||||
intent="primary"
|
||||
disabled={!userInfo.is_authenticated}
|
||||
>
|
||||
Create new <strong>category</strong>
|
||||
</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 Clip from "./clip";
|
||||
|
||||
import AuthButtons from "./authButtons";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
@@ -36,8 +35,6 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
libraryVersions: state.config?.library_versions,
|
||||
auth: state.config?.authentication,
|
||||
userInfo: state.userInfo,
|
||||
undoDisabled: state["@@undoable/past"].length === 0,
|
||||
redoDisabled: state["@@undoable/future"].length === 0,
|
||||
aboutLink: state.config?.links?.["about-dataset"],
|
||||
@@ -209,8 +206,6 @@ class MenuBar extends React.PureComponent {
|
||||
colorAccessor,
|
||||
subsetPossible,
|
||||
subsetResetPossible,
|
||||
userInfo,
|
||||
auth,
|
||||
} = this.props;
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
|
||||
@@ -236,7 +231,6 @@ class MenuBar extends React.PureComponent {
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons {...{ auth, userInfo }} />
|
||||
<UndoRedoReset
|
||||
dispatch={dispatch}
|
||||
undoDisabled={undoDisabled}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const KEYS = {
|
||||
COOKIE_DECISION: "cxg.cookieDecision",
|
||||
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
|
||||
};
|
||||
|
||||
export function storageGet(key, defaultValue = null) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import thunk from "redux-thunk";
|
||||
import cascadeReducers from "./cascade";
|
||||
import undoable from "./undoable";
|
||||
import config from "./config";
|
||||
import userInfo from "./userInfo";
|
||||
import annoMatrix from "./annoMatrix";
|
||||
import obsCrossfilter from "./obsCrossfilter";
|
||||
import categoricalSelection from "./categoricalSelection";
|
||||
@@ -42,7 +41,6 @@ const Reducer = undoable(
|
||||
["centroidLabels", centroidLabels],
|
||||
["pointDilation", pointDialation],
|
||||
["autosave", autosave],
|
||||
["userInfo", userInfo],
|
||||
]),
|
||||
[
|
||||
"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