diff --git a/Makefile b/Makefile index ca8754f7..8f9a4083 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ lint: lint-server lint-client .PHONY: lint-server lint-server: fmt-py - flake8 server --per-file-ignores='server/test/fixtures/dataset_config_outline.py:F821 server/test/fixtures/server_config_outline.py:F821' + flake8 server --per-file-ignores='server/test/fixtures/dataset_config_outline.py:F821 server/test/fixtures/server_config_outline.py:F821 server/test/performance/scale_test_annotations.py:E501' .PHONY: lint-client diff --git a/client/Makefile b/client/Makefile index 3b36337a..1a8d3a81 100644 --- a/client/Makefile +++ b/client/Makefile @@ -3,6 +3,8 @@ include ../common.mk ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/fixtures/pbmc3k-annotations.csv) ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS)) +CXG_CONFIG := $(if $(CXG_CONFIG), $(CXG_CONFIG), ./__tests__/e2e/test_config.yaml) + # Packaging .PHONY: clean clean: @@ -31,9 +33,9 @@ start-frontend: .PHONY: smoke-test smoke-test: start_server_and_test \ - 'CXG_OPTIONS="--disable-annotations" $(MAKE) start-server' \ + 'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \ $(CXG_SERVER_PORT) \ - 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false' + 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE="test" npm run e2e -- --verbose false' # start an instance of cellxgene and run the end-to-end annotations tests .PHONY: smoke-test-annotations diff --git a/client/__tests__/e2e/cellxgeneActions.js b/client/__tests__/e2e/cellxgeneActions.js index 9fda69b1..7ffcc8cf 100644 --- a/client/__tests__/e2e/cellxgeneActions.js +++ b/client/__tests__/e2e/cellxgeneActions.js @@ -322,7 +322,7 @@ export async function login() { await goToPage(appUrlBase); - await clickOn("auth-button"); + await clickOn("log-in"); // (thuang): Auth0 form is unstable and unsafe for input until verified await waitUntilFormFieldStable('[name="email"]'); @@ -341,16 +341,15 @@ export async function login() { } export async function logout() { - await clickOnUntil("menu", async () => { - await expect(page).toMatch("Log Out"); - + await clickOnUntil("user-info", async () => { + await waitByID("log-out"); await Promise.all([ page.waitForNavigation({ waitUntil: "networkidle0" }), - expect(page).toClick("a", { text: "Log Out" }), + clickOn("log-out"), ]); }); - await expect(page).toMatch("Log In"); + await waitByID("log-in"); } async function waitUntilFormFieldStable(selector) { diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 05f52314..7e044c40 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -17,6 +17,7 @@ import { goToPage, typeInto, waitByID, + clickOnUntil, } from "./puppeteerUtils"; import { @@ -521,6 +522,22 @@ test("lasso moves after pan", async () => { expect(panCount).toBe(initialCount); }); +const describeIfCalledByMakeFileTarget = + process.env.CXG_AUTH_TYPE?.toLowerCase() === "test" + ? describe + : describe.skip; + +describeIfCalledByMakeFileTarget("auth buttons", () => { + test("login then logout", async () => { + await goToPage(appUrlBase); + await clickOnUntil("log-in", async () => { + await page.waitForNavigation({ waitUntil: "networkidle0" }); + await waitByID("user-info"); + }); + await logout(); + }); +}); + const conditionalDescribe = process.env.TEST_AUTH_INTEGRATION === "true" ? describe : describe.skip; diff --git a/client/__tests__/e2e/test_config.yaml b/client/__tests__/e2e/test_config.yaml new file mode 100644 index 00000000..11576878 --- /dev/null +++ b/client/__tests__/e2e/test_config.yaml @@ -0,0 +1,47 @@ +server: + app: + force_https: true + + # By default, cellxgene will serve api requests from the same base url as the webpage. + # In general api_base_url and web_base_url will not need to be set. + # There are two reasons to set these parameters: + # 1. Oauth authentication is used; the oauth server will redirect back to the api_base_url after login, + # which then redirects back to the web_base_url. If the web_base_url is not set, it will default to + # the api_base_url. If oauth authentication is used, the api_base_url must be set. + # For a local test (where the server runs on "http://localhost:"), then the api_base_url may be + # set to the string "local". + # 2. The cellxgene deploymnent is in an environment where the webpage and api have + # different base urls. In this case both api_base_url and web_base_url must be set. + # It is up to the server admin to ensure that the networking is setup correctly for this environment. + api_base_url: http://localhost:5005 + web_base_url: http://localhost:3000 + + authentication: + # The authentication types may be "none", "session", "oauth" + # 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 + +dataset: + app: + about_legal_tos: null + about_legal_privacy: null + + presentation: + max_categories: 1000 + custom_colors: true + + user_annotations: + enable: false + type: local_file_csv + local_file_csv: + directory: null + file: null + ontology: + enable: false + obo_location: null + + embeddings: + names: [] + enable_reembedding: false diff --git a/client/server/development.js b/client/server/development.js index 0b3e3811..1530856e 100644 --- a/client/server/development.js +++ b/client/server/development.js @@ -1,5 +1,3 @@ -const path = require("path"); -const historyApiFallback = require("connect-history-api-fallback"); const chalk = require("chalk"); const express = require("express"); const favicon = require("serve-favicon"); @@ -11,35 +9,51 @@ const utils = require("./utils"); process.env.NODE_ENV = "development"; const CLIENT_PORT = process.env.CXG_CLIENT_PORT; +const { CXG_SERVER_PORT } = process.env; + +const API = { + prefix: `http://localhost:${CXG_SERVER_PORT}/`, +}; // Set up compiler const compiler = webpack(config); -compiler.plugin("invalid", () => { +compiler.hooks.invalid.tap("invalid", () => { utils.clearConsole(); console.log("Compiling..."); }); -compiler.plugin("done", (stats) => { +compiler.hooks.done.tap("done", (stats) => { utils.formatStats(stats, CLIENT_PORT); }); // Launch server const app = express(); -app.use(historyApiFallback({ verbose: false })); - app.use( devMiddleware(compiler, { logLevel: "warn", publicPath: config.output.publicPath, + index: true, }) ); app.use(favicon("./favicon.png")); -app.get("*", (req, res) => { - res.sendFile(path.resolve("index.html")); +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}`); + } catch (err) { + console.error(err); + } }); app.listen(CLIENT_PORT, (err) => { diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 8730730e..a71149ee 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -43,12 +43,12 @@ async function configFetch(dispatch) { async function userInfoFetch(dispatch) { return fetchJson("userinfo").then((response) => { - const { userinfo } = response || {}; + const { userinfo: userInfo } = response || {}; dispatch({ - type: "userinfo load complete", - userinfo, + type: "userInfo load complete", + userInfo, }); - return userinfo; + return userInfo; }); } diff --git a/client/src/components/autosave/filenameDialog.js b/client/src/components/autosave/filenameDialog.js index 08fe1462..3ff7e500 100644 --- a/client/src/components/autosave/filenameDialog.js +++ b/client/src/components/autosave/filenameDialog.js @@ -15,7 +15,7 @@ import { idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null, annotations: state.annotations, auth: state.config?.authentication, - userinfo: state.userinfo, + userInfo: state.userInfo, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, })) class FilenameDialog extends React.Component { @@ -97,7 +97,7 @@ class FilenameDialog extends React.Component { writableCategoriesEnabled, annotations, idhash, - userinfo, + userInfo, } = this.props; const { filenameText } = this.state; @@ -105,7 +105,7 @@ class FilenameDialog extends React.Component { annotations.promptForFilename && !annotations.dataCollectionNameIsReadOnly && !annotations.dataCollectionName && - userinfo.is_authenticated ? ( + userInfo.is_authenticated ? ( { - const { annoMatrix } = this.props; + const { annoMatrix, field, dispatch, singleContinuousValues } = this.props; const { isClipped } = annoMatrix; - + if (singleContinuousValues.has(field)) { + return { + histogram: undefined, + range: undefined, + unclippedRange: undefined, + unclippedRangeColor: globals.blue, + isSingleValue: true, + OK2Render: false, + }; + } const query = this.createQuery(); const df = await annoMatrix.fetch(...query); const column = df.icol(0); - // if we are clipped, fetch both our value and our unclipped value, - // as we need the absolute min/max range, not just the clipped min/max. const summary = column.summarize(); const range = [summary.min, summary.max]; + if (summary.min === summary.max && !isClipped) { + dispatch({ + type: "add single continuous value", + field, + value: summary.min, + }); + return { + histogram: undefined, + range, + unclippedRange: range, + unclippedRangeColor: globals.blue, + isSingleValue: true, + OK2Render: false, + }; + } + + const isSingleValue = summary.min === summary.max; + // if we are clipped, fetch both our value and our unclipped value, + // as we need the absolute min/max range, not just the clipped min/max. let unclippedRange = [...range]; if (isClipped) { const parent = await annoMatrix.viewOf.fetch(...query); @@ -643,7 +670,6 @@ class HistogramBrush extends React.PureComponent { this.height ); - const isSingleValue = summary.min === summary.max; const nonFiniteExtent = summary.min === undefined || summary.max === undefined || diff --git a/client/src/components/categorical/index.js b/client/src/components/categorical/index.js index 63db6cac..9a267930 100644 --- a/client/src/components/categorical/index.js +++ b/client/src/components/categorical/index.js @@ -15,7 +15,7 @@ import actions from "../../actions"; writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, schema: state.annoMatrix?.schema, ontology: state.ontology, - userinfo: state.userinfo, + userInfo: state.userInfo, })) class Categories extends React.Component { constructor(props) { @@ -132,7 +132,7 @@ class Categories extends React.Component { writableCategoriesEnabled, schema, ontology, - userinfo, + userInfo, } = this.props; const ontologyEnabled = ontology?.enabled ?? false; /* all names, sorted in display order. Will be rendered in this order */ @@ -213,7 +213,7 @@ class Categories extends React.Component { {writableCategoriesEnabled ? ( Create new category diff --git a/client/src/components/continuous/continuous.js b/client/src/components/continuous/continuous.js index f1119507..ad5c908d 100644 --- a/client/src/components/continuous/continuous.js +++ b/client/src/components/continuous/continuous.js @@ -14,12 +14,7 @@ class Continuous extends React.PureComponent { if (!schema) return null; const obsIndex = schema.annotations.obs.index; const allContinuousNames = schema.annotations.obs.columns - .filter( - (col) => - col.type === "int32" || - col.type === "float32" || - col.type === "float64" - ) + .filter((col) => col.type === "int32" || col.type === "float32") .filter((col) => col.name !== obsIndex) .filter((col) => !col.writable) // skip user annotations - they will be treated as categorical .map((col) => col.name); diff --git a/client/src/components/infoDrawer/infoDrawer.js b/client/src/components/infoDrawer/infoDrawer.js index 57bfd77b..aad395bc 100644 --- a/client/src/components/infoDrawer/infoDrawer.js +++ b/client/src/components/infoDrawer/infoDrawer.js @@ -1,13 +1,8 @@ import React, { PureComponent } from "react"; import { connect, shallowEqual } from "react-redux"; import { Drawer } from "@blueprintjs/core"; -import Async from "react-async"; - import InfoFormat from "./infoFormat"; -import { - selectableCategoryNames, - createCategorySummaryFromDfCol, -} from "../../util/stateManager/controlsHelpers"; +import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers"; @connect((state) => { return { @@ -17,6 +12,7 @@ import { aboutURL: state.config?.links?.["about-dataset"], isOpen: state.controls.datasetDrawer, dataPortalProps: state.config?.["corpora_props"] ?? {}, + singleContinuousValues: state.singleContinuousValue.singleContinuousValues, }; }) class InfoDrawer extends PureComponent { @@ -24,40 +20,6 @@ class InfoDrawer extends PureComponent { 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) => { - // Actually check to see if it is null(user anno) - if (!categoryData) return acc; - 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; @@ -72,47 +34,37 @@ class InfoDrawer extends PureComponent { schema, isOpen, dataPortalProps, + singleContinuousValues, } = this.props; + const allCategoryNames = selectableCategoryNames(schema).sort(); + const allSingleValues = new Map(); + + allCategoryNames.forEach((catName) => { + const isUserAnno = schema?.annotations?.obsByName[catName]?.writable; + const colSchema = schema.annotations.obsByName[catName]; + if (!isUserAnno && colSchema.categories?.length === 1) { + allSingleValues.set(catName, colSchema.categories[0]); + } + }); + singleContinuousValues.forEach((value, catName) => { + allSingleValues.set(catName, value); + }); + return ( - - - - - - {(error) => { - console.error(error); - return Failed to load info; - }} - - - {(asyncProps) => { - const { singleValueCategories } = asyncProps; - return ( - - ); - }} - - + ); } diff --git a/client/src/components/infoDrawer/infoFormat.js b/client/src/components/infoDrawer/infoFormat.js index eacaa406..04de063b 100644 --- a/client/src/components/infoDrawer/infoFormat.js +++ b/client/src/components/infoDrawer/infoFormat.js @@ -1,15 +1,15 @@ -import { H3, H1, UL, Classes } from "@blueprintjs/core"; +import { H3, H1, UL } from "@blueprintjs/core"; import React from "react"; import Truncate from "../util/truncate"; -const renderContributors = (contributors, affiliations, skeleton) => { +const renderContributors = (contributors, affiliations) => { // eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII if (!contributors || contributors.length === 0 || true) return null; return ( <> -

