From a875df6653a7fbe82d541c0fe9d72b9f7bfd25e3 Mon Sep 17 00:00:00 2001 From: Suzette McCanny Date: Tue, 6 Aug 2024 15:24:44 -0400 Subject: [PATCH] feat: Add survey banner to Explorer (#1063) Co-authored-by: rainandbare --- .infra/rdev/values.yaml | 2 +- client/__tests__/e2e/e2e.test.ts | 42 ++++++++++- client/__tests__/util/helpers.ts | 12 ++++ .../src/components/BottomBanner/constants.ts | 7 ++ client/src/components/BottomBanner/index.tsx | 70 +++++++++++++++++++ client/src/components/BottomBanner/style.ts | 63 +++++++++++++++++ client/src/components/app.tsx | 57 ++++++++------- client/src/components/framework/container.tsx | 50 +++++++++---- client/src/components/framework/layout.tsx | 4 +- client/src/components/graph/graph.tsx | 6 ++ client/src/components/graph/types.ts | 1 + client/src/reducers/index.ts | 3 + client/src/reducers/showBottomBanner.ts | 41 +++++++++++ 13 files changed, 314 insertions(+), 44 deletions(-) create mode 100644 client/src/components/BottomBanner/constants.ts create mode 100644 client/src/components/BottomBanner/index.tsx create mode 100644 client/src/components/BottomBanner/style.ts create mode 100644 client/src/reducers/showBottomBanner.ts diff --git a/.infra/rdev/values.yaml b/.infra/rdev/values.yaml index 18b40bc3d..344930479 100644 --- a/.infra/rdev/values.yaml +++ b/.infra/rdev/values.yaml @@ -2,7 +2,7 @@ stack: services: explorer: image: - tag: sha-3cf6306f + tag: sha-4c9517de replicaCount: 1 env: # env vars common to all deployment stages diff --git a/client/__tests__/e2e/e2e.test.ts b/client/__tests__/e2e/e2e.test.ts index 6acfd47dc..677db2912 100644 --- a/client/__tests__/e2e/e2e.test.ts +++ b/client/__tests__/e2e/e2e.test.ts @@ -66,6 +66,7 @@ import { pageURLSpatial, } from "../common/constants"; import { + closeBottomBanner, conditionallyToggleSidePanel, goToPage, shouldSkipTests, @@ -207,6 +208,33 @@ for (const testDataset of testDatasets) { }); }); + + describe("bottom banner", () => { + const SURVEY_LINK = "https://airtable.com/app8fNSQ8ieIiHLOv/shrmD31azkGtSupmO"; + test("bottom banner appears", async ({ page }, testInfo) => { + await goToPage(page, url); + + const bottomBanner = page.getByTestId("bottom-banner"); + + await expect(bottomBanner).toBeVisible(); + + await expect(page.getByText("quick survey")).toHaveAttribute( + "href", + SURVEY_LINK + ); + + await snapshotTestGraph(page, testInfo); + }); + test("bottom banner disappears", async ({ page }, testInfo) => { + await goToPage(page, url); + + const bottomBanner = await closeBottomBanner(page) + await expect(bottomBanner).not.toBeVisible(); + + await snapshotTestGraph(page, testInfo); + }); + }); + test("resize graph", async ({ page }, testInfo) => { skipIfSidePanel(graphTestId, MAIN_PANEL); @@ -314,6 +342,8 @@ for (const testDataset of testDatasets) { await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + await closeBottomBanner(page); + const originalCellCount = await getCellSetCount(1, page); for (const cellset of data.cellsets.lasso) { @@ -506,6 +536,8 @@ for (const testDataset of testDatasets) { await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + await closeBottomBanner(page) + const lassoSelection = await calcDragCoordinates( graphTestId, data.subset.lasso["coordinates-as-percent"], @@ -717,6 +749,7 @@ for (const testDataset of testDatasets) { }); describe("graph overlay", () => { + test("transform centroids correctly", async ({ page }, testInfo) => { skipIfSidePanel(graphTestId, MAIN_PANEL); @@ -815,6 +848,7 @@ for (const testDataset of testDatasets) { }, testInfo) => { await goToPage(page, url); await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + await closeBottomBanner(page); await tryUntil( async () => { @@ -871,6 +905,7 @@ for (const testDataset of testDatasets) { test("lasso moves after pan", async ({ page }, testInfo) => { await goToPage(page, url); await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + await closeBottomBanner(page); await tryUntil( async () => { @@ -1701,7 +1736,12 @@ async function setup({ testInfo: TestInfo; }) { await goToPage(page, url); - + await tryUntil( + async () => { + await closeBottomBanner(page); + }, + { page } + ); if (withSubset) { await subset({ x1: 0.1, y1: 0.15, x2: 0.8, y2: 0.85 }, page, testInfo); } diff --git a/client/__tests__/util/helpers.ts b/client/__tests__/util/helpers.ts index 1ce673cdd..da40dbb14 100644 --- a/client/__tests__/util/helpers.ts +++ b/client/__tests__/util/helpers.ts @@ -304,3 +304,15 @@ export function shouldSkipTests( ): boolean { return graphTestId === SIDE_PANEL; } + + +export async function closeBottomBanner(page: Page): Promise { + const bottomBanner = page.getByTestId("bottom-banner"); + + if(bottomBanner) { + const bottomBannerClose = bottomBanner.getByRole("button"); + await bottomBannerClose.click(); + } + + return bottomBanner; +} \ No newline at end of file diff --git a/client/src/components/BottomBanner/constants.ts b/client/src/components/BottomBanner/constants.ts new file mode 100644 index 000000000..191bb6b51 --- /dev/null +++ b/client/src/components/BottomBanner/constants.ts @@ -0,0 +1,7 @@ +export const BANNER_FEEDBACK_SURVEY_LINK = + "https://airtable.com/app8fNSQ8ieIiHLOv/shrmD31azkGtSupmO"; +export const BOTTOM_BANNER_SURVEY_LINK_TEXT = "quick survey."; +export const BOTTOM_BANNER_SURVEY_TEXT = "Send us feedback with this"; +export const BOTTOM_BANNER_LAST_CLOSED_TIME_KEY = "bottomBannerLastClosedTime"; +export const BOTTOM_BANNER_EXPIRATION_TIME_MS = 30 * 24 * 60 * 60 * 1000; // 30 days +// export const BOTTOM_BANNER_EXPIRATION_TIME_MS = 30 * 1000; // 30 seconds diff --git a/client/src/components/BottomBanner/index.tsx b/client/src/components/BottomBanner/index.tsx new file mode 100644 index 000000000..2ef7ae7bb --- /dev/null +++ b/client/src/components/BottomBanner/index.tsx @@ -0,0 +1,70 @@ +import React, { memo } from "react"; +import { connect } from "react-redux"; +import { + BOTTOM_BANNER_ID, + StyledBanner, + StyledBottomBannerWrapper, + StyledLink, +} from "./style"; +import { + BOTTOM_BANNER_SURVEY_LINK_TEXT, + BOTTOM_BANNER_SURVEY_TEXT, +} from "./constants"; +import { AppDispatch, RootState } from "../../reducers"; + +export interface BottomBannerProps { + surveyLink: string; + showBottomBanner: boolean; + dispatch: AppDispatch; +} + +const mapStateToProps = (state: RootState) => ({ + showBottomBanner: state.showBottomBanner, +}); + +const mapDispatchToProps = (dispatch: AppDispatch) => ({ + dispatch, +}); + +const BottomBanner = memo( + ({ + surveyLink, + showBottomBanner, + dispatch, + }: BottomBannerProps): JSX.Element | null => { + const setBottomBannerLastClosedTime = () => { + dispatch({ + type: "update bottom banner last closed time", + time: Date.now(), + }); + }; + + if (!showBottomBanner) return null; + + return ( + <> + + + {BOTTOM_BANNER_SURVEY_TEXT} + + {BOTTOM_BANNER_SURVEY_LINK_TEXT} + + + } + /> + + + ); + } +); + +export default connect(mapStateToProps, mapDispatchToProps)(BottomBanner); diff --git a/client/src/components/BottomBanner/style.ts b/client/src/components/BottomBanner/style.ts new file mode 100644 index 000000000..cf6c75b5e --- /dev/null +++ b/client/src/components/BottomBanner/style.ts @@ -0,0 +1,63 @@ +import styled from "@emotion/styled"; +import { Banner, Icon } from "czifui"; +import { beta100, beta400, gray500 } from "../theme"; + +export const SKINNY_MODE_BREAKPOINT_WIDTH = 960; +export const BOTTOM_BANNER_ID = "bottom-banner"; + +export const StyledBanner = styled(Banner)` + @media (max-width: ${SKINNY_MODE_BREAKPOINT_WIDTH}px) { + padding: 8px 16px; + box-shadow: 0px 0px 4px 0px rgba(50, 50, 50, 0.75); + } + span { + font-family: "Roboto Condensed", "Helvetica Neue", "Helvetica", "Arial", + sans-serif; + font-weight: 400; + } + /** + * beta intent does not exist for SDS banner, but the colors do targeting + * specific id to overwrite style + */ + border-color: ${beta400} !important; + background-color: ${beta100}; + color: black; + + /* Hide default svg icon in the Banner as it is not in figma */ + :first-of-type > div:first-of-type > div:first-of-type { + display: none; + } + + /* Change close button icon default color */ + button svg { + path { + fill: ${gray500}; + } + } +`; + +export const StyledBottomBannerWrapper = styled.div` + width: 100%; + + /* Right behind modal overlay */ + z-index: 19; + background-color: purple; +`; + +export const StyledLink = styled.a` + padding: 0px 5px 0px 5px; + text-decoration-line: underline; + color: #8f5aff; + font-weight: 500; + + :hover { + color: #5826c1; + } +`; + +const STYLED_CLOSE_BUTTON_ICON_DENY_PROPS = ["hideCloseButton"]; + +export const StyledCloseButtonIcon = styled(Icon, { + shouldForwardProp: (prop) => + !STYLED_CLOSE_BUTTON_ICON_DENY_PROPS.includes(prop), +})``; diff --git a/client/src/components/app.tsx b/client/src/components/app.tsx index 7ed196efb..e17eda196 100644 --- a/client/src/components/app.tsx +++ b/client/src/components/app.tsx @@ -4,6 +4,7 @@ import { connect } from "react-redux"; import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; import { StylesProvider, ThemeProvider } from "@material-ui/core/styles"; import { theme } from "./theme"; +import BottomBanner from "./BottomBanner"; import Controls from "./controls"; import DatasetSelector from "./datasetSelector/datasetSelector"; @@ -23,6 +24,7 @@ import Graph from "./graph/graph"; import DiffexNotice from "./diffexNotice"; import Scatterplot from "./scatterplot/scatterplot"; import PanelEmbedding from "./PanelEmbedding"; +import { BANNER_FEEDBACK_SURVEY_LINK } from "./BottomBanner/constants"; interface StateProps { loading: RootState["controls"]["loading"]; @@ -99,32 +101,35 @@ class App extends React.Component {
)} {loading || error ? null : ( - ( - <> - - - - - - - {scatterplotXXaccessor && scatterplotYYaccessor && ( - - )} - - - - - - )} - > - - - + <> + ( + <> + + + + + + + {scatterplotXXaccessor && scatterplotYYaccessor && ( + + )} + + + + + + )} + > + + + + + )} diff --git a/client/src/components/framework/container.tsx b/client/src/components/framework/container.tsx index a50e25e6a..9fbf3e420 100644 --- a/client/src/components/framework/container.tsx +++ b/client/src/components/framework/container.tsx @@ -1,22 +1,44 @@ import React from "react"; +import { connect } from "react-redux"; + +import { RootState } from "../../reducers"; + +const mapStateToProps = (state: RootState) => ({ + showBottomBanner: state.showBottomBanner, +}); // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. function Container(props: any) { - const { children } = props; + const { children, showBottomBanner } = props; return ( -
- {children} -
+ (showBottomBanner && ( +
+ {children} +
+ )) || ( +
+ {children} +
+ ) ); } -export default Container; +export default connect(mapStateToProps)(Container); diff --git a/client/src/components/framework/layout.tsx b/client/src/components/framework/layout.tsx index 83e70e26c..e29d6dcf9 100644 --- a/client/src/components/framework/layout.tsx +++ b/client/src/components/framework/layout.tsx @@ -43,8 +43,8 @@ const Layout: React.FC = (props) => { columnGap: "0px", justifyItems: "stretch", alignItems: "stretch", - height: "inherit", - width: "inherit", + height: "100%", + width: "100%", position: "relative", top: 0, left: 0, diff --git a/client/src/components/graph/graph.tsx b/client/src/components/graph/graph.tsx index 470201a63..b3b502ca7 100644 --- a/client/src/components/graph/graph.tsx +++ b/client/src/components/graph/graph.tsx @@ -93,6 +93,7 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => ({ unsMetadata: state.controls.unsMetadata, imageOpacity: state.controls.imageOpacity, dotOpacity: state.controls.dotOpacity, + showBottomBanner: state.showBottomBanner, }); class Graph extends React.Component { @@ -315,6 +316,7 @@ class Graph extends React.Component { isHidden, isSidePanel, imageOpacity, + showBottomBanner, } = this.props; const { toolSVG, viewport } = this.state; @@ -377,6 +379,10 @@ class Graph extends React.Component { this.showOpenseadragon(); } + if (prevProps.showBottomBanner !== showBottomBanner) { + this.handleResize(); + } + // Re-center when switching embedding mode if (prevProps.layoutChoice.current !== layoutChoice.current) { this.handleResize(); diff --git a/client/src/components/graph/types.ts b/client/src/components/graph/types.ts index 38fe01179..ff57d89f2 100644 --- a/client/src/components/graph/types.ts +++ b/client/src/components/graph/types.ts @@ -65,6 +65,7 @@ export interface StateProps { unsMetadata: RootState["controls"]["unsMetadata"]; imageOpacity: RootState["controls"]["imageOpacity"]; dotOpacity: RootState["controls"]["dotOpacity"]; + showBottomBanner: RootState["showBottomBanner"]; } export interface OwnProps { diff --git a/client/src/reducers/index.ts b/client/src/reducers/index.ts index 99b7044e0..028ca0307 100644 --- a/client/src/reducers/index.ts +++ b/client/src/reducers/index.ts @@ -27,6 +27,7 @@ import pointDilation from "./pointDilation"; import quickGenes from "./quickGenes"; import singleContinuousValue from "./singleContinuousValue"; import panelEmbedding from "./panelEmbedding"; +import showBottomBanner from "./showBottomBanner"; import { gcMiddleware as annoMatrixGC } from "../annoMatrix"; @@ -52,6 +53,7 @@ const AppReducer = undoable( ["centroidLabels", centroidLabels], ["pointDilation", pointDilation], ["datasetMetadata", datasetMetadata], + ["showBottomBanner", showBottomBanner], ] as Parameters[0]), [ "annoMatrix", @@ -101,6 +103,7 @@ export type RootState = { centroidLabels: ReturnType; pointDilation: ReturnType; datasetMetadata: ReturnType; + showBottomBanner: ReturnType; }; export type AppDispatch = ThunkDispatch; diff --git a/client/src/reducers/showBottomBanner.ts b/client/src/reducers/showBottomBanner.ts new file mode 100644 index 000000000..f6dacad45 --- /dev/null +++ b/client/src/reducers/showBottomBanner.ts @@ -0,0 +1,41 @@ +/* +Reducer for showBottomBanner +*/ + +import { AnyAction } from "redux"; +import { + BOTTOM_BANNER_EXPIRATION_TIME_MS, + BOTTOM_BANNER_LAST_CLOSED_TIME_KEY, +} from "../components/BottomBanner/constants"; +import type { RootState } from "./index"; + +const showBanner = () => { + const bottomBannerLastClosedTime = localStorage.getItem( + BOTTOM_BANNER_LAST_CLOSED_TIME_KEY + ); + const show = + !bottomBannerLastClosedTime || + Date.now() - Number(bottomBannerLastClosedTime) > + BOTTOM_BANNER_EXPIRATION_TIME_MS; + if (show && bottomBannerLastClosedTime) { + localStorage.setItem(BOTTOM_BANNER_LAST_CLOSED_TIME_KEY, "0"); + } + return show; +}; + +const showBottomBanner = (_state: RootState, action: AnyAction): boolean => { + switch (action.type) { + case "initial data load start": { + return showBanner(); + } + case "update bottom banner last closed time": { + localStorage.setItem(BOTTOM_BANNER_LAST_CLOSED_TIME_KEY, action.time); + return showBanner(); + } + default: { + return showBanner(); + } + } +}; + +export default showBottomBanner;