# All files / src/components/layout StatusBar.tsx

0% Statements 0/87
100% Branches 1/1
100% Functions 1/1
0% Lines 0/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124

     import type { JSX } from "react";
    import { Circle, Menu, PanelLeft } from "lucide-react";
    import type { ConnectionStatus, Environment, Session } from "../../hooks/types.js";
    import { ICON_LG, ICON_XS } from "../../utils/iconSize.js";
    import { HOME_URL, useAppNavigate } from "../../utils/navigation.js";
    import { assetUrl } from "../../utils/assetUrl.js";
    import { Tooltip } from "../display/Tooltip.js";
    import styles from "./StatusBar.module.scss";

    /** Human-readable label for each connection state. */
    const CONNECTION_LABEL: Record<ConnectionStatus, string> = {
      connected: "Connected",
      connecting: "Connecting...",
      disconnected: "Disconnected",
    };

    /** CSS class for the connection dot in each state. */
    const CONNECTION_DOT_CLASS: Record<ConnectionStatus, string> = {
      connected: styles.connected,
      connecting: styles.connecting,
      disconnected: styles.disconnected,
    };

    /** Props for the StatusBar component. */
    interface StatusBarProps {
      /** Current connection state of the event stream. */
      connectionStatus: ConnectionStatus;
      /** List of all environments. */
      environments: Environment[];
      /** List of all sessions. */
      sessions: Session[];
      /** Callback to toggle the mobile sidebar drawer. */
      onToggleSidebar?: () => void;
      /** Whether the sidebar drawer is currently open (for aria-expanded). */
      sidebarOpen?: boolean;
      /** Callback to toggle the mobile context-nav drawer. */
      onToggleContextNav?: () => void;
      /** Whether the context-nav drawer is currently open (for aria-expanded). */
      contextNavOpen?: boolean;
    }

    /** Top status bar showing connection state, environment counts, and active session count. */
    export function StatusBar({
      connectionStatus,
      environments,
      sessions,
      onToggleSidebar,
      sidebarOpen,
      onToggleContextNav,
      contextNavOpen,
    }: StatusBarProps): JSX.Element {
      const navigate = useAppNavigate();
      const totalEnvs = environments.length;
      const connectedEnvs = environments.filter((e) => e.status === "connected").length;
      const activeCount = sessions.filter((s) => ["running", "idle"].includes(s.status)).length;
      const label = CONNECTION_LABEL[connectionStatus];

      return (
        <div className={styles.container}>
          {(onToggleContextNav ?? onToggleSidebar) && (
            <div className={styles.drawerControls}>
              {onToggleContextNav && (
                <button
                  type="button"
                  className={styles.hamburger}
                  onClick={onToggleContextNav}
                  aria-label="Toggle contexts"
                  aria-expanded={contextNavOpen}
                  data-testid="statusbar-context-toggle"
                >
                  <PanelLeft size={ICON_LG} aria-hidden="true" />
                </button>
              )}
              {onToggleSidebar && (
                <button
                  type="button"
                  className={styles.hamburger}
                  onClick={onToggleSidebar}
                  aria-label="Toggle sidebar"
                  aria-expanded={sidebarOpen}
                >
                  <Menu size={ICON_LG} aria-hidden="true" />
                </button>
              )}
            </div>
          )}
          <Tooltip text="Home" placement="bottom">
            <button
              type="button"
              className={styles.brand}
              onClick={() => navigate(HOME_URL)}
              data-testid="statusbar-brand"
            >
              <img
                src={assetUrl("icon-192x192.png")}
                alt=""
                className={styles.brandLogo}
                aria-hidden="true"
                data-testid="statusbar-logo"
              />
              Grackle
            </button>
          </Tooltip>
          <div className={styles.info}>
            <span aria-label={label}>
              <span
                className={`${styles.connectionDot} ${CONNECTION_DOT_CLASS[connectionStatus]}`}
                aria-hidden="true"
              >
                <Circle size={ICON_XS} fill="currentColor" />
              </span>{" "}
              <span className={styles.connectionLabel} aria-hidden="true">
                {label}
              </span>
            </span>
            <span>
              {connectedEnvs}/{totalEnvs} env{totalEnvs !== 1 ? "s" : ""}
            </span>
            <span>{activeCount} active</span>
          </div>
        </div>
      );
    }