* Copyright (C) 2024 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useEffect } from 'react';
import { createRoot } from "react-dom/client";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { WithDialogs } from "dialogs.jsx";
import { useInit, useEvent, useOn, useLoggedInUser } from "hooks";
import { TopNav } from "./topnav.jsx";
import { SidebarToggle, PageNav } from "./nav.jsx";
import { CockpitHosts, CockpitCurrentHost } from "./hosts.jsx";
import { HostModalState, HostModal, connect_host } from "./hosts_dialog.jsx";
import { Frames } from "./frames.jsx";
import { EarlyFailure, Disconnected, MachineTroubleshoot } from "./failures.jsx";
import { ShellState } from "./state.jsx";
import { get_session_controller } from "./session";
import { SkipToContent } from '@patternfly/react-core/dist/esm/components/SkipToContent/index.js';
import 'cockpit-dark-theme';
import '../lib/patternfly/patternfly-6-cockpit.scss';
import "./shell.scss";
const _ = cockpit.gettext;
interface SkipLinkProps {
focus_id: string;
children?: React.ReactNode
}
const SkipLink = ({ focus_id, children }: SkipLinkProps) => {
return (
<SkipToContent
href={`#${focus_id}`}
onClick={ev => {
const el = document.getElementById(focus_id);
el?.focus();
el?.scrollIntoView();
ev.preventDefault();
}}>
{children}
</SkipToContent>
);
};
const SessionCountdownModal = () => {
const controller = get_session_controller();
useOn(controller, "changed");
controller.inhibit_activity_reporting(controller.countdown > 0);
if (controller.countdown <= 0)
return null;
return (
<Modal isOpen position="top" variant="medium"
id="session-timeout-modal">
<ModalHeader title={_("Session is about to expire")} />
<ModalBody>
{ cockpit.format(_("You will be logged out in $0 seconds."), controller.countdown) }
</ModalBody>
<ModalFooter>
<Button variant='primary'
onClick={() => controller.continue_session()}
>
{_("Continue session")}
</Button>
</ModalFooter>
</Modal>
);
};
const Shell = () => {
const current_user = useLoggedInUser()?.name || "";
const state = useInit(() => new ShellState());
const session_controller = get_session_controller();
const host_modal_state = useInit(() => HostModalState());
useOn(state, "update");
useOn(session_controller, "changed");
useEvent(host_modal_state, "changed");
useEffect(() => {
return state.on("connect", () => {
connect_host(host_modal_state, state, state.current_machine);
});
}, [host_modal_state, state]);
const {
ready,
config,
current_machine,
current_manifest_item,
} = state;
const { countdown, problem } = session_controller;
if (problem && !ready)
return <EarlyFailure />;
if (!ready)
return null;
cockpit.assert(current_machine && current_manifest_item);
const title_parts = [];
if (current_manifest_item.label)
title_parts.push(current_manifest_item.label);
title_parts.push((current_machine.user || current_user) + "@" + current_machine.label);
document.title = title_parts.join(" - ");
if (countdown > 0)
document.title = "(" + countdown + ") " + document.title;
document.documentElement.lang = config.language;
if (config.language_direction)
document.documentElement.dir = config.language_direction;
let failure = null;
if (problem) {
failure = <Disconnected problem={problem} />;
} else if (current_machine.state !== "connected") {
failure = <MachineTroubleshoot machine={current_machine}
onClick={() => connect_host(host_modal_state, state, current_machine)} />;
}
return (
<div id="main" className="page"
style={
{
'--ct-color-host-accent': (current_machine.address === "localhost" ? undefined : current_machine.color)
} as React.CSSProperties
}>
<SkipLink focus_id="content">{_("Skip to content")}</SkipLink>
<SkipLink focus_id="hosts-sel">{_("Skip main navigation")}</SkipLink>
<div id="sidebar-toggle" className="pf-v6-c-select pf-m-dark sidebar-toggle">
<SidebarToggle />
</div>
<div id="nav-system" className="area-ct-subnav nav-system-menu sidebar interact">
<nav id="host-apps" className="host-apps">
<PageNav state={state} />
</nav>
</div>
<nav id="hosts-sel" className="navbar navbar-default navbar-pf navbar-pf-vertical" tabIndex={-1}>
{ config.host_switcher_enabled
? <CockpitHosts state={state} host_modal_state={host_modal_state} selector="nav-hosts" />
: <CockpitCurrentHost current_user={current_user} machine={current_machine} />
}
</nav>
<div id="nav-hosts" className="area-ct-subnav nav-hosts-menu sidebar" />
<div id="topnav" className="header">
<TopNav state={state} />
</div>
<Frames hidden={!!failure} state={state} />
{ failure &&
<div id="failure-content" className="area-ct-content" role="main" tabIndex={-1}>
{ failure }
</div>
}
<SessionCountdownModal />
<HostModal state={host_modal_state} machines={state.machines} />
</div>);
};
interface ShellWindow extends Window {
features?: object;
}
function init() {
cockpit.translate();
* child frames that we are a cockpit1 router frame. But they
* actually check for a "cockpit1:" prefix of their own name.
*/
window.name = "cockpit1";
(window as ShellWindow).features = {
navbar_is_for_current_machine: true
};
function follow(arg: unknown) {
if (arguments.length === 1 && arg && typeof arg == "object" && "then" in arg && typeof arg.then == "function") {
arg.then(function(...args: unknown[]) { console.log(...args) },
function(...args: unknown[]) { console.error(...args) });
if ("stream" in arg && typeof arg.stream == "function")
arg.stream(function(...args: unknown[]) { console.log(...args) });
}
}
let zz_value: unknown;
Object.defineProperties(window, {
cockpit: { value: cockpit },
zz: {
get: function() { return zz_value },
set: function(val) { zz_value = val; follow(val) }
}
});
const root = createRoot(document.getElementById("shell")!);
root.render(<WithDialogs><Shell /></WithDialogs>);
}
document.addEventListener("DOMContentLoaded", init);