LCOV - code coverage report
Current view: top level - pkg/shell - shell.tsx Coverage Total Hit
Test: cockpit Lines: 87.1 % 140 122
Test Date: 2026-06-25 11:17:56

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          341 : import cockpit from "cockpit";
       7              : 
       8          341 : import React, { useEffect } from 'react';
       9          341 : import { createRoot } from "react-dom/client";
      10              : 
      11              : import {
      12              :     Modal, ModalBody, ModalFooter, ModalHeader
      13              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      14              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      15              : 
      16              : import { WithDialogs } from "dialogs.jsx";
      17              : import { useInit, useEvent, useOn, useLoggedInUser } from "hooks";
      18              : 
      19              : import { TopNav } from "./topnav.jsx";
      20              : import { SidebarToggle, PageNav } from "./nav.jsx";
      21              : import { CockpitHosts, CockpitCurrentHost } from "./hosts.jsx";
      22              : import { HostModalState, HostModal, connect_host } from "./hosts_dialog.jsx";
      23              : import { Frames } from "./frames.jsx";
      24              : import { EarlyFailure, Disconnected, MachineTroubleshoot } from "./failures.jsx";
      25              : 
      26              : import { ShellState } from "./state.jsx";
      27              : import { get_session_controller } from "./session";
      28              : 
      29              : import { SkipToContent } from '@patternfly/react-core/dist/esm/components/SkipToContent/index.js';
      30              : 
      31              : import 'cockpit-dark-theme'; // once per page
      32              : 
      33              : import '../lib/patternfly/patternfly-6-cockpit.scss';
      34              : import "./shell.scss";
      35              : 
      36          341 : const _ = cockpit.gettext;
      37              : 
      38              : interface SkipLinkProps {
      39              :     focus_id: string;
      40              :     children?: React.ReactNode
      41              : }
      42              : 
      43              : // We create a wrapper specifically to handle edge-cases like headless browsers
      44              : // not getting focus when clicking an anchor to a heading. We can also scroll
      45              : // it into view in case it wouldn't do that before - just to be safe.
      46          338 : const SkipLink = ({ focus_id, children }: SkipLinkProps) => {
      47          338 :     return (
      48          338 :         <SkipToContent
      49          338 :             href={`#${focus_id}`}
      50            0 :             onClick={ev => {
      51            0 :                 const el = document.getElementById(focus_id);
      52            0 :                 el?.focus();
      53            0 :                 el?.scrollIntoView();
      54            0 :                 ev.preventDefault();
      55            0 :             }}>
      56          338 :             {children}
      57          338 :         </SkipToContent>
      58              :     );
      59          338 : };
      60              : 
      61          338 : const SessionCountdownModal = () => {
      62          338 :     const controller = get_session_controller();
      63          338 :     useOn(controller, "changed");
      64              : 
      65          338 :     controller.inhibit_activity_reporting(controller.countdown > 0);
      66              : 
      67          338 :     if (controller.countdown <= 0)
      68          338 :         return null;
      69              : 
      70           63 :     return (
      71           63 :         <Modal isOpen position="top" variant="medium"
      72           63 :                id="session-timeout-modal">
      73           63 :             <ModalHeader title={_("Session is about to expire")} />
      74           63 :             <ModalBody>
      75           63 :                 { cockpit.format(_("You will be logged out in $0 seconds."), controller.countdown) }
      76           63 :             </ModalBody>
      77           63 :             <ModalFooter>
      78           63 :                 <Button variant='primary'
      79            0 :                     onClick={() => controller.continue_session()}
      80              :                 >
      81           63 :                     {_("Continue session")}
      82           63 :                 </Button>
      83           63 :             </ModalFooter>
      84           63 :         </Modal>
      85              :     );
      86          338 : };
      87              : 
      88          341 : const Shell = () => {
      89          338 :     const current_user = useLoggedInUser()?.name || "";
      90          341 :     const state = useInit(() => new ShellState());
      91          341 :     const session_controller = get_session_controller();
      92          341 :     const host_modal_state = useInit(() => HostModalState());
      93              : 
      94          341 :     useOn(state, "update");
      95          341 :     useOn(session_controller, "changed");
      96          341 :     useEvent(host_modal_state, "changed");
      97              : 
      98          341 :     useEffect(() => {
      99            8 :         return state.on("connect", () => {
     100            8 :             connect_host(host_modal_state, state, state.current_machine);
     101            8 :         });
     102          341 :     }, [host_modal_state, state]);
     103              : 
     104          341 :     const {
     105          341 :         ready,
     106          341 :         config,
     107              : 
     108          341 :         current_machine,
     109          341 :         current_manifest_item,
     110          341 :     } = state;
     111              : 
     112          341 :     const { countdown, problem } = session_controller;
     113              : 
     114           69 :     if (problem && !ready)
     115           66 :         return <EarlyFailure />;
     116              : 
     117          341 :     if (!ready)
     118          341 :         return null;
     119              : 
     120          338 :     cockpit.assert(current_machine && current_manifest_item);
     121              : 
     122          341 :     const title_parts = [];
     123          341 :     if (current_manifest_item.label)
     124          338 :         title_parts.push(current_manifest_item.label);
     125          338 :     title_parts.push((current_machine.user || current_user) + "@" + current_machine.label);
     126          341 :     document.title = title_parts.join(" - ");
     127              : 
     128          341 :     if (countdown > 0)
     129           63 :         document.title = "(" + countdown + ") " + document.title;
     130              : 
     131          338 :     document.documentElement.lang = config.language;
     132          338 :     if (config.language_direction)
     133          338 :         document.documentElement.dir = config.language_direction;
     134              : 
     135          338 :     let failure = null;
     136           66 :     if (problem) {
     137           66 :         failure = <Disconnected problem={problem} />;
     138           63 :     } else if (current_machine.state != "connected") {
     139           73 :         failure = <MachineTroubleshoot machine={current_machine}
     140            0 :                                        onClick={() => connect_host(host_modal_state, state, current_machine)} />;
     141           73 :     }
     142              : 
     143          338 :     return (
     144          338 :         <div id="main" className="page"
     145          338 :              style={
     146          338 :                  {
     147           73 :                      '--ct-color-host-accent': (current_machine.address == "localhost" ? undefined : current_machine.color)
     148          341 :                  } as React.CSSProperties
     149              :              }>
     150              : 
     151          341 :             <SkipLink focus_id="content">{_("Skip to content")}</SkipLink>
     152          341 :             <SkipLink focus_id="hosts-sel">{_("Skip main navigation")}</SkipLink>
     153              : 
     154          341 :             <div id="sidebar-toggle" className="pf-v6-c-select pf-m-dark sidebar-toggle">
     155          341 :                 <SidebarToggle />
     156          341 :             </div>
     157              : 
     158          341 :             <div id="nav-system" className="area-ct-subnav nav-system-menu sidebar interact">
     159          341 :                 <nav id="host-apps" className="host-apps">
     160          341 :                     <PageNav state={state} />
     161          341 :                 </nav>
     162          341 :             </div>
     163              : 
     164          341 :             <nav id="hosts-sel" className="navbar navbar-default navbar-pf navbar-pf-vertical" tabIndex={-1}>
     165          341 :                 { config.host_switcher_enabled
     166           76 :                     ? <CockpitHosts state={state} host_modal_state={host_modal_state} selector="nav-hosts" />
     167          325 :                     : <CockpitCurrentHost current_user={current_user} machine={current_machine} />
     168              :                 }
     169          341 :             </nav>
     170              : 
     171          341 :             <div id="nav-hosts" className="area-ct-subnav nav-hosts-menu sidebar" />
     172              : 
     173          341 :             <div id="topnav" className="header">
     174          341 :                 <TopNav state={state} />
     175          341 :             </div>
     176              : 
     177          341 :             <Frames hidden={!!failure} state={state} />
     178              : 
     179          341 :             { failure &&
     180           76 :             <div id="failure-content" className="area-ct-content" role="main" tabIndex={-1}>
     181           76 :                 { failure }
     182           76 :             </div>
     183              :             }
     184              : 
     185          341 :             <SessionCountdownModal />
     186          341 :             <HostModal state={host_modal_state} machines={state.machines} />
     187              : 
     188          341 :         </div>);
     189          341 : };
     190              : 
     191              : interface ShellWindow extends Window {
     192              :     features?: object;
     193              : }
     194              : 
     195          341 : function init() {
     196          341 :     cockpit.translate();
     197              : 
     198              :     /* Give us a name.  This used to (maybe) indicate at some point to
     199              :      * child frames that we are a cockpit1 router frame.  But they
     200              :      * actually check for a "cockpit1:" prefix of their own name.
     201              :      */
     202          341 :     window.name = "cockpit1";
     203              : 
     204              :     /* Tell the pages about our features. */
     205          341 :     (window as ShellWindow).features = {
     206          341 :         navbar_is_for_current_machine: true
     207          341 :     };
     208              : 
     209            0 :     function follow(arg: unknown) {
     210              :         /* A promise of some sort */
     211            0 :         if (arguments.length == 1 && arg && typeof arg == "object" && "then" in arg && typeof arg.then == "function") {
     212            0 :             arg.then(function(...args: unknown[]) { console.log(...args) },
     213            0 :                      function(...args: unknown[]) { console.error(...args) });
     214            0 :             if ("stream" in arg && typeof arg.stream == "function")
     215            0 :                 arg.stream(function(...args: unknown[]) { console.log(...args) });
     216            0 :         }
     217            0 :     }
     218              : 
     219          341 :     let zz_value: unknown;
     220              : 
     221              :     /* For debugging in the browser console */
     222          341 :     Object.defineProperties(window, {
     223          341 :         cockpit: { value: cockpit },
     224          341 :         zz: {
     225            0 :             get: function() { return zz_value },
     226            0 :             set: function(val) { zz_value = val; follow(val) }
     227          341 :         }
     228          341 :     });
     229              : 
     230          341 :     const root = createRoot(document.getElementById("shell")!);
     231          341 :     root.render(<WithDialogs><Shell /></WithDialogs>);
     232          341 : }
     233              : 
     234          341 : document.addEventListener("DOMContentLoaded", init);
        

Generated by: LCOV version 2.0-1