Contributors

-

+

Contributors

+

{contributors.map((contributor) => { const { email, name, institution } = contributor; @@ -22,7 +22,7 @@ const renderContributors = (contributors, affiliations, skeleton) => { ); })}

- {renderAffiliations(affiliations, skeleton)} + {renderAffiliations(affiliations)} ); }; @@ -39,14 +39,14 @@ const buildAffiliations = (contributors = []) => { return affiliations; }; -const renderAffiliations = (affiliations, skeleton) => { +const renderAffiliations = (affiliations) => { if (affiliations.length === 0) return null; return ( <> -

Affiliations

+

Affiliations

    {affiliations.map((item, index) => ( -
    +
    {index + 1} {" "} {item} @@ -57,12 +57,12 @@ const renderAffiliations = (affiliations, skeleton) => { ); }; -const renderDOILink = (type, doi, skeleton) => { +const renderDOILink = (type, doi) => { if (!doi) return null; return ( <> -

    {type}

    -

    +

    {type}

    +

    {doi} @@ -71,12 +71,12 @@ const renderDOILink = (type, doi, skeleton) => { ); }; -const renderOrganism = (organism, skeleton) => { +const renderOrganism = (organism) => { if (!organism) return null; return ( <> -

    Organism

    -

    {organism}

    +

    Organism

    +

    {organism}

    ); }; @@ -85,13 +85,13 @@ const ONTOLOGY_KEY = "ontology_term_id"; const CAT_WIDTH = "30%"; const VAL_WIDTH = "35%"; // Render list of metadata attributes found in categorical field -const renderSingleValueCategories = (singleValueCategories, skeleton) => { - if (singleValueCategories.size === 0) return null; +const renderSingleValues = (singleValues) => { + if (singleValues.size === 0) return null; return ( <> -

    Dataset Metadata

    +

    Dataset Metadata

      - {Array.from(singleValueCategories).reduce((elems, pair) => { + {Array.from(singleValues).reduce((elems, pair) => { const [category, value] = pair; // If the value is empty skip it if (!value) return elems; @@ -115,11 +115,7 @@ const renderSingleValueCategories = (singleValueCategories, skeleton) => { } else { // Create the list item elems.push( -
    • +
    • {`${category}:`} @@ -138,20 +134,17 @@ const renderSingleValueCategories = (singleValueCategories, skeleton) => { // Renders any links found in the config where link_type is not "SUMMARY" // If there are no links in the config, render the aboutURL -const renderLinks = (projectLinks, aboutURL, skeleton) => { +const renderLinks = (projectLinks, aboutURL) => { if (!projectLinks && !aboutURL) return null; if (projectLinks) return ( <> -

      Project Links

      +

      Project Links

        {projectLinks.map((link) => { if (link.link_type === "SUMMARY") return null; return ( -
      • +
      • {link.link_name} @@ -164,14 +157,9 @@ const renderLinks = (projectLinks, aboutURL, skeleton) => { return ( <> -

        More Info

        +

        More Info

        - + {aboutURL}

        @@ -179,24 +167,9 @@ const renderLinks = (projectLinks, aboutURL, skeleton) => { ); }; -const NUM_CATEGORIES = 8; - -// Generates arbitrary placeholder array for singleValueCategories skeleton shape -const singleValueCategoriesPlaceholder = Array.from(Array(NUM_CATEGORIES)).map( - (_, index) => { - return [index, index]; - } -); - const InfoFormat = React.memo( - ({ - datasetTitle, - singleValueCategories = new Map(singleValueCategoriesPlaceholder), - aboutURL = "thisisabouthtelengthofaurl", - dataPortalProps = {}, - skeleton = false, - }) => { - if (dataPortalProps.corpora_schema_version === "1.0.0") { + ({ datasetTitle, allSingleValues, aboutURL, dataPortalProps = {} }) => { + if (dataPortalProps.version?.["corpora_schema_version"] !== "1.0.0") { dataPortalProps = {}; } const { @@ -212,15 +185,13 @@ const InfoFormat = React.memo( return (
        -

        - {title ?? datasetTitle} -

        - {renderContributors(contributors, affiliations, skeleton)} - {renderDOILink("DOI", doi, skeleton)} - {renderDOILink("Preprint DOI", preprintDOI, skeleton)} - {renderOrganism(organism, skeleton)} - {renderSingleValueCategories(singleValueCategories, skeleton)} - {renderLinks(projectLinks, aboutURL, skeleton)} +

        {title ?? datasetTitle}

        + {renderContributors(contributors, affiliations)} + {renderDOILink("DOI", doi)} + {renderDOILink("Preprint DOI", preprintDOI)} + {renderOrganism(organism)} + {renderSingleValues(allSingleValues)} + {renderLinks(projectLinks, aboutURL)}
        ); } diff --git a/client/src/components/leftSidebar/infoMenu.js b/client/src/components/leftSidebar/infoMenu.js new file mode 100644 index 00000000..6ab9bdde --- /dev/null +++ b/client/src/components/leftSidebar/infoMenu.js @@ -0,0 +1,72 @@ +// jshint esversion: 6 +import React from "react"; +import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; + +const InformationMenu = React.memo((props) => { + const { libraryVersions, tosURL, privacyURL } = props; + return ( + + + + + + + {tosURL && ( + + )} + {privacyURL && ( + + )} + + } + position={Position.BOTTOM_RIGHT} + modifiers={{ + preventOverflow: { enabled: false }, + hide: { enabled: false }, + }} + > +
    -
    +
    ); diff --git a/client/src/components/menubar/authButtons.js b/client/src/components/menubar/authButtons.js index 985b3da3..91af0b42 100644 --- a/client/src/components/menubar/authButtons.js +++ b/client/src/components/menubar/authButtons.js @@ -1,31 +1,175 @@ -import React from "react"; -import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core"; +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 { auth, userinfo } = props; + const [isPromptOpen, setIsPromptOpen] = useState(shouldShowPrompt()); - if (!auth || (auth && !auth.requires_client_login)) return null; + const { auth, userInfo } = props; - return ( - - - - {!userinfo.is_authenticated ? "Log In" : "Log Out"} - - - + 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 = ( + + + + + ); + + return ( + + + + ); + } + + const LoginButton = ( + + + Log In + + + ); + + if (isPromptOpen) { + return ( + } + onInteraction={setIsPromptOpen} + > + {LoginButton} + + ); + } + + 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 ( + +

    + 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. +

    + + Do not show me this message again + +
    + +
    +
    + ); +} + export default Auth; diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js index b4271302..cbfeec5e 100644 --- a/client/src/components/menubar/index.js +++ b/client/src/components/menubar/index.js @@ -7,6 +7,7 @@ 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"; @@ -41,7 +42,7 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers"; celllist2: state.differential.celllist2, libraryVersions: state.config?.["library_versions"], auth: state.config?.authentication, - userinfo: state.userinfo, + userInfo: state.userInfo, undoDisabled: state["@@undoable/past"].length === 0, redoDisabled: state["@@undoable/future"].length === 0, aboutLink: state.config?.links?.["about-dataset"], @@ -216,6 +217,8 @@ class MenuBar extends React.PureComponent { subsetPossible, subsetResetPossible, enableReembedding, + userInfo, + auth, } = this.props; const { pendingClipPercentiles } = this.state; @@ -241,6 +244,7 @@ class MenuBar extends React.PureComponent { zIndex: 3, }} > + { - dispatch({ type: "toggle dataset drawer" }); -}; - -const InformationMenu = React.memo((props) => { - const { - libraryVersions, - tosURL, - privacyURL, - auth, - userinfo, - dispatch, - } = props; - return ( - - - handleClick(dispatch)} - icon="info-sign" - text="Dataset Overview" - /> - - - - - - - {tosURL ? ( - - ) : null} - {privacyURL ? ( - - ) : null} - - {auth?.["requires_client_login"] && - userinfo?.["is_authenticated"] ? ( - <> - - - - ) : null} - - } - position={Position.BOTTOM_RIGHT} - modifiers={{ - preventOverflow: { enabled: false }, - hide: { enabled: false }, - }} - > -