Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions console/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ import { FIXED_TOP_BAR_Z_INDEX, MAIN_CONTENT_Z_INDEX } from "./zIndex";
export interface BaseLayoutProps {
children?: React.ReactNode;
containerProps?: FlexProps;
/**
* Suppresses the welcome dialog that pops when a region first becomes
* healthy. Used by flows with their own region-ready affordances, so the
* dialog doesn't cover them.
*/
hideWelcomeDialog?: boolean;
/** Renders only account-scoped navigation. See NavBarProps.accountOnly. */
accountOnlyNav?: boolean;
navBarOverride?: React.FunctionComponent;
sectionNav?: React.ReactNode;
}
Expand Down Expand Up @@ -98,15 +106,18 @@ export const BaseLayout = (props: BaseLayoutProps) => {
data-testid="page-layout"
{...props.containerProps}
>
<WelcomeDialog />
{!props.hideWelcomeDialog && <WelcomeDialog />}
<MfaAlert />
<ImpersonationAlert />
<Flex
direction={{ base: "column", lg: "row" }}
flexGrow="1"
minHeight="0"
>
<NavigationBar isCollapsed={Boolean(props.sectionNav)} />
<NavigationBar
isCollapsed={Boolean(props.sectionNav)}
accountOnly={props.accountOnlyNav}
/>
<HStack
alignItems="stretch"
flexGrow="1"
Expand Down
55 changes: 55 additions & 0 deletions console/src/layouts/NavBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

import { screen } from "@testing-library/react";
import React from "react";

import server from "~/api/mocks/server";
import {
disabledEnvironment,
healthyEnvironment,
renderComponent,
setFakeEnvironment,
} from "~/test/utils";

import { NavBar } from "./NavBar";

describe("NavBar", () => {
afterEach(() => {
server.resetHandlers();
vi.clearAllMocks();
});

it("renders the Admin group after the region-scoped items", async () => {
await renderComponent(<NavBar isCollapsed={false} />, {
initializeState: ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", healthyEnvironment),
});
const clusters = await screen.findByText("Clusters");
const admin = screen.getByText("Admin");
// Admin is pinned to the bottom of the menu, so it must come after the
// region-scoped items in document order.
expect(
clusters.compareDocumentPosition(admin) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(screen.getByText("Create New")).toBeInTheDocument();
});

it("hides the create button and region items without an enabled environment", async () => {
await renderComponent(<NavBar isCollapsed={false} />, {
initializeState: ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", disabledEnvironment),
});
expect(await screen.findByText("Admin")).toBeInTheDocument();
expect(screen.queryByText("Create New")).not.toBeInTheDocument();
expect(screen.queryByText("Clusters")).not.toBeInTheDocument();
expect(screen.queryByText("SQL Shell")).not.toBeInTheDocument();
});
});
24 changes: 20 additions & 4 deletions console/src/layouts/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,15 @@ export const NavBarEnvironmentSelect = () => {

export interface NavBarProps {
isCollapsed: boolean;
/**
* Renders only account-scoped navigation (the Admin group), hiding
* region-scoped items and object creation. Used by flows where no
* environment is usable yet, such as environment-not-ready.
*/
accountOnly?: boolean;
}

export const NavBar = ({ isCollapsed }: NavBarProps) => {
export const NavBar = ({ isCollapsed, accountOnly }: NavBarProps) => {
const {
isOpen: isConnectModalOpen,
onClose: onCloseConnectModal,
Expand Down Expand Up @@ -224,32 +230,42 @@ export const NavBar = ({ isCollapsed }: NavBarProps) => {
offsetY={navBarContainerHeight}
runtimeConfig={runtimeConfig}
isMobile={true}
accountOnly={accountOnly}
/>
)}
selfManagedConfigElement={
<SelfManagedNavMenu
closeMenu={closeMobileNav}
offsetY={navBarContainerHeight}
isMobile={true}
accountOnly={accountOnly}
/>
}
/>
</Box>
)}
<CreateObjectButton isCollapsed={isCollapsed} />
{!accountOnly && (
<HideIfEnvironmentDisabled>
<CreateObjectButton isCollapsed={isCollapsed} />
</HideIfEnvironmentDisabled>
)}
<AppConfigSwitch
cloudConfigElement={({ runtimeConfig }) => (
<CloudNavMenu
isCollapsed={isCollapsed}
runtimeConfig={runtimeConfig}
isMobile={false}
accountOnly={accountOnly}
/>
)}
selfManagedConfigElement={
<SelfManagedNavMenu isCollapsed={isCollapsed} isMobile={false} />
<SelfManagedNavMenu
isCollapsed={isCollapsed}
isMobile={false}
accountOnly={accountOnly}
/>
}
/>
{!isMobile && <Spacer />}
{!isMobile && !isCollapsed && (
<FreeTrialNotice mb="4" mx={{ lg: "4" }} />
)}
Expand Down
60 changes: 48 additions & 12 deletions console/src/layouts/NavBar/NavMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type NavItemType = {
navItems?: NavItemType[];
onClick?: () => void;
forceShow?: boolean;
/** Renders the item at the bottom of the nav menu, below a spacer. */
pinToBottom?: boolean;
};

const getNavItems = ({
Expand Down Expand Up @@ -144,6 +146,7 @@ const getNavItems = ({
icon: <AdminIcon />,
label: "Admin",
forceShow: true,
pinToBottom: true,
navItems: [
...(canViewAppPasswords
? [
Expand Down Expand Up @@ -253,9 +256,17 @@ const useSelfManagedNavMenuItems = () => {
};

const NavMenu = (props: { isCollapsed?: boolean; items: NavItemType[] }) => {
const topItems = props.items.filter((item) => !item.pinToBottom);
const bottomItems = props.items.filter((item) => item.pinToBottom);
return (
<NavMenuContainer>
{props.items.map((item) => (
{topItems.map((item) => (
<HideIfEnvironmentDisabled key={item.label} forceShow={item.forceShow}>
<NavItem key={item.label} {...item} isCollapsed={props.isCollapsed} />
</HideIfEnvironmentDisabled>
))}
<Spacer />
{bottomItems.map((item) => (
<HideIfEnvironmentDisabled key={item.label} forceShow={item.forceShow}>
<NavItem key={item.label} {...item} isCollapsed={props.isCollapsed} />
</HideIfEnvironmentDisabled>
Expand Down Expand Up @@ -285,16 +296,30 @@ const NavMenuMobile = (props: {
overflowY="auto"
>
<VStack px="4" py="6">
{props.items.map((item) => (
<HideIfEnvironmentDisabled
key={item.label}
forceShow={item.forceShow}
>
<NavItem key={item.label} closeMenu={props.closeMenu} {...item} />
</HideIfEnvironmentDisabled>
))}
{props.items
.filter((item) => !item.pinToBottom)
.map((item) => (
<HideIfEnvironmentDisabled
key={item.label}
forceShow={item.forceShow}
>
<NavItem key={item.label} closeMenu={props.closeMenu} {...item} />
</HideIfEnvironmentDisabled>
))}
</VStack>
<Spacer />
<VStack px="4" pb="2">
{props.items
.filter((item) => item.pinToBottom)
.map((item) => (
<HideIfEnvironmentDisabled
key={item.label}
forceShow={item.forceShow}
>
<NavItem key={item.label} closeMenu={props.closeMenu} {...item} />
</HideIfEnvironmentDisabled>
))}
</VStack>
<VStack align="stretch">
<FreeTrialNotice />
<VStack>
Expand Down Expand Up @@ -347,6 +372,8 @@ const NavMenuMobile = (props: {

type BaseCloudNavMenuProps = {
runtimeConfig: CloudRuntimeConfig;
/** Renders only account-scoped (forceShow) items. */
accountOnly?: boolean;
};

type MobileNavMenuProps = {
Expand All @@ -365,9 +392,12 @@ export const CloudNavMenu = (
| (BaseCloudNavMenuProps & DesktopNavMenuProps)
| (BaseCloudNavMenuProps & MobileNavMenuProps),
) => {
const items = useCloudNavMenuItems({
const allItems = useCloudNavMenuItems({
runtimeConfig: props.runtimeConfig,
});
const items = props.accountOnly
? allItems.filter((item) => item.forceShow)
: allItems;

if (props.isMobile) {
return <NavMenuMobile {...props} items={items} />;
Expand All @@ -377,9 +407,15 @@ export const CloudNavMenu = (
};

export const SelfManagedNavMenu = (
props: DesktopNavMenuProps | MobileNavMenuProps,
props: (DesktopNavMenuProps | MobileNavMenuProps) & {
/** Renders only account-scoped (forceShow) items. */
accountOnly?: boolean;
},
) => {
const items = useSelfManagedNavMenuItems();
const allItems = useSelfManagedNavMenuItems();
const items = props.accountOnly
? allItems.filter((item) => item.forceShow)
: allItems;

if (props.isMobile) {
return <NavMenuMobile {...props} items={items} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import server from "~/api/mocks/server";
import { dummyValidUser } from "~/external-library-wrappers/__mocks__/frontegg";
import {
disabledEnvironment,
healthyEnvironment,
InitializeStateFn,
renderComponent,
RenderWithPathname,
Expand Down Expand Up @@ -91,4 +92,45 @@ describe("EnvironmentNotReadyRoutes", () => {
);
});
});

it("shows account-level navigation without an enabled environment", async () => {
await renderRoutes(["/enable-region"]);
// Admin items are account-scoped and must stay reachable while no
// environment is enabled.
expect(await screen.findByText("Admin")).toBeInTheDocument();
expect(screen.getByText("App Passwords")).toBeInTheDocument();
expect(screen.getByText("Usage & Billing")).toBeInTheDocument();
// Region-scoped items and object creation are hidden in this flow.
expect(screen.queryByText("Clusters")).not.toBeInTheDocument();
expect(screen.queryByText("SQL Shell")).not.toBeInTheDocument();
expect(screen.queryByText("Create New")).not.toBeInTheDocument();
});

it("keeps the nav account-only in this flow regardless of environment health", async () => {
// The nav in this flow is gated on the route, not on health state, so a
// transient "crashed" reading during boot cannot flash the full sidebar.
await renderRoutes(["/creating-environment"], ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", {
...healthyEnvironment,
status: { ...healthyEnvironment.status, health: "crashed" },
}),
);
expect(await screen.findByText("Admin")).toBeInTheDocument();
expect(screen.queryByText("Clusters")).not.toBeInTheDocument();
expect(screen.queryByText("Create New")).not.toBeInTheDocument();
});

it("does not show the welcome dialog when a region becomes ready", async () => {
// The dialog would cover the flow's own region-ready affordances (the
// toast and the tutorial's "Open console" button).
await renderRoutes(["/creating-environment"], ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", healthyEnvironment),
);
expect(
await screen.findByText("We’re creating your environment"),
).toBeVisible();
expect(
screen.queryByTestId("welcome-dialog-close-button"),
).not.toBeInTheDocument();
});
});
Loading
Loading