Skip to content

Commit

Permalink
pkg/lib: Port various components to TypeScript
Browse files Browse the repository at this point in the history
... mostly stuff used by Cockpit Files.

This was all fairly trivial.  In `utils.tsx` most of the changes are
adjusting the newer language features rather than adding typing.
  • Loading branch information
allisonkarlitskaya committed Jun 24, 2024
1 parent 84aad2d commit 7f9d34b
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/

import React, { useState } from 'react';
import PropTypes from "prop-types";

import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
import { Dropdown, DropdownList } from "@patternfly/react-core/dist/esm/components/Dropdown";
Expand All @@ -36,7 +35,14 @@ import { EllipsisVIcon } from '@patternfly/react-icons';
* require a separator between DropdownItem's use PatternFly's Divivder
* component.
*/
export const KebabDropdown = ({ dropdownItems, position = "end", isDisabled = false, toggleButtonId, isOpen, setIsOpen, props }) => {
export const KebabDropdown = ({ dropdownItems, position = "end", isDisabled = false, toggleButtonId, isOpen, setIsOpen, props } : {
dropdownItems: React.ReactNode,
position?: "start" | "end" | "right" | "left" | "center",
isDisabled?: boolean,
toggleButtonId?: string;
isOpen?: boolean, setIsOpen?: React.Dispatch<React.SetStateAction<boolean>>,
props?: Parameters<typeof Dropdown>[0]
}) => {
const [isKebabOpenInternal, setKebabOpenInternal] = useState(false);
const isKebabOpen = isOpen ?? isKebabOpenInternal;
const setKebabOpen = setIsOpen ?? setKebabOpenInternal;
Expand Down Expand Up @@ -67,12 +73,3 @@ export const KebabDropdown = ({ dropdownItems, position = "end", isDisabled = fa
</Dropdown>
);
};

KebabDropdown.propTypes = {
dropdownItems: PropTypes.array.isRequired,
isDisabled: PropTypes.bool,
toggleButtonId: PropTypes.string,
position: PropTypes.oneOf(['right', 'left', 'center', 'start', 'end']),
isOpen: PropTypes.bool,
setIsOpen: PropTypes.func,
};
16 changes: 9 additions & 7 deletions pkg/lib/cockpit-dark-theme.js → pkg/lib/cockpit-dark-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

function debug() {
function debug(...args: unknown[]) {
if (window.debugging == "all" || window.debugging?.includes("style")) {
console.debug([`cockpit-dark-theme: ${document.documentElement.id}:`, ...arguments].join(" "));
console.debug([`cockpit-dark-theme: ${document.documentElement.id}:`, ...args].join(" "));
}
}

function changeDarkThemeClass(documentElement, dark_mode) {
function changeDarkThemeClass(documentElement: Element, dark_mode: boolean) {
debug(`Setting cockpit theme to ${dark_mode ? "dark" : "light"}`);

if (dark_mode) {
Expand All @@ -33,7 +33,7 @@ function changeDarkThemeClass(documentElement, dark_mode) {
}
}

function _setDarkMode(_style) {
function _setDarkMode(_style?: string) {
const style = _style || localStorage.getItem('shell:style') || 'auto';
let dark_mode;
// If a user set's an explicit theme, ignore system changes.
Expand All @@ -56,10 +56,12 @@ window.addEventListener("storage", event => {
// When changing the theme from the shell switcher the localstorage change will not fire for the same page (aka shell)
// so we need to listen for the event on the window object.
window.addEventListener("cockpit-style", event => {
const style = event.detail.style;
debug(`Event received from shell with 'cockpit-style' ${style}`);
if (event instanceof CustomEvent) {
const style = event.detail.style;
debug(`Event received from shell with 'cockpit-style' ${style}`);

_setDarkMode(style);
_setDarkMode(style);
}
});

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
Expand Down
2 changes: 2 additions & 0 deletions pkg/lib/cockpit.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ declare module 'cockpit' {

function assert(predicate: unknown, message?: string): asserts predicate;

export const manifests: { [package in string]?: JsonObject };

export let language: string;

/* === jQuery compatible promise ============== */
Expand Down
6 changes: 3 additions & 3 deletions pkg/lib/pam_user_parser.js → pkg/lib/pam_user_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

function parse_passwd_content(content) {
function parse_passwd_content(content: string) {
if (!content) {
console.warn("Couldn't read /etc/passwd");
return [];
Expand Down Expand Up @@ -48,7 +48,7 @@ export const etc_passwd_syntax = {
parse: parse_passwd_content
};

function parse_group_content(content) {
function parse_group_content(content: string) {
// /etc/group file is used to set only secondary groups of users. The primary group is saved in /etc/passwd-
content = (content || "").trim();
if (!content) {
Expand Down Expand Up @@ -78,7 +78,7 @@ export const etc_group_syntax = {
parse: parse_group_content
};

function parse_shells_content(content) {
function parse_shells_content(content: string) {
content = (content || "").trim();
if (!content) {
console.warn("Couldn't read /etc/shells");
Expand Down
File renamed without changes.
42 changes: 27 additions & 15 deletions pkg/lib/utils.jsx → pkg/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,33 @@ import React from "react";

import cockpit from "cockpit";

export function fmt_to_fragments(fmt) {
const args = Array.prototype.slice.call(arguments, 1);

function replace(part) {
export function fmt_to_fragments(format: string, ...args: React.ReactNode[]) {
const fragments = format.split(/(\$[0-9]+)/g).map(part => {
if (part[0] == "$") {
return args[parseInt(part.slice(1))];
return args[parseInt(part.slice(1))]; // placeholder, from `args`
} else
return part;
}
return part; // literal string content
});

return React.createElement(React.Fragment, { }, ...fragments);
}

return React.createElement.apply(null, [React.Fragment, { }].concat(fmt.split(/(\$[0-9]+)/g).map(replace)));
/**
* Checks if a JsonValue is a JsonObject, and acts as a type guard.
*
* This function produces correct results for any possible JsonValue, and also
* for undefined. If you pass other types of values to this function it may
* return an incorrect result (ie: it doesn't check deeply, so anything that
* looks like a "simple object" will pass the check).
*/
export function is_json_dict(value: cockpit.JsonValue | undefined): value is cockpit.JsonObject {
return value?.constructor === Object;
}

function try_fields(dict, fields, def) {
for (let i = 0; i < fields.length; i++)
if (fields[i] && dict[fields[i]] !== undefined)
return dict[fields[i]];
function try_fields(dict: cockpit.JsonObject, fields: string[], def: cockpit.JsonValue): cockpit.JsonValue {
for (const field in fields)
if (field in dict)
return dict[field];
return def;
}

Expand All @@ -56,12 +66,14 @@ function try_fields(dict, fields, def) {
* "platform:el9": { "color": "red" }
* }
*/
export function get_manifest_config_matchlist(manifest_name, config_name, default_value, matches) {
export function get_manifest_config_matchlist(
manifest_name: string, config_name: string, default_value: cockpit.JsonValue, matches: string[]
): cockpit.JsonValue {
const config = cockpit.manifests[manifest_name]?.config;

if (config) {
if (is_json_dict(config)) {
const val = config[config_name];
if (typeof val === 'object' && val !== null && !Array.isArray(val))
if (is_json_dict(val))
return try_fields(val, matches, default_value);
else
return val !== undefined ? val : default_value;
Expand Down

0 comments on commit 7f9d34b

Please sign in to comment.