diff --git a/.infra/rdev/values.yaml b/.infra/rdev/values.yaml index ffdd3ff5f..89887401a 100644 --- a/.infra/rdev/values.yaml +++ b/.infra/rdev/values.yaml @@ -2,7 +2,7 @@ stack: services: explorer: image: - tag: sha-d5d17168 + tag: sha-8a03b930 replicaCount: 1 env: # env vars common to all deployment stages diff --git a/client/__tests__/e2e/cellxgeneActions.ts b/client/__tests__/e2e/cellxgeneActions.ts index cc96feb61..ec9a3eb59 100644 --- a/client/__tests__/e2e/cellxgeneActions.ts +++ b/client/__tests__/e2e/cellxgeneActions.ts @@ -337,6 +337,8 @@ export async function clip(min = "0", max = "100", page: Page): Promise { await clearInputAndTypeInto("clip-min-input", min, page); await clearInputAndTypeInto("clip-max-input", max, page); await page.getByTestId("clip-commit").click(); + // close clip dialog + await page.getByTestId("visualization-settings").click(); } /** diff --git a/client/__tests__/e2e/data.ts b/client/__tests__/e2e/data.ts index 3b9a9d749..0f4a58e19 100644 --- a/client/__tests__/e2e/data.ts +++ b/client/__tests__/e2e/data.ts @@ -100,7 +100,7 @@ export const datasets = { }, lasso: { "coordinates-as-percent": { x1: 0.25, y1: 0.1, x2: 0.75, y2: 0.65 }, - count: "357", + count: "332", }, }, scatter: { @@ -273,7 +273,7 @@ export const datasets = { }, lasso: { "coordinates-as-percent": { x1: 0.25, y1: 0.1, x2: 0.75, y2: 0.65 }, - count: "44", + count: "12", }, }, scatter: { diff --git a/client/src/components/menubar/index.tsx b/client/src/components/menubar/index.tsx index df404e649..8183a8932 100644 --- a/client/src/components/menubar/index.tsx +++ b/client/src/components/menubar/index.tsx @@ -1,11 +1,24 @@ -import React from "react"; +import React, { useState } from "react"; import { connect } from "react-redux"; -import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core"; +import { + ButtonGroup, + AnchorButton, + Tooltip, + ResizeSensor, +} from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import * as globals from "../../globals"; // @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message import styles from "./menubar.css"; +import { + ControlsWrapper, + EmbeddingWrapper, + MenuBarWrapper, + ResponsiveMenuGroupOne, + ResponsiveMenuGroupTwo, + MAX_VERTICAL_THRESHOLD_WIDTH_PX, +} from "./style"; import actions from "../../actions"; import Clip from "./clip"; @@ -25,7 +38,6 @@ const INITIAL_PERCENTILES = { clipPercentileMin: 0, clipPercentileMax: 100, }; - interface StateProps { subsetPossible: boolean; subsetResetPossible: boolean; @@ -72,58 +84,54 @@ interface DispatchProps { dispatch: AppDispatch; } export type MenuBarProps = StateProps & DispatchProps; -interface State { - pendingClipPercentiles: { - clipPercentileMin: number; - clipPercentileMax: number; - }; -} -class MenuBar extends React.PureComponent { - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - static isValidDigitKeyEvent(e: any) { - /* + +const isValidDigitKeyEvent = (e: KeyboardEvent) => { + /* Return true if this event is necessary to enter a percent number input. Return false if not. Returns true for events with keys: backspace, control, alt, meta, [0-9], or events that don't have a key. */ - if (e.key === null) return true; - if (e.ctrlKey || e.altKey || e.metaKey) return true; - - // concept borrowed from blueprint's numericInputUtils: - // keys that print a single character when pressed have a `key` name of - // length 1. every other key has a longer `key` name (e.g. "Backspace", - // "ArrowUp", "Shift"). since none of those keys can print a character - // to the field--and since they may have important native behaviors - // beyond printing a character--we don't want to disable their effects. - const isSingleCharKey = e.key.length === 1; - if (!isSingleCharKey) return true; - - const key = e.key.charCodeAt(0) - 48; /* "0" */ - return key >= 0 && key <= 9; - } - - constructor(props: MenuBarProps) { - super(props); - - this.state = { - pendingClipPercentiles: INITIAL_PERCENTILES, - }; - } - - isClipDisabled = () => { + if (e.key === null) return true; + if (e.ctrlKey || e.altKey || e.metaKey) return true; + + // concept borrowed from blueprint's numericInputUtils: + // keys that print a single character when pressed have a `key` name of + // length 1. every other key has a longer `key` name (e.g. "Backspace", + // "ArrowUp", "Shift"). since none of those keys can print a character + // to the field--and since they may have important native behaviors + // beyond printing a character--we don't want to disable their effects. + const isSingleCharKey = e.key.length === 1; + if (!isSingleCharKey) return true; + + const key = e.key.charCodeAt(0) - 48; /* "0" */ + return key >= 0 && key <= 9; +}; + +const MenuBar = ({ + dispatch, + disableDiffexp, + clipPercentileMin: currentClipMin, + clipPercentileMax: currentClipMax, + graphInteractionMode, + showCentroidLabels, + categoricalSelection, + colorAccessor, + subsetPossible, + subsetResetPossible, + screenCap, +}: MenuBarProps) => { + const [pendingClipPercentiles, setPendingClipPercentiles] = + useState(INITIAL_PERCENTILES); + const [isVertical, setIsVertical] = useState(false); + + const isClipDisabled = () => { /* return true if clip button should be disabled. */ - const { - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, - } = this.state; - const { - clipPercentileMin: currentClipMin, - clipPercentileMax: currentClipMax, - } = this.props; + const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles; // if you change this test, be careful with logic around // comparisons between undefined / NaN handling. @@ -136,24 +144,23 @@ class MenuBar extends React.PureComponent { }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipOnKeyPress = (e: any) => { + const handleClipOnKeyPress = (e: KeyboardEvent) => { /* allow only numbers, plus other critical keys which may be required to make a number */ - if (!MenuBar.isValidDigitKeyEvent(e)) { + if (!isValidDigitKeyEvent(e)) { e.preventDefault(); } }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipPercentileMinValueChange = (v: any) => { + const handleClipPercentileMinValueChange = (v: any) => { /* Ignore anything that isn't a legit number */ if (!Number.isFinite(v)) return; - const { pendingClipPercentiles } = this.state; const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax; /* @@ -162,19 +169,17 @@ class MenuBar extends React.PureComponent { if (v <= 0) v = 0; if (v > 100) v = 100; const clipPercentileMin = Math.round(v); // paranoia - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, - }); + + setPendingClipPercentiles({ clipPercentileMin, clipPercentileMax }); }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipPercentileMaxValueChange = (v: any) => { + const handleClipPercentileMaxValueChange = (v: any) => { /* Ignore anything that isn't a legit number */ if (!Number.isFinite(v)) return; - const { pendingClipPercentiles } = this.state; const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin; /* @@ -184,14 +189,10 @@ class MenuBar extends React.PureComponent { if (v > 100) v = 100; const clipPercentileMax = Math.round(v); // paranoia - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, - }); + setPendingClipPercentiles({ clipPercentileMin, clipPercentileMax }); }; - handleClipCommit = () => { - const { dispatch } = this.props; - const { pendingClipPercentiles } = this.state; + const handleClipCommit = () => { const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles; const min = clipPercentileMin / 100; const max = clipPercentileMax / 100; @@ -201,24 +202,19 @@ class MenuBar extends React.PureComponent { dispatch(actions.clipAction(min, max)); }; - handleClipOpening = () => { - const { clipPercentileMin, clipPercentileMax } = this.props; + const handleClipOpening = () => { track(EVENTS.EXPLORER_CLIP_CLICKED); - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, + setPendingClipPercentiles({ + clipPercentileMin: currentClipMin, + clipPercentileMax: currentClipMax, }); }; - handleClipClosing = () => { - this.setState({ - pendingClipPercentiles: INITIAL_PERCENTILES, - }); + const handleClipClosing = () => { + setPendingClipPercentiles(INITIAL_PERCENTILES); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleCentroidChange = () => { - const { dispatch, showCentroidLabels } = this.props; - + const handleCentroidChange = () => { if (!showCentroidLabels) { // only track when turning on track(EVENTS.EXPLORER_SHOW_LABELS); @@ -230,220 +226,183 @@ class MenuBar extends React.PureComponent { }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleSubset = () => { - const { dispatch } = this.props; - + const handleSubset = () => { track(EVENTS.EXPLORER_SUBSET_BUTTON_CLICKED); dispatch(actions.subsetAction()); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleSubsetReset = () => { - const { dispatch } = this.props; - + const handleSubsetReset = () => { track(EVENTS.EXPLORER_RESET_SUBSET_BUTTON_CLICKED); dispatch(actions.resetSubsetAction()); }; - render() { - const { - dispatch, - disableDiffexp, - clipPercentileMin, - clipPercentileMax, - graphInteractionMode, - showCentroidLabels, - categoricalSelection, - colorAccessor, - subsetPossible, - subsetResetPossible, - screenCap, - } = this.props; - const { pendingClipPercentiles } = this.state; - - const isColoredByCategorical = - !!categoricalSelection?.[colorAccessor || ""]; - - const isTest = getFeatureFlag(FEATURES.TEST); - const isDownload = getFeatureFlag(FEATURES.DOWNLOAD); - - // constants used to create selection tool button - const [selectionTooltip, selectionButtonIcon] = [ - "select", - "polygon-filter", - ]; - - return ( -
-
+ const onResize = (e: ResizeObserverEntry[]) => { + if (e[0].contentRect.width <= MAX_VERTICAL_THRESHOLD_WIDTH_PX) { + setIsVertical(true); + } else { + setIsVertical(false); + } + }; + + const isColoredByCategorical = !!categoricalSelection?.[colorAccessor || ""]; + + const isTest = getFeatureFlag(FEATURES.TEST); + const isDownload = getFeatureFlag(FEATURES.DOWNLOAD); + + // constants used to create selection tool button + const [selectionTooltip, selectionButtonIcon] = ["select", "polygon-filter"]; + + return ( + + + -
-
- - { - dispatch({ - type: "toggle active info panel", - activeTab: "Dataset", - }); - }} - style={{ - cursor: "pointer", - }} - data-testid="drawer" - /> - - {isDownload && ( - + + + + { + dispatch({ + type: "toggle active info panel", + activeTab: "Dataset", + }); + }} style={{ cursor: "pointer", }} - loading={screenCap} - onClick={() => dispatch({ type: "graph: screencap start" })} + data-testid="drawer" /> - - )} - {isTest && ( - dispatch({ type: "test: screencap start" })} - /> - )} - - - - - - + + {isDownload && ( + + dispatch({ type: "graph: screencap start" })} + /> + + )} + {isTest && ( { - track(EVENTS.EXPLORER_MODE_LASSO_BUTTON_CLICKED); - - dispatch({ - type: "change graph interaction mode", - data: "select", - }); + icon={IconNames.TORCH} + style={{ + cursor: "pointer", }} + data-testid={GRAPH_AS_IMAGE_TEST_ID} + data-chromatic="ignore" + loading={screenCap} + onClick={() => dispatch({ type: "test: screencap start" })} /> - + )} + { - track(EVENTS.EXPLORER_MODE_PAN_ZOOM_BUTTON_CLICKED); - - dispatch({ - type: "change graph interaction mode", - data: "zoom", - }); - }} + data-testid="centroid-label-toggle" + icon="property" + onClick={handleCentroidChange} + active={showCentroidLabels} + intent={showCentroidLabels ? "primary" : "none"} + disabled={!isColoredByCategorical} /> - - + + + + + { + track(EVENTS.EXPLORER_MODE_LASSO_BUTTON_CLICKED); + + dispatch({ + type: "change graph interaction mode", + data: "select", + }); + }} + /> + + + { + track(EVENTS.EXPLORER_MODE_PAN_ZOOM_BUTTON_CLICKED); + + dispatch({ + type: "change graph interaction mode", + data: "zoom", + }); + }} + /> + + + + {disableDiffexp ? null : } -
-
- ); - } -} + + + + ); +}; export default connect(mapStateToProps)(MenuBar); diff --git a/client/src/components/menubar/style.ts b/client/src/components/menubar/style.ts new file mode 100644 index 000000000..4aa5a2184 --- /dev/null +++ b/client/src/components/menubar/style.ts @@ -0,0 +1,75 @@ +import styled from "@emotion/styled"; +import { spacesS } from "../theme"; +import { getFeatureFlag } from "../../util/featureFlags/featureFlags"; +import { FEATURES } from "../../util/featureFlags/features"; + +export const MAX_VERTICAL_THRESHOLD_WIDTH_PX = 500; +const isTest = getFeatureFlag(FEATURES.TEST); +const isDownload = getFeatureFlag(FEATURES.DOWNLOAD); + +let FIRST_VERTICAL_THRESHOLD_WIDTH_PX = 655; +if (isTest && isDownload) { + FIRST_VERTICAL_THRESHOLD_WIDTH_PX = 705; +} else if (isTest || isDownload) { + FIRST_VERTICAL_THRESHOLD_WIDTH_PX = 685; +} + +export const MenuBarWrapper = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + width: 100%; + position: relative; + container: menu-bar / inline-size; +`; + +export const ResponsiveMenuGroupOne = styled.div` + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: right; + @container menu-bar (max-width: ${MAX_VERTICAL_THRESHOLD_WIDTH_PX}px) { + flex-direction: column-reverse; + position: absolute; + top: 37px; + } +`; + +export const ResponsiveMenuGroupTwo = styled.div` + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: right; + @container menu-bar (max-width: ${FIRST_VERTICAL_THRESHOLD_WIDTH_PX}px) { + flex-direction: column-reverse; + position: absolute; + top: 40px; + } + @container menu-bar (max-width: ${MAX_VERTICAL_THRESHOLD_WIDTH_PX}px) { + top: 172px; + } +`; + +export const EmbeddingWrapper = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: left; + margin-top: ${spacesS}px; +`; + +export const ControlsWrapper = styled.div` + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: right; + align-items: flex-start; + position: relative; + @container menu-bar (max-width: ${MAX_VERTICAL_THRESHOLD_WIDTH_PX}px) { + flex-direction: column-reverse; + align-items: flex-end; + position: absolute; + right: 0px; + } +`; diff --git a/client/src/components/menubar/subset.tsx b/client/src/components/menubar/subset.tsx index ca96329e2..5fa87af17 100644 --- a/client/src/components/menubar/subset.tsx +++ b/client/src/components/menubar/subset.tsx @@ -4,20 +4,25 @@ import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core"; import styles from "./menubar.css"; import * as globals from "../../globals"; -const Subset = React.memo((props) => { +interface SubsetProps { + subsetPossible: boolean; + subsetResetPossible: boolean; + handleSubset: () => void; + handleSubsetReset: () => void; + isVertical: boolean; +} + +const Subset = React.memo((props: SubsetProps) => { const { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetPossible' does not exist on type '... Remove this comment to see the full error message subsetPossible, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetResetPossible' does not exist on t... Remove this comment to see the full error message subsetResetPossible, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'handleSubset' does not exist on type '{ ... Remove this comment to see the full error message handleSubset, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'handleSubsetReset' does not exist on typ... Remove this comment to see the full error message handleSubsetReset, + isVertical, } = props; return ( - +