Skip to content

refactor(console): decompose AppLayout into smaller subcomponents #276

Description

@r2dedios

Problem

console/src/app/AppLayout/AppLayout.tsx is a 277-line monolith that mixes several unrelated concerns:

  1. Theme management (lines 43-152): dark/light state, localStorage persistence, OS preference listener, CSS class toggle
  2. Help dropdown (lines 53-82, 159-182): link definitions, dropdown state, JSX
  3. User dropdown (lines 84-101, 194-213): logout handler, dropdown state, JSX
  4. Sidebar (lines 103-125, 256-262): responsive open/close, resize listener
  5. Masthead / branding (lines 219-254): logo, title, toggle button

This makes the component hard to navigate and modify. For example, changing theme behavior requires scanning past unrelated dropdown logic.

Current Structure

AppLayout
├── State: isSidebarOpen, isHelpMenuOpen, isAboutModalOpen, isUserDropdownOpen, isDarkTheme
├── Data: defaultHelpLinks, helpDropdownItems, userDropdownItems
├── Handlers: handleLogout, onUserDropdownToggle/Select, onResize, onSidebarToggle, toggleTheme
├── Effects: resize listener, theme class toggle, OS preference listener
├── JSX vars: headerToolbar, header, sidebar
└── Return: <Page> + <AboutModalComponent>

Proposed Solution

Extract three self-contained subcomponents into console/src/app/AppLayout/:

1. ThemeToggle.tsx (~40 lines)

Owns isDarkTheme state, localStorage persistence, OS preference listener, and the toggle button JSX.

interface ThemeToggleProps {}

export const ThemeToggle: React.FC<ThemeToggleProps> = () => {
  const [isDarkTheme, setIsDarkTheme] = useState(() => { ... });
  // effects for class toggle and media query listener
  return (
    <Tooltip content={...}>
      <MenuToggle variant="plain" onClick={toggleTheme} ...>
        {isDarkTheme ? <SunIcon /> : <MoonIcon />}
      </MenuToggle>
    </Tooltip>
  );
};

2. HelpMenu.tsx (~50 lines)

Owns isHelpMenuOpen state, link definitions, and the Dropdown JSX. Also renders AboutModalComponent since it's triggered from here.

export const HelpMenu: React.FC = () => {
  const [isHelpMenuOpen, setIsHelpMenuOpen] = useState(false);
  const [isAboutModalOpen, setIsAboutModalOpen] = useState(false);
  // ...
  return (
    <>
      <Dropdown ...>{helpDropdownItems}</Dropdown>
      <AboutModalComponent isOpen={isAboutModalOpen} onClose={...} />
    </>
  );
};

3. UserMenu.tsx (~40 lines)

Owns isUserDropdownOpen state, logout handler, and the Dropdown JSX.

export const UserMenu: React.FC = () => {
  const { userEmail, setUserEmail } = useUser();
  const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
  // ...
  return <Dropdown ...>{userDropdownItems}</Dropdown>;
};

Result in AppLayout

After extraction, AppLayout shrinks to ~100 lines handling only page structure and sidebar:

const AppLayout: React.FC<IAppLayout> = ({ children }) => {
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  // resize effect
  const headerToolbar = (
    <Toolbar>
      <ToolbarContent>
        <ToolbarGroup align={{ default: 'alignEnd' }}>
          <ToolbarItem><HelpMenu /></ToolbarItem>
          <ToolbarItem><ThemeToggle /></ToolbarItem>
          <ToolbarItem><UserMenu /></ToolbarItem>
        </ToolbarGroup>
      </ToolbarContent>
    </Toolbar>
  );
  // masthead, sidebar, return <Page>
};

Files to Create

  • console/src/app/AppLayout/ThemeToggle.tsx
  • console/src/app/AppLayout/HelpMenu.tsx
  • console/src/app/AppLayout/UserMenu.tsx

Files to Modify

  • console/src/app/AppLayout/AppLayout.tsx — remove extracted code, import new components

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions