diff --git a/frontend/src/metabase-types/store/embed.ts b/frontend/src/metabase-types/store/embed.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e5bb3414f82a05fc61dcd8903d054bb7fd0324f --- /dev/null +++ b/frontend/src/metabase-types/store/embed.ts @@ -0,0 +1,10 @@ +export interface EmbedOptions { + top_nav?: boolean; + search?: boolean; + new_button?: boolean; + side_nav?: boolean | "default"; +} + +export interface EmbedState { + options: EmbedOptions; +} diff --git a/frontend/src/metabase-types/store/index.ts b/frontend/src/metabase-types/store/index.ts index 9a4bd5500d0cde49c26b971dd497a45f18de2b7c..fe6a10c8255910499d431c456cd6a98080ee1246 100644 --- a/frontend/src/metabase-types/store/index.ts +++ b/frontend/src/metabase-types/store/index.ts @@ -1,5 +1,6 @@ export * from "./admin"; export * from "./app"; +export * from "./embed"; export * from "./entities"; export * from "./forms"; export * from "./settings"; diff --git a/frontend/src/metabase-types/store/mocks/embed.ts b/frontend/src/metabase-types/store/mocks/embed.ts new file mode 100644 index 0000000000000000000000000000000000000000..08b4d0b451694c318919e2a67e25678ee7ab5848 --- /dev/null +++ b/frontend/src/metabase-types/store/mocks/embed.ts @@ -0,0 +1,12 @@ +import { EmbedOptions, EmbedState } from "metabase-types/store"; + +export const createMockEmbedOptions = (opts?: Partial<EmbedOptions>) => ({ + ...opts, +}); + +export const createMockEmbedState = ( + opts?: Partial<EmbedState>, +): EmbedState => ({ + options: createMockEmbedOptions(), + ...opts, +}); diff --git a/frontend/src/metabase-types/store/mocks/index.ts b/frontend/src/metabase-types/store/mocks/index.ts index 01343301932b7d843cfb485b29abf48a74c3de80..b3215315b9a0c01bea49f5e98b28a9dd90f1393d 100644 --- a/frontend/src/metabase-types/store/mocks/index.ts +++ b/frontend/src/metabase-types/store/mocks/index.ts @@ -1,5 +1,6 @@ export * from "./admin"; export * from "./app"; +export * from "./embed"; export * from "./entities"; export * from "./forms"; export * from "./qb"; diff --git a/frontend/src/metabase-types/store/mocks/state.ts b/frontend/src/metabase-types/store/mocks/state.ts index 5b80c6d644c2c6d826beda278fc5004c52262d59..e7a05e80c7c8bac11d4b4c2de7614e2d23702e22 100644 --- a/frontend/src/metabase-types/store/mocks/state.ts +++ b/frontend/src/metabase-types/store/mocks/state.ts @@ -3,17 +3,19 @@ import { createMockUser } from "metabase-types/api/mocks"; import { createMockAdminState, createMockAppState, - createMockSettingsState, + createMockEmbedState, createMockEntitiesState, + createMockFormState, createMockQueryBuilderState, + createMockSettingsState, createMockSetupState, - createMockFormState, } from "metabase-types/store/mocks"; export const createMockState = (opts?: Partial<State>): State => ({ admin: createMockAdminState(), app: createMockAppState(), currentUser: createMockUser(), + embed: createMockEmbedState(), entities: createMockEntitiesState(), form: createMockFormState(), qb: createMockQueryBuilderState(), diff --git a/frontend/src/metabase-types/store/state.ts b/frontend/src/metabase-types/store/state.ts index dc079ead7c0b50595450a022b763c66b1b1dd9b7..9a7613ef12d7852e61a2da2c7148721bd1d980c8 100644 --- a/frontend/src/metabase-types/store/state.ts +++ b/frontend/src/metabase-types/store/state.ts @@ -1,6 +1,7 @@ import { User } from "metabase-types/api"; import { AdminState } from "./admin"; import { AppState } from "./app"; +import { EmbedState } from "./embed"; import { EntitiesState } from "./entities"; import { FormState } from "./forms"; import { QueryBuilderState } from "./qb"; @@ -11,6 +12,7 @@ export interface State { admin: AdminState; app: AppState; currentUser: User; + embed: EmbedState; entities: EntitiesState; form: FormState; qb: QueryBuilderState; diff --git a/frontend/src/metabase/App.tsx b/frontend/src/metabase/App.tsx index 34fe4103e5c8e753e7524269cd9d92c992789667..98e2c390440c1fb40a6126531861421d14ee278c 100644 --- a/frontend/src/metabase/App.tsx +++ b/frontend/src/metabase/App.tsx @@ -1,6 +1,5 @@ import React, { ErrorInfo, ReactNode, useMemo, useState } from "react"; import { connect } from "react-redux"; -import _ from "underscore"; import { Location } from "history"; import AppErrorCard from "metabase/components/AppErrorCard/AppErrorCard"; @@ -15,6 +14,7 @@ import { import UndoListing from "metabase/containers/UndoListing"; import { getErrorPage } from "metabase/selectors/app"; +import { getEmbedOptions } from "metabase/selectors/embed"; import { getUser } from "metabase/selectors/user"; import { getIsEditing as getIsEditingDashboard } from "metabase/dashboard/selectors"; import { useOnMount } from "metabase/hooks/use-on-mount"; @@ -25,7 +25,7 @@ import Navbar from "metabase/nav/containers/Navbar"; import StatusListing from "metabase/status/containers/StatusListing"; import { User } from "metabase-types/api"; -import { AppErrorDescriptor, State } from "metabase-types/store"; +import { AppErrorDescriptor, EmbedOptions, State } from "metabase-types/store"; import { AppContentContainer, AppContent } from "./App.styled"; @@ -58,6 +58,8 @@ interface AppStateProps { currentUser?: User; errorPage: AppErrorDescriptor | null; isEditingDashboard: boolean; + isEmbedded: boolean; + embedOptions: EmbedOptions; } interface AppRouterOwnProps { @@ -72,6 +74,8 @@ function mapStateToProps(state: State): AppStateProps { currentUser: getUser(state), errorPage: getErrorPage(state), isEditingDashboard: getIsEditingDashboard(state), + isEmbedded: IFRAMED, + embedOptions: getEmbedOptions(state), }; } @@ -92,6 +96,8 @@ function App({ errorPage, location: { pathname, hash }, isEditingDashboard, + isEmbedded, + embedOptions, children, }: AppProps) { const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null); @@ -106,19 +112,22 @@ function App({ if (!currentUser || isEditingDashboard) { return false; } - if (IFRAMED) { + if (isEmbedded && !embedOptions.side_nav) { + return false; + } + if (isEmbedded && embedOptions.side_nav === "default") { return EMBEDDED_ROUTES_WITH_NAVBAR.some(pattern => pattern.test(pathname), ); } return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname)); - }, [currentUser, pathname, isEditingDashboard]); + }, [currentUser, pathname, isEditingDashboard, isEmbedded, embedOptions]); const hasAppBar = useMemo(() => { const isFullscreen = hash.includes("fullscreen"); if ( !currentUser || - IFRAMED || + (isEmbedded && !embedOptions.top_nav) || isAdminApp || isEditingDashboard || isFullscreen @@ -126,7 +135,15 @@ function App({ return false; } return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname)); - }, [currentUser, pathname, isEditingDashboard, isAdminApp, hash]); + }, [ + currentUser, + pathname, + isEditingDashboard, + isEmbedded, + embedOptions, + isAdminApp, + hash, + ]); return ( <ErrorBoundary onError={setErrorInfo}> diff --git a/frontend/src/metabase/lib/browser.js b/frontend/src/metabase/lib/browser.js index 11b5e5a149677029f93c558c84b0bcac7855ef26..094afc17a4ef2cd514743a286ec4cb84f5b87dfc 100644 --- a/frontend/src/metabase/lib/browser.js +++ b/frontend/src/metabase/lib/browser.js @@ -1,7 +1,8 @@ import querystring from "querystring"; -export function parseHashOptions(hash) { - const options = querystring.parse(hash.replace(/^#/, "")); +function parseQueryStringOptions(s) { + const options = querystring.parse(s); + for (const name in options) { if (options[name] === "") { options[name] = true; @@ -9,9 +10,18 @@ export function parseHashOptions(hash) { options[name] = JSON.parse(options[name]); } } + return options; } +export function parseHashOptions(hash) { + return parseQueryStringOptions(hash.replace(/^#/, "")); +} + +export function parseSearchOptions(search) { + return parseQueryStringOptions(search.replace(/^\?/, "")); +} + export function stringifyHashOptions(options) { return querystring.stringify(options).replace(/=true\b/g, ""); } diff --git a/frontend/src/metabase/lib/embed.js b/frontend/src/metabase/lib/embed.js index 5a22ca670d9af169e1007041c79cba1db998b037..dc3aec422569ff2f48fbadb196dd9832c312db2a 100644 --- a/frontend/src/metabase/lib/embed.js +++ b/frontend/src/metabase/lib/embed.js @@ -1,9 +1,8 @@ import { push } from "react-router-redux"; - import _ from "underscore"; - +import { parseSearchOptions } from "metabase/lib/browser"; import { IFRAMED, IFRAMED_IN_SELF } from "metabase/lib/dom"; - +import { setOptions } from "metabase/redux/embed"; import { isFitViewportMode } from "metabase/hoc/FitViewPort"; // detect if this page is embedded in itself, i.e. it's a embed preview @@ -42,6 +41,7 @@ export function initializeEmbedding(store) { } } }); + store.dispatch(setOptions(parseSearchOptions(window.location.search))); } } diff --git a/frontend/src/metabase/nav/containers/AppBar.styled.tsx b/frontend/src/metabase/nav/containers/AppBar.styled.tsx index 661fcfdccdbe145ef102a3a82aad0b2a381c34a2..89f51d5743117394b2bbd52417f3d54a6764cb66 100644 --- a/frontend/src/metabase/nav/containers/AppBar.styled.tsx +++ b/frontend/src/metabase/nav/containers/AppBar.styled.tsx @@ -30,8 +30,7 @@ export const LogoLink = styled(Link)` justify-content: center; border-radius: 6px; left: 0; - padding: ${space(1)}; - padding-left: ${space(2)}; + padding: ${space(1)} ${space(2)}; margin-left: ${space(2)}; position: absolute; transition: opacity 0.3s; @@ -58,6 +57,7 @@ export const SidebarButtonContainer = styled.div` `; export interface LeftContainerProps { + isLogoActive: boolean; isSearchActive: boolean; } @@ -70,12 +70,12 @@ export const LeftContainer = styled.div<LeftContainerProps>` &:hover { ${LogoLink} { - opacity: 0; - pointer-events: none; + opacity: ${props => (props.isLogoActive ? 1 : 0)}; + pointer-events: ${props => (props.isLogoActive ? "" : "none")}; } ${SidebarButtonContainer} { - opacity: 1; + opacity: ${props => (props.isLogoActive ? 0 : 1)}; } } diff --git a/frontend/src/metabase/nav/containers/AppBar.tsx b/frontend/src/metabase/nav/containers/AppBar.tsx index e562ad78a062cef542ff58e138f74e0d559f8ea6..a1ba149bf7e79e4309d03dcdec8eadb201aa7907 100644 --- a/frontend/src/metabase/nav/containers/AppBar.tsx +++ b/frontend/src/metabase/nav/containers/AppBar.tsx @@ -11,11 +11,12 @@ import SearchBar from "metabase/nav/components/SearchBar"; import SidebarButton from "metabase/nav/components/SidebarButton"; import NewButton from "metabase/nav/containers/NewButton"; -import { State } from "metabase-types/store"; +import { EmbedOptions, State } from "metabase-types/store"; import { getIsNavbarOpen, closeNavbar, toggleNavbar } from "metabase/redux/app"; import { isMac } from "metabase/lib/browser"; -import { isSmallScreen } from "metabase/lib/dom"; +import { IFRAMED, isSmallScreen } from "metabase/lib/dom"; +import { getEmbedOptions } from "metabase/selectors/embed"; import { AppBarRoot, @@ -30,6 +31,8 @@ import { type Props = { isNavbarOpen: boolean; + isEmbedded: boolean; + embedOptions: EmbedOptions; toggleNavbar: () => void; closeNavbar: () => void; }; @@ -37,6 +40,8 @@ type Props = { function mapStateToProps(state: State) { return { isNavbarOpen: getIsNavbarOpen(state), + isEmbedded: IFRAMED, + embedOptions: getEmbedOptions(state), }; } @@ -53,8 +58,17 @@ function HomepageLink({ handleClick }: { handleClick: () => void }) { ); } -function AppBar({ isNavbarOpen, toggleNavbar, closeNavbar }: Props) { +function AppBar({ + isNavbarOpen, + isEmbedded, + embedOptions, + toggleNavbar, + closeNavbar, +}: Props) { const [isSearchActive, setSearchActive] = useState(false); + const hasSearch = !isEmbedded || embedOptions.search; + const hasNewButton = !isEmbedded || embedOptions.new_button; + const hasSidebar = !isEmbedded || embedOptions.side_nav; const onLogoClick = useCallback(() => { if (isSmallScreen()) { @@ -83,33 +97,42 @@ function AppBar({ isNavbarOpen, toggleNavbar, closeNavbar }: Props) { return ( <AppBarRoot> - <LeftContainer isSearchActive={isSearchActive}> + <LeftContainer isLogoActive={!hasSidebar} isSearchActive={isSearchActive}> <HomepageLink handleClick={onLogoClick} /> - <SidebarButtonContainer> - <Tooltip tooltip={sidebarButtonTooltip} isEnabled={!isSmallScreen()}> - <SidebarButton - isSidebarOpen={isNavbarOpen} - onClick={toggleNavbar} - /> - </Tooltip> - </SidebarButtonContainer> + {hasSidebar && ( + <SidebarButtonContainer> + <Tooltip + tooltip={sidebarButtonTooltip} + isEnabled={!isSmallScreen()} + > + <SidebarButton + isSidebarOpen={isNavbarOpen} + onClick={toggleNavbar} + /> + </Tooltip> + </SidebarButtonContainer> + )} </LeftContainer> {!isSearchActive && ( <MiddleContainer> <HomepageLink handleClick={onLogoClick} /> </MiddleContainer> )} - <RightContainer> - <SearchBarContainer> - <SearchBarContent> - <SearchBar - onSearchActive={onSearchActive} - onSearchInactive={onSearchInactive} - /> - </SearchBarContent> - </SearchBarContainer> - <NewButton /> - </RightContainer> + {(hasSearch || hasNewButton) && ( + <RightContainer> + {hasSearch && ( + <SearchBarContainer> + <SearchBarContent> + <SearchBar + onSearchActive={onSearchActive} + onSearchInactive={onSearchInactive} + /> + </SearchBarContent> + </SearchBarContainer> + )} + {hasNewButton && <NewButton />} + </RightContainer> + )} </AppBarRoot> ); } diff --git a/frontend/src/metabase/reducers-common.js b/frontend/src/metabase/reducers-common.js index 1ac8a6d035194fcd96a3d7ecc88c0d30b5cafce7..621783dc9764be2925d228724bbd4914597c0476 100644 --- a/frontend/src/metabase/reducers-common.js +++ b/frontend/src/metabase/reducers-common.js @@ -2,6 +2,7 @@ /* ducks */ import app from "metabase/redux/app"; +import embed from "metabase/redux/embed"; import requests from "metabase/redux/requests"; import settings from "metabase/redux/settings"; import undo from "metabase/redux/undo"; @@ -14,6 +15,7 @@ import { currentUser } from "metabase/redux/user"; export default { // global reducers app, + embed, currentUser, // "entities" framework needs control over "requests" state requests: enhanceRequestsReducer(requests), diff --git a/frontend/src/metabase/redux/embed.js b/frontend/src/metabase/redux/embed.js new file mode 100644 index 0000000000000000000000000000000000000000..48cd5024d4b66f9cf7ea78016d4ac6e4c6a87be1 --- /dev/null +++ b/frontend/src/metabase/redux/embed.js @@ -0,0 +1,26 @@ +import { + combineReducers, + createAction, + handleActions, +} from "metabase/lib/redux"; + +const DEFAULT_OPTIONS = { + top_nav: false, + side_nav: "default", + search: false, + new_button: false, +}; + +export const SET_OPTIONS = "metabase/embed/SET_OPTIONS"; +export const setOptions = createAction(SET_OPTIONS); + +const options = handleActions( + { + [SET_OPTIONS]: (state, { payload }) => ({ ...DEFAULT_OPTIONS, ...payload }), + }, + {}, +); + +export default combineReducers({ + options, +}); diff --git a/frontend/src/metabase/selectors/embed.ts b/frontend/src/metabase/selectors/embed.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5e6ef8763eae5e95d6191a1965acc15dd0e0e48 --- /dev/null +++ b/frontend/src/metabase/selectors/embed.ts @@ -0,0 +1,5 @@ +import { State } from "metabase-types/store"; + +export const getEmbedOptions = (state: State) => { + return state.embed.options; +};