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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions