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
Tags v0.49.3 v1.49.3
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 "./app";
export * from "./embed";
export * from "./entities";
export * from "./forms";
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 "./app";
export * from "./embed";
export * from "./entities";
export * from "./forms";
export * from "./qb";
......
......@@ -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(),
......
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;
......
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}>
......
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, "");
}
......
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)));
}
}
......
......@@ -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)};
}
}
......
......@@ -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>
);
}
......
......@@ -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),
......
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