diff --git a/pkg/lib/cockpit.d.ts b/pkg/lib/cockpit.d.ts index 99524c268755..22d4af66e033 100644 --- a/pkg/lib/cockpit.d.ts +++ b/pkg/lib/cockpit.d.ts @@ -220,6 +220,7 @@ declare module 'cockpit' { interface DBusOptions { bus?: string; address?: string; + host?: string; superuser?: "require" | "try"; track?: boolean; } diff --git a/pkg/shell/machines/machines.d.ts b/pkg/shell/machines/machines.d.ts index 7a0d9f7fa76a..3165f88d9890 100644 --- a/pkg/shell/machines/machines.d.ts +++ b/pkg/shell/machines/machines.d.ts @@ -1,4 +1,4 @@ -import { EventSource, EventMap } from "cockpit"; +import { EventSource, EventMap, JsonObject } from "cockpit"; export function generate_connection_string(user: string | null, port: string | null, addr: string) : string; export function split_connection_string (conn_to: string) : { address: string, user?: string, port?: number }; @@ -27,9 +27,7 @@ export interface ManifestSection { [name: string]: ManifestEntry; } -export interface Manifest { - [section: string]: ManifestSection; -} +export type Manifest = JsonObject; export interface Manifests { [pkg: string]: Manifest; diff --git a/pkg/shell/nav.jsx b/pkg/shell/nav.tsx similarity index 69% rename from pkg/shell/nav.jsx rename to pkg/shell/nav.tsx index c78e7c3b53d4..c3ee48ccb5d3 100644 --- a/pkg/shell/nav.jsx +++ b/pkg/shell/nav.tsx @@ -1,7 +1,25 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2024 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + import cockpit from "cockpit"; import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Nav } from "@patternfly/react-core/dist/esm/components/Nav/index.js"; @@ -9,7 +27,9 @@ import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchIn import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; import { ContainerNodeIcon, ExclamationCircleIcon, ExclamationTriangleIcon, InfoCircleIcon } from '@patternfly/react-icons'; -import { encode_location } from "./util.jsx"; +import { Location, encode_location, ManifestItem } from "./util.jsx"; +import { ShellState, PageStatus } from "./state"; +import { ManifestKeyword } from "./machines/machines"; const _ = cockpit.gettext; @@ -22,8 +42,8 @@ export const SidebarToggle = () => { * However, when clicking on an iframe moves focus to its content's window that triggers the main window.blur event. * Additionally, when clicking on an element in the same iframe make sure to unset the 'active' state of the 'System' dropdown selector. */ - const handleClickOutside = (ev) => { - if (ev.target.id == "nav-system-item") + const handleClickOutside = (ev: Event) => { + if ((ev.target as Element).id == "nav-system-item") return; setActive(false); @@ -37,7 +57,7 @@ export const SidebarToggle = () => { }, []); useEffect(() => { - document.getElementById("nav-system").classList.toggle("interact", active); + document.getElementById("nav-system")!.classList.toggle("interact", active); }, [active]); return ( @@ -50,8 +70,45 @@ export const SidebarToggle = () => { ); }; +interface NavKeyword { + keyword: string; + score: number; + goto: string | null; +} + +interface NavItem extends ManifestItem { + keyword: NavKeyword; +} + +interface ItemGroup { + name: string; + items: T[]; + action?: { + label: string; + target: Location; + } | undefined; +} + +interface CockpitNavProps { + groups: ItemGroup[]; + selector: string; + current: string; + filtering: (item: ManifestItem, term: string) => NavItem | null; + sorting: (a: NavItem, b: NavItem) => number; + item_render: (item: NavItem, term: string) => React.ReactNode; + jump: (loc: Location) => void; +} + +interface CockpitNavState { + search: string; + current: string; +} + export class CockpitNav extends React.Component { - constructor(props) { + props: CockpitNavProps; + state: CockpitNavState; + + constructor(props : CockpitNavProps) { super(props); this.state = { @@ -60,29 +117,29 @@ export class CockpitNav extends React.Component { }; this.clearSearch = this.clearSearch.bind(this); + this.props = props; } componentDidMount() { - const self = this; const sel = this.props.selector; // Click on active menu item (when using arrows to navigate through menu) function clickActiveItem() { const cur = document.activeElement; - if (cur.nodeName === "INPUT") { - const el = document.querySelector("#" + sel + " li:first-of-type a"); + if (cur instanceof HTMLInputElement) { + const el: HTMLElement | null = document.querySelector("#" + sel + " li:first-of-type a"); if (el) el.click(); - } else { + } else if (cur instanceof HTMLElement) { cur.click(); } } // Move focus to next item in menu (when using arrows to navigate through menu) // With arguments it is possible to change direction - function focusNextItem(begin, step) { + function focusNextItem(begin: number, step: number) { const cur = document.activeElement; - const all = Array.from(document.querySelectorAll("#" + sel + " li a")); - if (cur.nodeName === "INPUT" && all) { + const all = Array.from(document.querySelectorAll("#" + sel + " li a")); + if (cur instanceof HTMLInputElement && all.length > 0) { if (begin < 0) begin = all.length - 1; all[begin].focus(); @@ -90,30 +147,29 @@ export class CockpitNav extends React.Component { let i = all.findIndex(item => item === cur); i += step; if (i < 0 || i >= all.length) - document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input").focus(); + document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input")?.focus(); else all[i].focus(); } } - function navigate_apps(ev) { - if (ev.keyCode === 13) // Enter + const navigate_apps = (ev: KeyboardEvent) => { + if (ev.key == "Enter") clickActiveItem(); - else if (ev.keyCode === 40) // Arrow Down + else if (ev.key == "ArrowDown") focusNextItem(0, 1); - else if (ev.keyCode === 38) // Arrow Up + else if (ev.key == "ArrowUp") focusNextItem(-1, -1); - else if (ev.keyCode === 27) { // Escape - clean selection - self.setState({ search: "" }); - document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input").focus(); + else if (ev.key == "Escape") { + this.setState({ search: "" }); + document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input")?.focus(); } - } + }; - document.getElementById(sel).addEventListener("keyup", navigate_apps); - document.getElementById(sel).addEventListener("change", navigate_apps); + document.getElementById(sel)?.addEventListener("keyup", navigate_apps); } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: CockpitNavProps, prevState: CockpitNavState) { if (nextProps.current !== prevState.current) return { search: "", @@ -127,10 +183,10 @@ export class CockpitNav extends React.Component { } render() { - const groups = []; + const groups: ItemGroup[] = []; const term = this.state.search.toLowerCase(); this.props.groups.forEach(g => { - const new_items = g.items.map(i => this.props.filtering(i, term)).filter(Boolean); + const new_items = g.items.map(i => this.props.filtering(i, term)).filter((i): i is NavItem => !!i); new_items.sort(this.props.sorting); if (new_items.length > 0) groups.push({ name: g.name, items: new_items, action: g.action }); @@ -139,7 +195,7 @@ export class CockpitNav extends React.Component { return ( <> this.setState({ search })} onClear={() => this.setState({ search: "" })} className="search" /> -