Line data Source code
1 : /*
2 : * Copyright (C) 2024 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 123 : import cockpit from "cockpit";
7 :
8 123 : import React, { useEffect } from 'react';
9 123 : 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 123 : 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 120 : const SkipLink = ({ focus_id, children }: SkipLinkProps) => {
47 120 : return (
48 120 : <SkipToContent
49 120 : 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 120 : {children}
57 120 : </SkipToContent>
58 : );
59 120 : };
60 :
61 120 : const SessionCountdownModal = () => {
62 120 : const controller = get_session_controller();
63 120 : useOn(controller, "changed");
64 :
65 120 : controller.inhibit_activity_reporting(controller.countdown > 0);
66 :
67 120 : if (controller.countdown <= 0)
68 120 : return null;
69 :
70 19 : return (
71 19 : <Modal isOpen position="top" variant="medium"
72 19 : id="session-timeout-modal">
73 19 : <ModalHeader title={_("Session is about to expire")} />
74 19 : <ModalBody>
75 19 : { cockpit.format(_("You will be logged out in $0 seconds."), controller.countdown) }
76 19 : </ModalBody>
77 19 : <ModalFooter>
78 19 : <Button variant='primary'
79 0 : onClick={() => controller.continue_session()}
80 : >
81 19 : {_("Continue session")}
82 19 : </Button>
83 19 : </ModalFooter>
84 19 : </Modal>
85 : );
86 120 : };
87 :
88 123 : const Shell = () => {
89 120 : const current_user = useLoggedInUser()?.name || "";
90 123 : const state = useInit(() => new ShellState());
91 123 : const session_controller = get_session_controller();
92 123 : const host_modal_state = useInit(() => HostModalState());
93 :
94 123 : useOn(state, "update");
95 123 : useOn(session_controller, "changed");
96 123 : useEvent(host_modal_state, "changed");
97 :
98 123 : useEffect(() => {
99 0 : return state.on("connect", () => {
100 0 : connect_host(host_modal_state, state, state.current_machine);
101 0 : });
102 123 : }, [host_modal_state, state]);
103 :
104 123 : const {
105 123 : ready,
106 123 : config,
107 :
108 123 : current_machine,
109 123 : current_manifest_item,
110 123 : } = state;
111 :
112 123 : const { countdown, problem } = session_controller;
113 :
114 24 : if (problem && !ready)
115 22 : return <EarlyFailure />;
116 :
117 123 : if (!ready)
118 123 : return null;
119 :
120 120 : cockpit.assert(current_machine && current_manifest_item);
121 :
122 123 : const title_parts = [];
123 123 : if (current_manifest_item.label)
124 120 : title_parts.push(current_manifest_item.label);
125 120 : title_parts.push((current_machine.user || current_user) + "@" + current_machine.label);
126 123 : document.title = title_parts.join(" - ");
127 :
128 123 : if (countdown > 0)
129 19 : document.title = "(" + countdown + ") " + document.title;
130 :
131 120 : document.documentElement.lang = config.language;
132 120 : if (config.language_direction)
133 120 : document.documentElement.dir = config.language_direction;
134 :
135 120 : let failure = null;
136 21 : if (problem) {
137 21 : failure = <Disconnected problem={problem} />;
138 19 : } else if (current_machine.state != "connected") {
139 19 : failure = <MachineTroubleshoot machine={current_machine}
140 0 : onClick={() => connect_host(host_modal_state, state, current_machine)} />;
141 19 : }
142 :
143 120 : return (
144 120 : <div id="main" className="page"
145 120 : style={
146 120 : {
147 19 : '--ct-color-host-accent': (current_machine.address == "localhost" ? undefined : current_machine.color)
148 123 : } as React.CSSProperties
149 : }>
150 :
151 123 : <SkipLink focus_id="content">{_("Skip to content")}</SkipLink>
152 123 : <SkipLink focus_id="hosts-sel">{_("Skip main navigation")}</SkipLink>
153 :
154 123 : <div id="sidebar-toggle" className="pf-v6-c-select pf-m-dark sidebar-toggle">
155 123 : <SidebarToggle />
156 123 : </div>
157 :
158 123 : <div id="nav-system" className="area-ct-subnav nav-system-menu sidebar interact">
159 123 : <nav id="host-apps" className="host-apps">
160 123 : <PageNav state={state} />
161 123 : </nav>
162 123 : </div>
163 :
164 123 : <nav id="hosts-sel" className="navbar navbar-default navbar-pf navbar-pf-vertical" tabIndex={-1}>
165 123 : { config.host_switcher_enabled
166 19 : ? <CockpitHosts state={state} host_modal_state={host_modal_state} selector="nav-hosts" />
167 120 : : <CockpitCurrentHost current_user={current_user} machine={current_machine} />
168 : }
169 123 : </nav>
170 :
171 123 : <div id="nav-hosts" className="area-ct-subnav nav-hosts-menu sidebar" />
172 :
173 123 : <div id="topnav" className="header">
174 123 : <TopNav state={state} />
175 123 : </div>
176 :
177 123 : <Frames hidden={!!failure} state={state} />
178 :
179 123 : { failure &&
180 21 : <div id="failure-content" className="area-ct-content" role="main" tabIndex={-1}>
181 21 : { failure }
182 21 : </div>
183 : }
184 :
185 123 : <SessionCountdownModal />
186 123 : <HostModal state={host_modal_state} machines={state.machines} />
187 :
188 123 : </div>);
189 123 : };
190 :
191 : interface ShellWindow extends Window {
192 : features?: object;
193 : }
194 :
195 123 : function init() {
196 123 : 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 123 : window.name = "cockpit1";
203 :
204 : /* Tell the pages about our features. */
205 123 : (window as ShellWindow).features = {
206 123 : navbar_is_for_current_machine: true
207 123 : };
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 123 : let zz_value: unknown;
220 :
221 : /* For debugging in the browser console */
222 123 : Object.defineProperties(window, {
223 123 : cockpit: { value: cockpit },
224 123 : zz: {
225 0 : get: function() { return zz_value },
226 0 : set: function(val) { zz_value = val; follow(val) }
227 123 : }
228 123 : });
229 :
230 123 : const root = createRoot(document.getElementById("shell")!);
231 123 : root.render(<WithDialogs><Shell /></WithDialogs>);
232 123 : }
233 :
234 123 : document.addEventListener("DOMContentLoaded", init);
|