Skip to content
Snippets Groups Projects
Unverified Commit d198f6e8 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Add embedding options for app nav (#22580)

parent dd6a2e26
No related branches found
No related tags found
No related merge requests found
Showing
with 152 additions and 41 deletions
export interface EmbedOptions {
top_nav?: boolean;
search?: boolean;
new_button?: boolean;
side_nav?: boolean | "default";
}
export interface EmbedState {
options: EmbedOptions;
}
export * from "./admin"; export * from "./admin";
export * from "./app"; export * from "./app";
export * from "./embed";
export * from "./entities"; export * from "./entities";
export * from "./forms"; export * from "./forms";
export * from "./settings"; export * from "./settings";
......
import { EmbedOptions, EmbedState } from "metabase-types/store";
export const createMockEmbedOptions = (opts?: Partial<EmbedOptions>) => ({
...opts,
});
export const createMockEmbedState = (
opts?: Partial<EmbedState>,
): EmbedState => ({
options: createMockEmbedOptions(),
...opts,
});
export * from "./admin"; export * from "./admin";
export * from "./app"; export * from "./app";
export * from "./embed";
export * from "./entities"; export * from "./entities";
export * from "./forms"; export * from "./forms";
export * from "./qb"; export * from "./qb";
......
...@@ -3,17 +3,19 @@ import { createMockUser } from "metabase-types/api/mocks"; ...@@ -3,17 +3,19 @@ import { createMockUser } from "metabase-types/api/mocks";
import { import {
createMockAdminState, createMockAdminState,
createMockAppState, createMockAppState,
createMockSettingsState, createMockEmbedState,
createMockEntitiesState, createMockEntitiesState,
createMockFormState,
createMockQueryBuilderState, createMockQueryBuilderState,
createMockSettingsState,
createMockSetupState, createMockSetupState,
createMockFormState,
} from "metabase-types/store/mocks"; } from "metabase-types/store/mocks";
export const createMockState = (opts?: Partial<State>): State => ({ export const createMockState = (opts?: Partial<State>): State => ({
admin: createMockAdminState(), admin: createMockAdminState(),
app: createMockAppState(), app: createMockAppState(),
currentUser: createMockUser(), currentUser: createMockUser(),
embed: createMockEmbedState(),
entities: createMockEntitiesState(), entities: createMockEntitiesState(),
form: createMockFormState(), form: createMockFormState(),
qb: createMockQueryBuilderState(), qb: createMockQueryBuilderState(),
......
import { User } from "metabase-types/api"; import { User } from "metabase-types/api";
import { AdminState } from "./admin"; import { AdminState } from "./admin";
import { AppState } from "./app"; import { AppState } from "./app";
import { EmbedState } from "./embed";
import { EntitiesState } from "./entities"; import { EntitiesState } from "./entities";
import { FormState } from "./forms"; import { FormState } from "./forms";
import { QueryBuilderState } from "./qb"; import { QueryBuilderState } from "./qb";
...@@ -11,6 +12,7 @@ export interface State { ...@@ -11,6 +12,7 @@ export interface State {
admin: AdminState; admin: AdminState;
app: AppState; app: AppState;
currentUser: User; currentUser: User;
embed: EmbedState;
entities: EntitiesState; entities: EntitiesState;
form: FormState; form: FormState;
qb: QueryBuilderState; qb: QueryBuilderState;
......
import React, { ErrorInfo, ReactNode, useMemo, useState } from "react"; import React, { ErrorInfo, ReactNode, useMemo, useState } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import _ from "underscore";
import { Location } from "history"; import { Location } from "history";
import AppErrorCard from "metabase/components/AppErrorCard/AppErrorCard"; import AppErrorCard from "metabase/components/AppErrorCard/AppErrorCard";
...@@ -15,6 +14,7 @@ import { ...@@ -15,6 +14,7 @@ import {
import UndoListing from "metabase/containers/UndoListing"; import UndoListing from "metabase/containers/UndoListing";
import { getErrorPage } from "metabase/selectors/app"; import { getErrorPage } from "metabase/selectors/app";
import { getEmbedOptions } from "metabase/selectors/embed";
import { getUser } from "metabase/selectors/user"; import { getUser } from "metabase/selectors/user";
import { getIsEditing as getIsEditingDashboard } from "metabase/dashboard/selectors"; import { getIsEditing as getIsEditingDashboard } from "metabase/dashboard/selectors";
import { useOnMount } from "metabase/hooks/use-on-mount"; import { useOnMount } from "metabase/hooks/use-on-mount";
...@@ -25,7 +25,7 @@ import Navbar from "metabase/nav/containers/Navbar"; ...@@ -25,7 +25,7 @@ import Navbar from "metabase/nav/containers/Navbar";
import StatusListing from "metabase/status/containers/StatusListing"; import StatusListing from "metabase/status/containers/StatusListing";
import { User } from "metabase-types/api"; 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"; import { AppContentContainer, AppContent } from "./App.styled";
...@@ -58,6 +58,8 @@ interface AppStateProps { ...@@ -58,6 +58,8 @@ interface AppStateProps {
currentUser?: User; currentUser?: User;
errorPage: AppErrorDescriptor | null; errorPage: AppErrorDescriptor | null;
isEditingDashboard: boolean; isEditingDashboard: boolean;
isEmbedded: boolean;
embedOptions: EmbedOptions;
} }
interface AppRouterOwnProps { interface AppRouterOwnProps {
...@@ -72,6 +74,8 @@ function mapStateToProps(state: State): AppStateProps { ...@@ -72,6 +74,8 @@ function mapStateToProps(state: State): AppStateProps {
currentUser: getUser(state), currentUser: getUser(state),
errorPage: getErrorPage(state), errorPage: getErrorPage(state),
isEditingDashboard: getIsEditingDashboard(state), isEditingDashboard: getIsEditingDashboard(state),
isEmbedded: IFRAMED,
embedOptions: getEmbedOptions(state),
}; };
} }
...@@ -92,6 +96,8 @@ function App({ ...@@ -92,6 +96,8 @@ function App({
errorPage, errorPage,
location: { pathname, hash }, location: { pathname, hash },
isEditingDashboard, isEditingDashboard,
isEmbedded,
embedOptions,
children, children,
}: AppProps) { }: AppProps) {
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null); const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
...@@ -106,19 +112,22 @@ function App({ ...@@ -106,19 +112,22 @@ function App({
if (!currentUser || isEditingDashboard) { if (!currentUser || isEditingDashboard) {
return false; return false;
} }
if (IFRAMED) { if (isEmbedded && !embedOptions.side_nav) {
return false;
}
if (isEmbedded && embedOptions.side_nav === "default") {
return EMBEDDED_ROUTES_WITH_NAVBAR.some(pattern => return EMBEDDED_ROUTES_WITH_NAVBAR.some(pattern =>
pattern.test(pathname), pattern.test(pathname),
); );
} }
return !PATHS_WITHOUT_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 hasAppBar = useMemo(() => {
const isFullscreen = hash.includes("fullscreen"); const isFullscreen = hash.includes("fullscreen");
if ( if (
!currentUser || !currentUser ||
IFRAMED || (isEmbedded && !embedOptions.top_nav) ||
isAdminApp || isAdminApp ||
isEditingDashboard || isEditingDashboard ||
isFullscreen isFullscreen
...@@ -126,7 +135,15 @@ function App({ ...@@ -126,7 +135,15 @@ function App({
return false; return false;
} }
return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname)); return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname));
}, [currentUser, pathname, isEditingDashboard, isAdminApp, hash]); }, [
currentUser,
pathname,
isEditingDashboard,
isEmbedded,
embedOptions,
isAdminApp,
hash,
]);
return ( return (
<ErrorBoundary onError={setErrorInfo}> <ErrorBoundary onError={setErrorInfo}>
......
import querystring from "querystring"; import querystring from "querystring";
export function parseHashOptions(hash) { function parseQueryStringOptions(s) {
const options = querystring.parse(hash.replace(/^#/, "")); const options = querystring.parse(s);
for (const name in options) { for (const name in options) {
if (options[name] === "") { if (options[name] === "") {
options[name] = true; options[name] = true;
...@@ -9,9 +10,18 @@ export function parseHashOptions(hash) { ...@@ -9,9 +10,18 @@ export function parseHashOptions(hash) {
options[name] = JSON.parse(options[name]); options[name] = JSON.parse(options[name]);
} }
} }
return options; return options;
} }
export function parseHashOptions(hash) {
return parseQueryStringOptions(hash.replace(/^#/, ""));
}
export function parseSearchOptions(search) {
return parseQueryStringOptions(search.replace(/^\?/, ""));
}
export function stringifyHashOptions(options) { export function stringifyHashOptions(options) {
return querystring.stringify(options).replace(/=true\b/g, ""); return querystring.stringify(options).replace(/=true\b/g, "");
} }
......
import { push } from "react-router-redux"; import { push } from "react-router-redux";
import _ from "underscore"; import _ from "underscore";
import { parseSearchOptions } from "metabase/lib/browser";
import { IFRAMED, IFRAMED_IN_SELF } from "metabase/lib/dom"; import { IFRAMED, IFRAMED_IN_SELF } from "metabase/lib/dom";
import { setOptions } from "metabase/redux/embed";
import { isFitViewportMode } from "metabase/hoc/FitViewPort"; import { isFitViewportMode } from "metabase/hoc/FitViewPort";
// detect if this page is embedded in itself, i.e. it's a embed preview // detect if this page is embedded in itself, i.e. it's a embed preview
...@@ -42,6 +41,7 @@ export function initializeEmbedding(store) { ...@@ -42,6 +41,7 @@ export function initializeEmbedding(store) {
} }
} }
}); });
store.dispatch(setOptions(parseSearchOptions(window.location.search)));
} }
} }
......
...@@ -30,8 +30,7 @@ export const LogoLink = styled(Link)` ...@@ -30,8 +30,7 @@ export const LogoLink = styled(Link)`
justify-content: center; justify-content: center;
border-radius: 6px; border-radius: 6px;
left: 0; left: 0;
padding: ${space(1)}; padding: ${space(1)} ${space(2)};
padding-left: ${space(2)};
margin-left: ${space(2)}; margin-left: ${space(2)};
position: absolute; position: absolute;
transition: opacity 0.3s; transition: opacity 0.3s;
...@@ -58,6 +57,7 @@ export const SidebarButtonContainer = styled.div` ...@@ -58,6 +57,7 @@ export const SidebarButtonContainer = styled.div`
`; `;
export interface LeftContainerProps { export interface LeftContainerProps {
isLogoActive: boolean;
isSearchActive: boolean; isSearchActive: boolean;
} }
...@@ -70,12 +70,12 @@ export const LeftContainer = styled.div<LeftContainerProps>` ...@@ -70,12 +70,12 @@ export const LeftContainer = styled.div<LeftContainerProps>`
&:hover { &:hover {
${LogoLink} { ${LogoLink} {
opacity: 0; opacity: ${props => (props.isLogoActive ? 1 : 0)};
pointer-events: none; pointer-events: ${props => (props.isLogoActive ? "" : "none")};
} }
${SidebarButtonContainer} { ${SidebarButtonContainer} {
opacity: 1; opacity: ${props => (props.isLogoActive ? 0 : 1)};
} }
} }
......
...@@ -11,11 +11,12 @@ import SearchBar from "metabase/nav/components/SearchBar"; ...@@ -11,11 +11,12 @@ import SearchBar from "metabase/nav/components/SearchBar";
import SidebarButton from "metabase/nav/components/SidebarButton"; import SidebarButton from "metabase/nav/components/SidebarButton";
import NewButton from "metabase/nav/containers/NewButton"; 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 { getIsNavbarOpen, closeNavbar, toggleNavbar } from "metabase/redux/app";
import { isMac } from "metabase/lib/browser"; 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 { import {
AppBarRoot, AppBarRoot,
...@@ -30,6 +31,8 @@ import { ...@@ -30,6 +31,8 @@ import {
type Props = { type Props = {
isNavbarOpen: boolean; isNavbarOpen: boolean;
isEmbedded: boolean;
embedOptions: EmbedOptions;
toggleNavbar: () => void; toggleNavbar: () => void;
closeNavbar: () => void; closeNavbar: () => void;
}; };
...@@ -37,6 +40,8 @@ type Props = { ...@@ -37,6 +40,8 @@ type Props = {
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
return { return {
isNavbarOpen: getIsNavbarOpen(state), isNavbarOpen: getIsNavbarOpen(state),
isEmbedded: IFRAMED,
embedOptions: getEmbedOptions(state),
}; };
} }
...@@ -53,8 +58,17 @@ function HomepageLink({ handleClick }: { handleClick: () => void }) { ...@@ -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 [isSearchActive, setSearchActive] = useState(false);
const hasSearch = !isEmbedded || embedOptions.search;
const hasNewButton = !isEmbedded || embedOptions.new_button;
const hasSidebar = !isEmbedded || embedOptions.side_nav;
const onLogoClick = useCallback(() => { const onLogoClick = useCallback(() => {
if (isSmallScreen()) { if (isSmallScreen()) {
...@@ -83,33 +97,42 @@ function AppBar({ isNavbarOpen, toggleNavbar, closeNavbar }: Props) { ...@@ -83,33 +97,42 @@ function AppBar({ isNavbarOpen, toggleNavbar, closeNavbar }: Props) {
return ( return (
<AppBarRoot> <AppBarRoot>
<LeftContainer isSearchActive={isSearchActive}> <LeftContainer isLogoActive={!hasSidebar} isSearchActive={isSearchActive}>
<HomepageLink handleClick={onLogoClick} /> <HomepageLink handleClick={onLogoClick} />
<SidebarButtonContainer> {hasSidebar && (
<Tooltip tooltip={sidebarButtonTooltip} isEnabled={!isSmallScreen()}> <SidebarButtonContainer>
<SidebarButton <Tooltip
isSidebarOpen={isNavbarOpen} tooltip={sidebarButtonTooltip}
onClick={toggleNavbar} isEnabled={!isSmallScreen()}
/> >
</Tooltip> <SidebarButton
</SidebarButtonContainer> isSidebarOpen={isNavbarOpen}
onClick={toggleNavbar}
/>
</Tooltip>
</SidebarButtonContainer>
)}
</LeftContainer> </LeftContainer>
{!isSearchActive && ( {!isSearchActive && (
<MiddleContainer> <MiddleContainer>
<HomepageLink handleClick={onLogoClick} /> <HomepageLink handleClick={onLogoClick} />
</MiddleContainer> </MiddleContainer>
)} )}
<RightContainer> {(hasSearch || hasNewButton) && (
<SearchBarContainer> <RightContainer>
<SearchBarContent> {hasSearch && (
<SearchBar <SearchBarContainer>
onSearchActive={onSearchActive} <SearchBarContent>
onSearchInactive={onSearchInactive} <SearchBar
/> onSearchActive={onSearchActive}
</SearchBarContent> onSearchInactive={onSearchInactive}
</SearchBarContainer> />
<NewButton /> </SearchBarContent>
</RightContainer> </SearchBarContainer>
)}
{hasNewButton && <NewButton />}
</RightContainer>
)}
</AppBarRoot> </AppBarRoot>
); );
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* ducks */ /* ducks */
import app from "metabase/redux/app"; import app from "metabase/redux/app";
import embed from "metabase/redux/embed";
import requests from "metabase/redux/requests"; import requests from "metabase/redux/requests";
import settings from "metabase/redux/settings"; import settings from "metabase/redux/settings";
import undo from "metabase/redux/undo"; import undo from "metabase/redux/undo";
...@@ -14,6 +15,7 @@ import { currentUser } from "metabase/redux/user"; ...@@ -14,6 +15,7 @@ import { currentUser } from "metabase/redux/user";
export default { export default {
// global reducers // global reducers
app, app,
embed,
currentUser, currentUser,
// "entities" framework needs control over "requests" state // "entities" framework needs control over "requests" state
requests: enhanceRequestsReducer(requests), requests: enhanceRequestsReducer(requests),
......
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,
});
import { State } from "metabase-types/store";
export const getEmbedOptions = (state: State) => {
return state.embed.options;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment