Skip to content
Snippets Groups Projects
Unverified Commit ff0a8001 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Refactor App, AppBar and Navbar components (#21749)

* Clean up Navbar

* Connect AppBar to redux

* Move modal management to NewButton

* Fix `User` type in `Store` type

* Add `app` to `State` type

* Add `getErrorPage` selector

* Convert `App` component to TypeScript

* Turn App into a functional component

* Rename `LeftRow` / `RightRow`

* Fix opening just created collection

* Fix transition to search page
parent a4022457
Branches
Tags
No related merge requests found
Showing
with 427 additions and 292 deletions
export interface AppErrorDescriptor {
status: number;
data?: {
error_code: string;
message: string;
};
context?: string;
}
export interface AppState {
errorPage: AppErrorDescriptor | null;
isNavbarOpen: boolean;
}
export * from "./admin";
export * from "./app";
export * from "./entities";
export * from "./forms";
export * from "./settings";
......
import { AppState } from "metabase-types/store";
export const createMockAppState = (opts?: Partial<AppState>): AppState => ({
isNavbarOpen: true,
errorPage: null,
...opts,
});
export * from "./admin";
export * from "./app";
export * from "./entities";
export * from "./forms";
export * from "./settings";
......
......@@ -2,6 +2,7 @@ import { State } from "metabase-types/store";
import { createMockUser } from "metabase-types/api/mocks";
import {
createMockAdminState,
createMockAppState,
createMockSettingsState,
createMockEntitiesState,
createMockSetupState,
......@@ -9,8 +10,9 @@ import {
} from "metabase-types/store/mocks";
export const createMockState = (opts?: Partial<State>): State => ({
currentUser: createMockUser(),
admin: createMockAdminState(),
app: createMockAppState(),
currentUser: createMockUser(),
entities: createMockEntitiesState(),
form: createMockFormState(),
settings: createMockSettingsState(),
......
import { User } from "metabase-types/api";
import { AdminState } from "./admin";
import { AppState } from "./app";
import { EntitiesState } from "./entities";
import { FormState } from "./forms";
import { SettingsState } from "./settings";
import { SetupState } from "./setup";
export interface State {
currentUser: User;
admin: AdminState;
app: AppState;
currentUser: User;
entities: EntitiesState;
form: FormState;
settings: SettingsState;
......
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import React, { ErrorInfo, ReactNode, useMemo, useState } from "react";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import _ from "underscore";
import { Location } from "history";
import AppErrorCard from "metabase/components/AppErrorCard/AppErrorCard";
import Modal from "metabase/components/Modal";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import ScrollToTop from "metabase/hoc/ScrollToTop";
import AppBar from "metabase/nav/containers/AppBar";
import Navbar from "metabase/nav/containers/Navbar";
import * as Urls from "metabase/lib/urls";
import {
Archived,
NotFound,
......@@ -20,83 +13,91 @@ import {
Unauthorized,
} from "metabase/containers/ErrorPages";
import UndoListing from "metabase/containers/UndoListing";
import StatusListing from "metabase/status/containers/StatusListing";
import CollectionCreate from "metabase/collections/containers/CollectionCreate";
import { getErrorPage } from "metabase/selectors/app";
import { getUser } from "metabase/selectors/user";
import { getIsEditing as getIsEditingDashboard } from "metabase/dashboard/selectors";
import { toggleNavbar, closeNavbar, getIsNavbarOpen } from "metabase/redux/app";
import { useOnMount } from "metabase/hooks/use-on-mount";
import { IFRAMED, initializeIframeResizer } from "metabase/lib/dom";
import { AppContentContainer, AppContent } from "./App.styled";
export const MODAL_NEW_DASHBOARD = "MODAL_NEW_DASHBOARD";
export const MODAL_NEW_COLLECTION = "MODAL_NEW_COLLECTION";
import AppBar from "metabase/nav/containers/AppBar";
import Navbar from "metabase/nav/containers/Navbar";
import StatusListing from "metabase/status/containers/StatusListing";
const mapStateToProps = state => ({
currentUser: state.currentUser,
errorPage: state.app.errorPage,
isSidebarOpen: getIsNavbarOpen(state),
isEditingDashboard: getIsEditingDashboard(state),
});
import { User } from "metabase-types/api";
import { AppErrorDescriptor, State } from "metabase-types/store";
const mapDispatchToProps = {
onChangeLocation: push,
toggleNavbar,
closeNavbar,
};
import { AppContentContainer, AppContent } from "./App.styled";
const getErrorComponent = ({ status, data, context }) => {
const getErrorComponent = ({ status, data, context }: AppErrorDescriptor) => {
if (status === 403) {
return <Unauthorized />;
} else if (status === 404 || data?.error_code === "not-found") {
}
if (status === 404 || data?.error_code === "not-found") {
return <NotFound />;
} else if (
data &&
data.error_code === "archived" &&
context === "dashboard"
) {
}
if (data?.error_code === "archived" && context === "dashboard") {
return <Archived entityName="dashboard" linkTo="/dashboards/archive" />;
} else if (
data &&
data.error_code === "archived" &&
context === "query-builder"
) {
}
if (data?.error_code === "archived" && context === "query-builder") {
return <Archived entityName="question" linkTo="/questions/archive" />;
} else {
return <GenericError details={data && data.message} />;
}
return <GenericError details={data?.message} />;
};
const PATHS_WITHOUT_NAVBAR = [/\/model\/.*\/query/, /\/model\/.*\/metadata/];
const EMBEDDED_ROUTES_WITH_NAVBAR = ["/collection", "/archive"];
class App extends Component {
state = {
errorInfo: undefined,
interface AppStateProps {
currentUser?: User;
errorPage: AppErrorDescriptor | null;
isEditingDashboard: boolean;
}
interface AppRouterOwnProps {
location: Location;
children: ReactNode;
}
type AppProps = AppStateProps & AppRouterOwnProps;
function mapStateToProps(state: State): AppStateProps {
return {
currentUser: getUser(state),
errorPage: getErrorPage(state),
isEditingDashboard: getIsEditingDashboard(state),
};
}
constructor(props) {
super(props);
initializeIframeResizer();
class ErrorBoundary extends React.Component<{
onError: (errorInfo: ErrorInfo) => void;
}> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError(errorInfo);
}
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo });
render() {
return this.props.children;
}
}
isAdminApp = () => {
const { pathname } = this.props.location;
return pathname.startsWith("/admin/");
};
function App({
currentUser,
errorPage,
location: { pathname },
isEditingDashboard,
children,
}: AppProps) {
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
useOnMount(() => {
initializeIframeResizer();
});
const isAdminApp = useMemo(() => pathname.startsWith("/admin/"), [pathname]);
hasNavbar = () => {
const {
currentUser,
isEditingDashboard,
location: { pathname },
} = this.props;
const hasNavbar = useMemo(() => {
if (!currentUser || isEditingDashboard) {
return false;
}
......@@ -106,106 +107,26 @@ class App extends Component {
);
}
return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname));
};
}, [currentUser, pathname, isEditingDashboard]);
hasAppBar = () => {
const {
currentUser,
location: { pathname },
isEditingDashboard,
} = this.props;
if (!currentUser || IFRAMED || this.isAdminApp() || isEditingDashboard) {
const hasAppBar = useMemo(() => {
if (!currentUser || IFRAMED || isAdminApp || isEditingDashboard) {
return false;
}
return !PATHS_WITHOUT_NAVBAR.some(pattern => pattern.test(pathname));
};
closeModal = () => {
this.setState({ modal: null });
};
setModal = modal => {
this.setState({ modal });
if (this._newPopover) {
this._newPopover.close();
}
};
renderModalContent() {
const { modal } = this.state;
const { onChangeLocation } = this.props;
switch (modal) {
case MODAL_NEW_COLLECTION:
return (
<CollectionCreate
onClose={() => this.setState({ modal: null })}
onSaved={collection => {
this.setState({ modal: null });
onChangeLocation(Urls.collection(collection));
}}
/>
);
case MODAL_NEW_DASHBOARD:
return (
<CreateDashboardModal
onClose={() => this.setState({ modal: null })}
/>
);
default:
return null;
}
}
renderModal = () => {
const { modal } = this.state;
}, [currentUser, pathname, isEditingDashboard, isAdminApp]);
if (modal) {
return (
<Modal onClose={this.closeModal}>{this.renderModalContent()}</Modal>
);
} else {
return null;
}
};
render() {
const {
isSidebarOpen,
children,
location,
errorPage,
onChangeLocation,
toggleNavbar,
closeNavbar,
} = this.props;
const { errorInfo } = this.state;
const hasAppBar = this.hasAppBar();
return (
return (
<ErrorBoundary onError={setErrorInfo}>
<ScrollToTop>
{errorPage ? (
getErrorComponent(errorPage)
) : (
<>
{hasAppBar && (
<AppBar
isSidebarOpen={isSidebarOpen}
location={location}
onNewClick={this.setModal}
onToggleSidebarClick={toggleNavbar}
handleCloseSidebar={closeNavbar}
onChangeLocation={onChangeLocation}
/>
)}
<AppContentContainer
hasAppBar={hasAppBar}
isAdminApp={this.isAdminApp()}
>
{this.hasNavbar() && (
<Navbar isOpen={isSidebarOpen} location={location} />
)}
{hasAppBar && <AppBar />}
<AppContentContainer hasAppBar={hasAppBar} isAdminApp={isAdminApp}>
{hasNavbar && <Navbar />}
<AppContent>{children}</AppContent>
{this.renderModal()}
<UndoListing />
<StatusListing />
</AppContentContainer>
......@@ -213,8 +134,10 @@ class App extends Component {
)}
<AppErrorCard errorInfo={errorInfo} />
</ScrollToTop>
);
}
</ErrorBoundary>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
export default connect<AppStateProps, unknown, AppRouterOwnProps, State>(
mapStateToProps,
)(App);
......@@ -215,7 +215,7 @@ export class FullPageModal extends Component {
// the "routeless" version should only be used for non-inline modals
const RoutelessFullPageModal = routeless(FullPageModal);
const Modal = ({ full, ...props }) =>
const Modal = ({ full = false, ...props }) =>
full ? (
props.isOpen ? (
<RoutelessFullPageModal {...props} />
......
......@@ -12,7 +12,7 @@ import { ErrorPageRoot } from "./ErrorPages.styled";
export const GenericError = ({
title = t`Something's gone wrong`,
message = t`We've run into an error. You can try refreshing the page, or just go back.`,
details = null,
details,
}) => (
<ErrorPageRoot>
<EmptyState
......
......@@ -5,7 +5,11 @@ import React, {
useRef,
useState,
} from "react";
import _ from "underscore";
import { t } from "ttag";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { withRouter } from "react-router";
import { Location, LocationDescriptorObject } from "history";
import Icon from "metabase/components/Icon";
......@@ -32,11 +36,23 @@ const ALLOWED_SEARCH_FOCUS_ELEMENTS = new Set(["BODY", "A"]);
type SearchAwareLocation = Location<{ q?: string }>;
type Props = {
type RouterProps = {
location: SearchAwareLocation;
};
type DispatchProps = {
onChangeLocation: (nextLocation: LocationDescriptorObject) => void;
};
type OwnProps = {
onSearchActive?: () => void;
onSearchInactive?: () => void;
onChangeLocation: (nextLocation: LocationDescriptorObject) => void;
};
type Props = RouterProps & DispatchProps & OwnProps;
const mapDispatchToProps = {
onChangeLocation: push,
};
function isSearchPageLocation(location: Location) {
......@@ -181,4 +197,7 @@ function SearchBar({
</div>
);
}
export default SearchBar;
export default _.compose(
withRouter,
connect(null, mapDispatchToProps),
)(SearchBar);
......@@ -57,11 +57,11 @@ export const SidebarButtonContainer = styled.div`
}
`;
export interface RowLeftProps {
export interface LeftContainerProps {
isSearchActive: boolean;
}
export const RowLeft = styled.div<RowLeftProps>`
export const LeftContainer = styled.div<LeftContainerProps>`
display: flex;
height: 100%;
flex-direction: row;
......@@ -93,7 +93,7 @@ export const RowLeft = styled.div<RowLeftProps>`
}
`;
export const RowMiddle = styled.div`
export const MiddleContainer = styled.div`
display: none;
justify-content: center;
width: 80px;
......@@ -109,7 +109,7 @@ export const RowMiddle = styled.div`
}
`;
export const RowRight = styled.div`
export const RightContainer = styled.div`
display: flex;
height: 100%;
flex-direction: row;
......
import React, { useCallback, useMemo, useState } from "react";
import { t } from "ttag";
import { Location, LocationDescriptorObject } from "history";
import _ from "underscore";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import Link from "metabase/core/components/Link";
import Tooltip from "metabase/components/Tooltip";
import LogoIcon from "metabase/components/LogoIcon";
......@@ -10,7 +11,9 @@ import SearchBar from "metabase/nav/components/SearchBar";
import SidebarButton from "metabase/nav/components/SidebarButton";
import NewButton from "metabase/nav/containers/NewButton";
import Database from "metabase/entities/databases";
import { 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";
......@@ -19,19 +22,27 @@ import {
LogoLink,
SearchBarContainer,
SearchBarContent,
RowLeft,
RowMiddle,
RowRight,
LeftContainer,
MiddleContainer,
RightContainer,
SidebarButtonContainer,
} from "./AppBar.styled";
type Props = {
isSidebarOpen: boolean;
location: Location;
onNewClick: (modalName: string) => void;
onToggleSidebarClick: () => void;
handleCloseSidebar: () => void;
onChangeLocation: (nextLocation: LocationDescriptorObject) => void;
isNavbarOpen: boolean;
toggleNavbar: () => void;
closeNavbar: () => void;
};
function mapStateToProps(state: State) {
return {
isNavbarOpen: getIsNavbarOpen(state),
};
}
const mapDispatchToProps = {
toggleNavbar,
closeNavbar,
};
function HomepageLink({ handleClick }: { handleClick: () => void }) {
......@@ -42,28 +53,21 @@ function HomepageLink({ handleClick }: { handleClick: () => void }) {
);
}
function AppBar({
isSidebarOpen,
location,
onNewClick,
onToggleSidebarClick,
handleCloseSidebar,
onChangeLocation,
}: Props) {
function AppBar({ isNavbarOpen, toggleNavbar, closeNavbar }: Props) {
const [isSearchActive, setSearchActive] = useState(false);
const onLogoClick = useCallback(() => {
if (isSmallScreen()) {
handleCloseSidebar();
closeNavbar();
}
}, [handleCloseSidebar]);
}, [closeNavbar]);
const onSearchActive = useCallback(() => {
if (isSmallScreen()) {
setSearchActive(true);
handleCloseSidebar();
closeNavbar();
}
}, [handleCloseSidebar]);
}, [closeNavbar]);
const onSearchInactive = useCallback(() => {
if (isSmallScreen()) {
......@@ -72,44 +76,45 @@ function AppBar({
}, []);
const sidebarButtonTooltip = useMemo(() => {
const message = isSidebarOpen ? t`Close sidebar` : t`Open sidebar`;
const message = isNavbarOpen ? t`Close sidebar` : t`Open sidebar`;
const shortcut = isMac() ? "(⌘ + .)" : "(Ctrl + .)";
return `${message} ${shortcut}`;
}, [isSidebarOpen]);
}, [isNavbarOpen]);
return (
<AppBarRoot>
<RowLeft isSearchActive={isSearchActive}>
<LeftContainer isSearchActive={isSearchActive}>
<HomepageLink handleClick={onLogoClick} />
<SidebarButtonContainer>
<Tooltip tooltip={sidebarButtonTooltip} isEnabled={!isSmallScreen()}>
<SidebarButton
isSidebarOpen={isSidebarOpen}
onClick={onToggleSidebarClick}
isSidebarOpen={isNavbarOpen}
onClick={toggleNavbar}
/>
</Tooltip>
</SidebarButtonContainer>
</RowLeft>
</LeftContainer>
{!isSearchActive && (
<RowMiddle>
<MiddleContainer>
<HomepageLink handleClick={onLogoClick} />
</RowMiddle>
</MiddleContainer>
)}
<RowRight>
<RightContainer>
<SearchBarContainer>
<SearchBarContent>
<SearchBar
location={location}
onChangeLocation={onChangeLocation}
onSearchActive={onSearchActive}
onSearchInactive={onSearchInactive}
/>
</SearchBarContent>
</SearchBarContainer>
<NewButton setModal={onNewClick} />
</RowRight>
<NewButton />
</RightContainer>
</AppBarRoot>
);
}
export default Database.loadList({ loadingAndErrorWrapper: false })(AppBar);
export default _.compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
)(AppBar);
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import Icon from "metabase/components/Icon";
import { NAV_SIDEBAR_WIDTH } from "metabase/nav/constants";
......@@ -11,6 +12,45 @@ import {
} from "metabase/styled-components/theme";
import { SidebarLink } from "./SidebarItems";
const openSidebarCSS = css`
width: ${NAV_SIDEBAR_WIDTH};
border-right: 1px solid ${color("border")};
${breakpointMaxSmall} {
width: 90vw;
}
`;
export const Sidebar = styled.aside<{ isOpen: boolean }>`
width: 0;
height: 100%;
position: relative;
flex-shrink: 0;
align-items: center;
padding: 0.5rem 0;
background-color: ${color("nav")};
overflow: auto;
overflow-x: hidden;
z-index: 4;
transition: width 0.2s;
@media (prefers-reduced-motion) {
transition: none;
}
${props => props.isOpen && openSidebarCSS};
${breakpointMaxSmall} {
position: absolute;
top: 0;
left: 0;
}
`;
export const NavRoot = styled.nav<{ isOpen: boolean }>`
display: flex;
flex-direction: column;
......
......@@ -28,7 +28,12 @@ import * as Urls from "metabase/lib/urls";
import { SelectedItem } from "./types";
import MainNavbarView from "./MainNavbarView";
import { NavRoot, LoadingContainer, LoadingTitle } from "./MainNavbar.styled";
import {
Sidebar,
NavRoot,
LoadingContainer,
LoadingTitle,
} from "./MainNavbar.styled";
function mapStateToProps(state: unknown) {
return {
......@@ -173,28 +178,32 @@ function MainNavbarContainer({
};
return (
<NavRoot isOpen={isOpen}>
{allFetched && rootCollection ? (
<MainNavbarView
{...props}
bookmarks={orderedBookmarks.length > 0 ? orderedBookmarks : bookmarks}
isOpen={isOpen}
currentUser={currentUser}
collections={collectionTree}
hasOwnDatabase={hasOwnDatabase}
selectedItem={selectedItem}
hasDataAccess={hasDataAccess}
reorderBookmarks={reorderBookmarks}
handleCloseNavbar={closeNavbar}
handleLogout={logout}
/>
) : (
<LoadingContainer>
<LoadingSpinner />
<LoadingTitle>{t`Loading…`}</LoadingTitle>
</LoadingContainer>
)}
</NavRoot>
<Sidebar className="Nav" isOpen={isOpen} aria-hidden={!isOpen}>
<NavRoot isOpen={isOpen}>
{allFetched && rootCollection ? (
<MainNavbarView
{...props}
bookmarks={
orderedBookmarks.length > 0 ? orderedBookmarks : bookmarks
}
isOpen={isOpen}
currentUser={currentUser}
collections={collectionTree}
hasOwnDatabase={hasOwnDatabase}
selectedItem={selectedItem}
hasDataAccess={hasDataAccess}
reorderBookmarks={reorderBookmarks}
handleCloseNavbar={closeNavbar}
handleLogout={logout}
/>
) : (
<LoadingContainer>
<LoadingSpinner />
<LoadingTitle>{t`Loading…`}</LoadingTitle>
</LoadingContainer>
)}
</NavRoot>
</Sidebar>
);
}
......
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { withRouter } from "react-router";
import Link from "metabase/core/components/Link";
import LogoIcon from "metabase/components/LogoIcon";
import { AdminNavbar } from "../components/AdminNavbar";
import { getPath, getContext, getUser } from "../selectors";
import { getHasDataAccess } from "metabase/new_query/selectors";
import Database from "metabase/entities/databases";
const mapStateToProps = (state, props) => ({
path: getPath(state, props),
context: getContext(state, props),
user: getUser(state),
hasDataAccess: getHasDataAccess(state),
});
import { Sidebar } from "./Navbar.styled";
import MainNavbar from "./MainNavbar";
const mapDispatchToProps = {
onChangeLocation: push,
};
@Database.loadList({
// set this to false to prevent a potential spinner on the main nav
loadingAndErrorWrapper: false,
})
@withRouter
@connect(mapStateToProps, mapDispatchToProps)
export default class Navbar extends Component {
static propTypes = {
context: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
user: PropTypes.object,
};
isActive(path) {
return this.props.path.startsWith(path);
}
renderEmptyNav() {
return (
// NOTE: DO NOT REMOVE `Nav` CLASS FOR NOW, USED BY MODALS, FULLSCREEN DASHBOARD, ETC
// TODO: hide nav using state in redux instead?
<nav className="Nav sm-py1 relative">
<ul className="wrapper flex align-center">
<li>
<Link
to="/"
data-metabase-event={"Navbar;Logo"}
className="NavItem cursor-pointer flex align-center"
>
<LogoIcon className="text-brand my2" />
</Link>
</li>
</ul>
</nav>
);
}
renderMainNav() {
const { isOpen, location, params } = this.props;
// NOTE: DO NOT REMOVE `Nav` CLASS FOR NOW, USED BY MODALS, FULLSCREEN DASHBOARD, ETC
return (
<Sidebar className="Nav" isOpen={isOpen} aria-hidden={!isOpen}>
<MainNavbar isOpen={isOpen} location={location} params={params} />
</Sidebar>
);
}
render() {
const { context, user } = this.props;
if (!user) {
return null;
}
switch (context) {
case "admin":
return <AdminNavbar {...this.props} />;
case "auth":
return null;
case "none":
return this.renderEmptyNav();
case "setup":
return null;
default:
return this.renderMainNav();
}
}
}
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import { color } from "metabase/lib/colors";
import { NAV_SIDEBAR_WIDTH } from "../constants";
import {
breakpointMaxSmall,
breakpointMinSmall,
} from "metabase/styled-components/theme";
const openNavbarCSS = css`
width: ${NAV_SIDEBAR_WIDTH};
border-right: 1px solid ${color("border")};
${breakpointMaxSmall} {
width: 90vw;
}
`;
export const Sidebar = styled.aside<{ isOpen: boolean }>`
width: 0;
height: 100%;
position: relative;
flex-shrink: 0;
align-items: center;
padding: 0.5rem 0;
background-color: ${color("nav")};
overflow: auto;
overflow-x: hidden;
z-index: 4;
transition: width 0.2s;
@media (prefers-reduced-motion) {
transition: none;
}
${props => props.isOpen && openNavbarCSS};
${breakpointMaxSmall} {
position: absolute;
top: 0;
left: 0;
}
`;
export const LogoIconContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-width: 2rem;
height: 2rem;
`;
export const EntityMenuContainer = styled.div`
display: flex;
position: relative;
align-items: center;
padding-left: 0.5rem;
z-index: 2;
${breakpointMinSmall} {
padding-left: 1rem;
}
`;
import React, { useMemo } from "react";
import _ from "underscore";
import { Location } from "history";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import Database from "metabase/entities/databases";
import { getIsNavbarOpen } from "metabase/redux/app";
import { getUser } from "metabase/selectors/user";
import { User } from "metabase-types/api";
import { State } from "metabase-types/store";
import { AdminNavbar } from "../components/AdminNavbar";
import MainNavbar from "./MainNavbar";
type NavbarProps = {
isOpen: boolean;
user: User;
location: Location;
params: Record<string, unknown>;
};
const mapStateToProps = (state: State) => ({
isOpen: getIsNavbarOpen(state),
user: getUser(state),
});
function Navbar({ isOpen, user, location, params }: NavbarProps) {
const isAdminApp = useMemo(() => location.pathname.startsWith("/admin/"), [
location.pathname,
]);
if (!user) {
return null;
}
return isAdminApp ? (
<AdminNavbar user={user} path={location.pathname} />
) : (
<MainNavbar isOpen={isOpen} location={location} params={params} />
);
}
export default _.compose(
Database.loadList({
loadingAndErrorWrapper: false,
}),
withRouter,
connect(mapStateToProps),
)(Navbar);
import React, { useMemo } from "react";
import { connect } from "react-redux";
import React, { useCallback, useMemo, useState } from "react";
import { t } from "ttag";
import _ from "underscore";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { LocationDescriptor } from "history";
import Icon from "metabase/components/Icon";
import Modal from "metabase/components/Modal";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import { Collection } from "metabase-types/api";
import { State } from "metabase-types/store";
import CollectionCreate from "metabase/collections/containers/CollectionCreate";
import { closeNavbar } from "metabase/redux/app";
import * as Urls from "metabase/lib/urls";
import {
......@@ -16,8 +23,10 @@ import {
import { Menu, StyledButton, Title } from "./NewButton.styled";
const MODAL_NEW_DASHBOARD = "MODAL_NEW_DASHBOARD";
const MODAL_NEW_COLLECTION = "MODAL_NEW_COLLECTION";
type NewButtonModal = "MODAL_NEW_DASHBOARD" | "MODAL_NEW_COLLECTION" | null;
const MODAL_NEW_DASHBOARD: NewButtonModal = "MODAL_NEW_DASHBOARD";
const MODAL_NEW_COLLECTION: NewButtonModal = "MODAL_NEW_COLLECTION";
interface NewButtonStateProps {
hasDataAccess: boolean;
......@@ -26,17 +35,11 @@ interface NewButtonStateProps {
}
interface NewButtonDispatchProps {
onChangeLocation: (nextLocation: LocationDescriptor) => void;
closeNavbar: () => void;
}
interface NewButtonOwnProps {
setModal: (modalName: string) => void;
}
interface NewButtonProps
extends NewButtonOwnProps,
NewButtonStateProps,
NewButtonDispatchProps {}
interface NewButtonProps extends NewButtonStateProps, NewButtonDispatchProps {}
const mapStateToProps: (state: State) => NewButtonStateProps = state => ({
hasDataAccess: getHasDataAccess(state),
......@@ -45,6 +48,7 @@ const mapStateToProps: (state: State) => NewButtonStateProps = state => ({
});
const mapDispatchToProps = {
onChangeLocation: push,
closeNavbar,
};
......@@ -52,9 +56,31 @@ function NewButton({
hasDataAccess,
hasNativeWrite,
hasDbWithJsonEngine,
setModal,
onChangeLocation,
closeNavbar,
}: NewButtonProps) {
const [modal, setModal] = useState<NewButtonModal>(null);
const closeModal = useCallback(() => setModal(null), []);
const renderModalContent = useCallback(() => {
if (modal === MODAL_NEW_COLLECTION) {
return (
<CollectionCreate
onClose={closeModal}
onSaved={(collection: Collection) => {
closeModal();
onChangeLocation(Urls.collection(collection));
}}
/>
);
}
if (modal === MODAL_NEW_DASHBOARD) {
return <CreateDashboardModal onClose={closeModal} />;
}
return null;
}, [modal, closeModal, onChangeLocation]);
const menuItems = useMemo(() => {
const items = [];
......@@ -109,28 +135,28 @@ function NewButton({
]);
return (
<Menu
trigger={
<StyledButton
primary
icon="add"
iconSize={14}
data-metabase-event="NavBar;Create Menu Click"
>
<Title>{t`New`}</Title>
</StyledButton>
}
items={menuItems}
/>
<>
<Menu
trigger={
<StyledButton
primary
icon="add"
iconSize={14}
data-metabase-event="NavBar;Create Menu Click"
>
<Title>{t`New`}</Title>
</StyledButton>
}
items={menuItems}
/>
{modal && <Modal onClose={closeModal}>{renderModalContent()}</Modal>}
</>
);
}
export default connect<
NewButtonStateProps,
NewButtonDispatchProps,
NewButtonOwnProps,
State
>(
mapStateToProps,
mapDispatchToProps,
export default _.compose(
connect<NewButtonStateProps, NewButtonDispatchProps, unknown, State>(
mapStateToProps,
mapDispatchToProps,
),
)(NewButton);
import { createSelector } from "reselect";
export { getUser } from "metabase/selectors/user";
export const getPath = (state, props) => props.location.pathname;
export const getContext = createSelector([getPath], path =>
path.startsWith("/auth/")
? "auth"
: path.startsWith("/setup/")
? "setup"
: path.startsWith("/admin/")
? "admin"
: path === "/"
? "home"
: "main",
);
......@@ -11,7 +11,7 @@ import { t } from "ttag";
import { loadCurrentUser } from "metabase/redux/user";
import MetabaseSettings from "metabase/lib/settings";
import App from "metabase/App.jsx";
import App from "metabase/App.tsx";
import ActivityApp from "metabase/home/containers/ActivityApp";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment