Line data Source code
1 : /*
2 : * Copyright (C) 2025 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 1 : import cockpit from "cockpit";
7 1 : import React, { useState } from "react";
8 1 : import { createRoot, Container } from 'react-dom/client';
9 :
10 : import '../lib/patternfly/patternfly-6-cockpit.scss';
11 :
12 : import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
13 : import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/esm/components/ToggleGroup/index.js";
14 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
15 : import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
16 : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
17 :
18 : import { useObject } from 'hooks';
19 : import { Terminal, TerminalState } from 'cockpit-components-terminal';
20 :
21 : import 'cockpit-dark-theme'; // once per page
22 : import 'page.scss';
23 :
24 1 : function create_channel() {
25 1 : return cockpit.channel({
26 1 : payload: "stream",
27 1 : spawn: ["/bin/bash"],
28 1 : environ: [
29 1 : "TERM=xterm-256color",
30 1 : ],
31 1 : directory: "/",
32 1 : pty: true,
33 1 : });
34 1 : }
35 :
36 1 : function create_states(n: number): TerminalState[] {
37 1 : const states = [];
38 1 : for (let i = 0; i < n; i++)
39 1 : states.push(new TerminalState(create_channel()));
40 1 : return states;
41 1 : }
42 :
43 1 : function close_states(states: TerminalState[]) {
44 1 : states.forEach(s => s.close());
45 1 : }
46 :
47 1 : const TabbedTerminal = () => {
48 1 : const num_terminals = 4;
49 1 : const states = useObject(() => create_states(num_terminals), states => close_states(states), []);
50 1 : const [tab, setTab] = useState<number>(0);
51 :
52 1 : function make_toggles() {
53 1 : const res = [];
54 :
55 1 : for (let i = 0; i < num_terminals; i++) {
56 1 : const t = i;
57 1 : res.push(<ToggleGroupItem
58 1 : key={t}
59 1 : text={"Terminal " + String(t + 1)}
60 1 : isSelected={tab == t}
61 1 : onChange={() => setTab(t)}
62 1 : />);
63 1 : }
64 :
65 1 : return res;
66 1 : }
67 :
68 1 : return (
69 1 : <PageSection isFilled hasBodyWrapper={false}>
70 1 : <Split>
71 1 : <SplitItem>
72 1 : <ToggleGroup>
73 1 : {make_toggles()}
74 1 : </ToggleGroup>
75 1 : </SplitItem>
76 1 : <SplitItem isFilled />
77 1 : <SplitItem>
78 1 : <Button variant="secondary" onClick={() => states[tab].resetChannel(create_channel())}>
79 : Reset
80 1 : </Button>
81 1 : </SplitItem>
82 1 : </Split>
83 1 : <div id="terminal" style={{ height: "100%" }}>
84 1 : <Terminal
85 1 : parentId="terminal"
86 1 : state={states[tab]}
87 1 : cols={80}
88 1 : rows={40}
89 1 : />
90 1 : </div>
91 1 : </PageSection>
92 : );
93 1 : };
94 :
95 1 : const TerminalDemo = () => {
96 1 : const [showTerminal, setShowTerminal] = useState(true);
97 :
98 1 : return (
99 1 : <Page isContentFilled className="no-masthead-sidebar">
100 1 : <PageSection>
101 1 : <Switch
102 1 : label="Show terminals"
103 1 : isChecked={showTerminal}
104 1 : onChange={(_event: React.FormEvent<HTMLInputElement>, checked: boolean) => setShowTerminal(checked)}
105 1 : />
106 1 : </PageSection>
107 1 : { showTerminal && <TabbedTerminal /> }
108 1 : </Page>
109 : );
110 1 : };
111 :
112 1 : function init_app(rootElement: Container) {
113 1 : const root = createRoot(rootElement);
114 1 : root.render(<TerminalDemo />);
115 1 : }
116 :
117 1 : document.addEventListener("DOMContentLoaded", function() {
118 1 : cockpit.transport.wait(function() {
119 1 : init_app(document.getElementById('app')!);
120 1 : });
121 1 : });
|