From 767b8d756de05c3154d6fd283354fc4bb4225a41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 19 Dec 2024 15:19:08 +0000
Subject: [PATCH 01/31] refactor(web): migrate core/EmailInput to TypeScript
---
...mailInput.test.jsx => EmailInput.test.tsx} | 51 ++++++++++---------
.../core/{EmailInput.jsx => EmailInput.tsx} | 29 +++++------
2 files changed, 42 insertions(+), 38 deletions(-)
rename web/src/components/core/{EmailInput.test.jsx => EmailInput.test.tsx} (74%)
rename web/src/components/core/{EmailInput.jsx => EmailInput.tsx} (67%)
diff --git a/web/src/components/core/EmailInput.test.jsx b/web/src/components/core/EmailInput.test.tsx
similarity index 74%
rename from web/src/components/core/EmailInput.test.jsx
rename to web/src/components/core/EmailInput.test.tsx
index b4572709f3..1a050e9f15 100644
--- a/web/src/components/core/EmailInput.test.jsx
+++ b/web/src/components/core/EmailInput.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -23,9 +23,35 @@
import React, { useState } from "react";
import { screen } from "@testing-library/react";
-import EmailInput from "./EmailInput";
+import EmailInput, { EmailInputProps } from "./EmailInput";
import { plainRender } from "~/test-utils";
+/**
+ * Controlled component for testing the EmailInputProps
+ *
+ * Instead of testing if given callbacks are called, below tests are going to
+ * check the rendered result to be more aligned with the React Testing Library
+ * principles, https://testing-library.com/docs/guiding-principles/
+ *
+ */
+const EmailInputTest = (props: EmailInputProps) => {
+ const [email, setEmail] = useState("");
+ const [isValid, setIsValid] = useState(true);
+
+ return (
+ <>
+ setEmail(v)}
+ onValidate={setIsValid}
+ />
+ {email && Email value updated!
}
+ {isValid === false && Email is not valid!
}
+ >
+ );
+};
+
describe("EmailInput component", () => {
it("renders an email input", () => {
plainRender(
@@ -36,27 +62,6 @@ describe("EmailInput component", () => {
expect(inputField).toHaveAttribute("type", "email");
});
- // Using a controlled component for testing the rendered result instead of testing if
- // the given onChange callback is called. The former is more aligned with the
- // React Testing Library principles, https://testing-library.com/docs/guiding-principles/
- const EmailInputTest = (props) => {
- const [email, setEmail] = useState("");
- const [isValid, setIsValid] = useState(true);
-
- return (
- <>
- setEmail(v)}
- onValidate={setIsValid}
- />
- {email && Email value updated!
}
- {isValid === false && Email is not valid!
}
- >
- );
- };
-
it("triggers onChange callback", async () => {
const { user } = plainRender();
const emailInput = screen.getByRole("textbox", { name: "Test email" });
diff --git a/web/src/components/core/EmailInput.jsx b/web/src/components/core/EmailInput.tsx
similarity index 67%
rename from web/src/components/core/EmailInput.jsx
rename to web/src/components/core/EmailInput.tsx
index 6bda2447b5..5d3bf09335 100644
--- a/web/src/components/core/EmailInput.jsx
+++ b/web/src/components/core/EmailInput.tsx
@@ -21,28 +21,25 @@
*/
import React, { useEffect, useState } from "react";
-import { InputGroup, TextInput } from "@patternfly/react-core";
-import { noop } from "~/utils";
+import { InputGroup, TextInput, TextInputProps } from "@patternfly/react-core";
+import { isEmpty, noop } from "~/utils";
/**
* Email validation.
*
* Code inspired by https://github.com/manishsaraan/email-validator/blob/master/index.js
- *
- * @param {string} email
- * @returns {boolean}
*/
-const validateEmail = (email) => {
+const validateEmail = (email: string) => {
const regexp =
/^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
- const validateFormat = (email) => {
+ const validateFormat = (email: string) => {
const parts = email.split("@");
return parts.length === 2 && regexp.test(email);
};
- const validateSizes = (email) => {
+ const validateSizes = (email: string) => {
const [account, address] = email.split("@");
if (account.length > 64) return false;
@@ -58,27 +55,29 @@ const validateEmail = (email) => {
return validateFormat(email) && validateSizes(email);
};
+export type EmailInputProps = TextInputProps & { onValidate?: (isValid: boolean) => void };
+
/**
* Renders an email input field which validates its value.
* @component
*
- * @param {(boolean) => void} onValidate - Callback to be called every time the input value is
+ * @param onValidate - Callback to be called every time the input value is
* validated.
- * @param {Object} props - Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput},
+ * @param props - Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput},
* except `type` and `validated` which are managed by the component.
*/
-export default function EmailInput({ onValidate = noop, ...props }) {
+export default function EmailInput({ onValidate = noop, value, ...props }: EmailInputProps) {
const [isValid, setIsValid] = useState(true);
useEffect(() => {
- const isValid = props.value.length === 0 || validateEmail(props.value);
+ const isValid = typeof value === "string" && (isEmpty(value) || validateEmail(value));
setIsValid(isValid);
- onValidate(isValid);
- }, [onValidate, props.value, setIsValid]);
+ typeof onValidate === "function" && onValidate(isValid);
+ }, [onValidate, value, setIsValid]);
return (
-
+
);
}
From 7a412fd0fde69b125be18996a24e06765a9c9029 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Mon, 23 Dec 2024 22:25:06 +0000
Subject: [PATCH 02/31] refactor(web): migrate src/client to TypeScript
---
web/src/client/{index.js => index.ts} | 50 ++++++++++++++---------
web/src/client/{ws.js => ws.ts} | 58 +++++++++++++--------------
2 files changed, 59 insertions(+), 49 deletions(-)
rename web/src/client/{index.js => index.ts} (55%)
rename web/src/client/{ws.js => ws.ts} (85%)
diff --git a/web/src/client/index.js b/web/src/client/index.ts
similarity index 55%
rename from web/src/client/index.js
rename to web/src/client/index.ts
index 5e161ef5f3..97826e5cd5 100644
--- a/web/src/client/index.js
+++ b/web/src/client/index.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2021-2023] SUSE LLC
+ * Copyright (c) [2021-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,28 +20,40 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import { WSClient } from "./ws";
-/**
- * @typedef {object} InstallerClient
- * @property {() => boolean} isConnected - determines whether the client is connected
- * @property {() => boolean} isRecoverable - determines whether the client is recoverable after disconnected
- * @property {(handler: () => void) => (() => void)} onConnect - registers a handler to run
- * @property {(handler: () => void) => (() => void)} onDisconnect - registers a handler to run
- * when the connection is lost. It returns a function to deregister the
- * handler.
- * @property {(handler: (any) => void) => (() => void)} onEvent - registers a handler to run on events
- */
+type VoidFn = () => void;
+type BooleanFn = () => boolean;
+type EventHandlerFn = (event) => void;
+
+export type InstallerClient = {
+ /** Whether the client is connected. */
+ isConnected: BooleanFn;
+ /** Whether the client is recoverable after disconnecting. */
+ isRecoverable: BooleanFn;
+ /**
+ * Registers a handler to run when connection is set. It returns a function
+ * for deregistering the handler.
+ */
+ onConnect: (handler: VoidFn) => VoidFn;
+ /**
+ * Registers a handler to run when connection is lost. It returns a function
+ * for deregistering the handler.
+ */
+ onDisconnect: (handler: VoidFn) => VoidFn;
+ /**
+ * Registers a handler to run on events. It returns a function for
+ * deregistering the handler.
+ */
+ onEvent: (handler: EventHandlerFn) => VoidFn;
+};
/**
* Creates the Agama client
*
- * @param {URL} url - URL of the HTTP API.
- * @return {InstallerClient}
+ * @param url - URL of the HTTP API.
*/
-const createClient = (url) => {
+const createClient = (url: URL): InstallerClient => {
url.hash = "";
url.pathname = url.pathname.concat("api/ws");
url.protocol = url.protocol === "http:" ? "ws" : "wss";
@@ -53,9 +65,9 @@ const createClient = (url) => {
return {
isConnected,
isRecoverable,
- onConnect: (handler) => ws.onOpen(handler),
- onDisconnect: (handler) => ws.onClose(handler),
- onEvent: (handler) => ws.onEvent(handler),
+ onConnect: (handler: VoidFn) => ws.onOpen(handler),
+ onDisconnect: (handler: VoidFn) => ws.onClose(handler),
+ onEvent: (handler: EventHandlerFn) => ws.onEvent(handler),
};
};
diff --git a/web/src/client/ws.js b/web/src/client/ws.ts
similarity index 85%
rename from web/src/client/ws.js
rename to web/src/client/ws.ts
index b63c8e2a51..608001f2cd 100644
--- a/web/src/client/ws.js
+++ b/web/src/client/ws.ts
@@ -20,19 +20,13 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
-/**
- * @callback RemoveFn
- * @return {void}
- */
+type RemoveFn = () => void;
+type BaseHandlerFn = () => void;
+type EventHandlerFn = (event) => void;
/**
* Enum for the WebSocket states.
- *
- *
*/
-
const SocketStates = Object.freeze({
CONNECTED: 0,
CONNECTING: 1,
@@ -52,10 +46,25 @@ const ATTEMPT_INTERVAL = 1000;
* HTTPClient API.
*/
class WSClient {
+ url: string;
+
+ client: WebSocket;
+
+ handlers: {
+ open: Array;
+ close: Array;
+ error: Array;
+ events: Array;
+ };
+
+ reconnectAttempts: number;
+
+ timeout: ReturnType;
+
/**
- * @param {URL} url - Websocket URL.
+ * @param url - Websocket URL.
*/
- constructor(url) {
+ constructor(url: URL) {
this.url = url.toString();
this.handlers = {
@@ -126,13 +135,10 @@ class WSClient {
/**
* Registers a handler for events.
*
- * The handler is executed for all the events. It is up to the callback to
- * filter the relevant events.
- *
- * @param {(object) => void} func - Handler function to register.
- * @return {RemoveFn}
+ * The handler is executed for all events. It is up to the callback to
+ * filter the relevant ones for it.
*/
- onEvent(func) {
+ onEvent(func: EventHandlerFn): RemoveFn {
this.handlers.events.push(func);
return () => {
const position = this.handlers.events.indexOf(func);
@@ -144,11 +150,8 @@ class WSClient {
* Registers a handler for close socket.
*
* The handler is executed when the socket is close.
- *
- * @param {(object) => void} func - Handler function to register.
- * @return {RemoveFn}
*/
- onClose(func) {
+ onClose(func: BaseHandlerFn): RemoveFn {
this.handlers.close.push(func);
return () => {
@@ -161,10 +164,8 @@ class WSClient {
* Registers a handler for open socket.
*
* The handler is executed when the socket is open.
- * @param {(object) => void} func - Handler function to register.
- * @return {RemoveFn}
*/
- onOpen(func) {
+ onOpen(func: BaseHandlerFn): RemoveFn {
this.handlers.open.push(func);
return () => {
@@ -177,11 +178,8 @@ class WSClient {
* Registers a handler for socket errors.
*
* The handler is executed when an error is reported by the socket.
- *
- * @param {(object) => void} func - Handler function to register.
- * @return {RemoveFn}
*/
- onError(func) {
+ onError(func: BaseHandlerFn): RemoveFn {
this.handlers.error.push(func);
return () => {
@@ -195,9 +193,9 @@ class WSClient {
*
* Dispatchs an event by running all the handlers.
*
- * @param {object} event - Event object, which is basically a websocket message.
+ * @param event - Event object, which is basically a websocket message.
*/
- dispatchEvent(event) {
+ dispatchEvent(event: MessageEvent) {
const eventObject = JSON.parse(event.data);
this.handlers.events.forEach((f) => f(eventObject));
}
From 77cf091b12efdeb6d627a411fefb6b5ebbd12cad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 26 Dec 2024 15:42:19 +0000
Subject: [PATCH 03/31] refactor(web): migrate contexts to TypeScript
---
web/src/context/{app.jsx => app.tsx} | 7 +--
web/src/context/{auth.jsx => auth.tsx} | 6 +--
...{installer.test.jsx => installer.test.tsx} | 4 +-
.../context/{installer.jsx => installer.tsx} | 43 ++++++++-----------
web/src/context/{root.jsx => root.tsx} | 9 +---
5 files changed, 25 insertions(+), 44 deletions(-)
rename web/src/context/{app.jsx => app.tsx} (89%)
rename web/src/context/{auth.jsx => auth.tsx} (95%)
rename web/src/context/{installer.test.jsx => installer.test.tsx} (94%)
rename web/src/context/{installer.jsx => installer.tsx} (75%)
rename web/src/context/{root.jsx => root.tsx} (84%)
diff --git a/web/src/context/app.jsx b/web/src/context/app.tsx
similarity index 89%
rename from web/src/context/app.jsx
rename to web/src/context/app.tsx
index 9ba49bf1f0..70686fde79 100644
--- a/web/src/context/app.jsx
+++ b/web/src/context/app.tsx
@@ -20,8 +20,6 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
import { InstallerClientProvider } from "./installer";
import { InstallerL10nProvider } from "./installerL10n";
@@ -31,11 +29,8 @@ const queryClient = new QueryClient();
/**
* Combines all application providers.
- *
- * @param {object} props
- * @param {React.ReactNode} [props.children] - content to display within the provider.
*/
-function AppProviders({ children }) {
+function AppProviders({ children }: React.PropsWithChildren) {
return (
diff --git a/web/src/context/auth.jsx b/web/src/context/auth.tsx
similarity index 95%
rename from web/src/context/auth.jsx
rename to web/src/context/auth.tsx
index 536e8c054b..65154a29a7 100644
--- a/web/src/context/auth.jsx
+++ b/web/src/context/auth.tsx
@@ -20,8 +20,6 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { useCallback, useEffect, useState } from "react";
const AuthContext = React.createContext(null);
@@ -48,11 +46,11 @@ const AuthErrors = Object.freeze({
* @param {object} props
* @param {React.ReactNode} [props.children] - content to display within the provider
*/
-function AuthProvider({ children }) {
+function AuthProvider({ children }: React.PropsWithChildren) {
const [isLoggedIn, setIsLoggedIn] = useState(undefined);
const [error, setError] = useState(null);
- const login = useCallback(async (password) => {
+ const login = useCallback(async (password: string) => {
const response = await fetch("/api/auth", {
method: "POST",
body: JSON.stringify({ password }),
diff --git a/web/src/context/installer.test.jsx b/web/src/context/installer.test.tsx
similarity index 94%
rename from web/src/context/installer.test.jsx
rename to web/src/context/installer.test.tsx
index cd55969ffd..5c4125da1e 100644
--- a/web/src/context/installer.test.jsx
+++ b/web/src/context/installer.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -41,7 +41,7 @@ const ClientStatus = () => {
describe("installer context", () => {
beforeEach(() => {
- createDefaultClient.mockImplementation(() => {
+ (createDefaultClient as jest.Mock).mockImplementation(() => {
return {
onConnect: jest.fn(),
onDisconnect: jest.fn(),
diff --git a/web/src/context/installer.jsx b/web/src/context/installer.tsx
similarity index 75%
rename from web/src/context/installer.jsx
rename to web/src/context/installer.tsx
index b2a96aefc2..6b48662e7d 100644
--- a/web/src/context/installer.jsx
+++ b/web/src/context/installer.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2021-2023] SUSE LLC
+ * Copyright (c) [2021-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,10 +20,21 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { useState, useEffect } from "react";
-import { createDefaultClient } from "~/client";
+import { createDefaultClient, InstallerClient } from "~/client";
+
+type ClientStatus = {
+ /** Whether the client is connected or not. */
+ connected: boolean;
+ /** Whether the client present an error and cannot reconnect. */
+ error: boolean;
+};
+
+type InstallerClientProviderProps = React.PropsWithChildren<{
+ /** Client to connect to Agama service; if it is undefined, it instantiates a
+ * new one using the address registered in /run/agama/bus.address. */
+ client?: InstallerClient;
+}>;
const InstallerClientContext = React.createContext(null);
// TODO: we use a separate context to avoid changing all the codes to
@@ -35,10 +46,8 @@ const InstallerClientStatusContext = React.createContext({
/**
* Returns the D-Bus installer client
- *
- * @return {import("~/client").InstallerClient}
*/
-function useInstallerClient() {
+function useInstallerClient(): InstallerClient {
const context = React.useContext(InstallerClientContext);
if (context === undefined) {
throw new Error("useInstallerClient must be used within a InstallerClientProvider");
@@ -49,15 +58,8 @@ function useInstallerClient() {
/**
* Returns the client status.
- *
- * @typedef {object} ClientStatus
- * @property {boolean} connected - whether the client is connected
- * @property {boolean} error - whether the client present an error and cannot
- * reconnect
- *
- * @return {ClientStatus} installer client status
*/
-function useInstallerClientStatus() {
+function useInstallerClientStatus(): ClientStatus {
const context = React.useContext(InstallerClientStatusContext);
if (!context) {
throw new Error("useInstallerClientStatus must be used within a InstallerClientProvider");
@@ -66,16 +68,7 @@ function useInstallerClientStatus() {
return context;
}
-/**
- * @param {object} props
- * @param {import("~/client").InstallerClient|undefined} [props.client] client to connect to
- * Agama service; if it is undefined, it instantiates a new one using the address
- * registered in /run/agama/bus.address.
- * @param {number} [props.interval=2000] - Interval in milliseconds between connection attempt
- * (2000 by default).
- * @param {React.ReactNode} [props.children] - content to display within the provider
- */
-function InstallerClientProvider({ children, client = null }) {
+function InstallerClientProvider({ children, client = null }: InstallerClientProviderProps) {
const [value, setValue] = useState(client);
const [connected, setConnected] = useState(false);
const [error, setError] = useState(false);
diff --git a/web/src/context/root.jsx b/web/src/context/root.tsx
similarity index 84%
rename from web/src/context/root.jsx
rename to web/src/context/root.tsx
index e47d708b9f..cb1dfbd5cc 100644
--- a/web/src/context/root.jsx
+++ b/web/src/context/root.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,19 +20,14 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { Suspense } from "react";
import { AuthProvider } from "./auth";
import { Loading } from "~/components/layout";
/**
* Combines all application providers.
- *
- * @param {object} props
- * @param {React.ReactNode} [props.children] - content to display within the provider.
*/
-function RootProviders({ children }) {
+function RootProviders({ children }: React.PropsWithChildren) {
return (
}>
{children}
From ea242a3a3ce6ee5132cd433d647de18d24e36f6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 26 Dec 2024 16:32:34 +0000
Subject: [PATCH 04/31] refactor(web): migrate src/Protected to TypeScript
---
web/src/{Protected.jsx => Protected.tsx} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename web/src/{Protected.jsx => Protected.tsx} (100%)
diff --git a/web/src/Protected.jsx b/web/src/Protected.tsx
similarity index 100%
rename from web/src/Protected.jsx
rename to web/src/Protected.tsx
From 760a780fcb45ce1292ae8db62845a1d1b40108b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 26 Dec 2024 16:59:29 +0000
Subject: [PATCH 05/31] refactor(web): migrate i18n to TypeScript
---
web/src/{i18n.test.js => i18n.test.ts} | 2 +-
web/src/{i18n.js => i18n.ts} | 44 ++++++++++++--------------
2 files changed, 22 insertions(+), 24 deletions(-)
rename web/src/{i18n.test.js => i18n.test.ts} (98%)
rename web/src/{i18n.js => i18n.ts} (74%)
diff --git a/web/src/i18n.test.js b/web/src/i18n.test.ts
similarity index 98%
rename from web/src/i18n.test.js
rename to web/src/i18n.test.ts
index 483e8ae0fc..6c620a30b1 100644
--- a/web/src/i18n.test.js
+++ b/web/src/i18n.test.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/i18n.js b/web/src/i18n.ts
similarity index 74%
rename from web/src/i18n.js
rename to web/src/i18n.ts
index 93a0bed507..0ab39d6df1 100644
--- a/web/src/i18n.js
+++ b/web/src/i18n.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -30,20 +30,18 @@ import agama from "~/agama";
/**
* Tests whether a special testing language is used.
- *
- * @returns {boolean} true if the testing language is set
*/
-const isTestingLanguage = () => agama.language === "xx";
+const isTestingLanguage = (): boolean => agama.language === "xx";
/**
* "Translate" the string to special "xx" testing language.
* It just replaces all alpha characters with "x".
* It keeps the percent placeholders like "%s" or "%d" unmodified.
*
- * @param {string} str input string
- * @returns {string} "translated" string
+ * @param str input string
+ * @returns "translated" string
*/
-const xTranslate = (str) => {
+const xTranslate = (str: string): string => {
let result = "";
let wasPercent = false;
@@ -70,23 +68,23 @@ const xTranslate = (str) => {
* Returns a translated text in the current locale or the original text if the
* translation is not found.
*
- * @param {string} str the input string to translate
- * @return {string} translated or original text
+ * @param str the input string to translate
+ * @return translated or original text
*/
-const _ = (str) => (isTestingLanguage() ? xTranslate(str) : agama.gettext(str));
+const _ = (str: string): string => (isTestingLanguage() ? xTranslate(str) : agama.gettext(str));
/**
* Similar to the _() function. This variant returns singular or plural form
* depending on an additional "num" argument.
*
* @see {@link _} for further information
- * @param {string} str1 the input string in the singular form
- * @param {string} strN the input string in the plural form
- * @param {number} n the actual number which decides whether to use the
+ * @param str1 the input string in the singular form
+ * @param strN the input string in the plural form
+ * @param n the actual number which decides whether to use the
* singular or plural form
- * @return {string} translated or original text
+ * @return translated or original text
*/
-const n_ = (str1, strN, n) => {
+const n_ = (str1: string, strN: string, n: number): string => {
return isTestingLanguage() ? xTranslate(n === 1 ? str1 : strN) : agama.ngettext(str1, strN, n);
};
@@ -120,22 +118,22 @@ const n_ = (str1, strN, n) => {
* // here the string will be translated using the current locale
* return Result: {_(result)}
;
*
- * @param {string} str the input string
- * @return {string} the input string
+ * @param str the input string
+ * @return the input string
*/
-const N_ = (str) => str;
+const N_ = (str: string): string => str;
/**
* Similar to the N_() function, but for the singular and plural form.
*
* @see {@link N_} for further information
- * @param {string} str1 the input string in the singular form
- * @param {string} strN the input string in the plural form
- * @param {number} n the actual number which decides whether to use the
+ * @param str1 the input string in the singular form
+ * @param strN the input string in the plural form
+ * @param n the actual number which decides whether to use the
* singular or plural form
- * @return {string} the original text, either "string1" or "stringN" depending
+ * @return the original text, either "string1" or "stringN" depending
* on the value "num"
*/
-const Nn_ = (str1, strN, n) => (n === 1 ? str1 : strN);
+const Nn_ = (str1: string, strN: string, n: number): string => (n === 1 ? str1 : strN);
export { _, n_, N_, Nn_ };
From 1e777ec8fe72c750a54f354f5bd9a44b58887c62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 26 Dec 2024 17:19:08 +0000
Subject: [PATCH 06/31] refactor(web): migrate src/agama to TypeScript
---
web/src/agama.js | 99 --------------------------------------------
web/src/agama.ts | 99 ++++++++++++++++++++++++++++++++++++++++++++
web/src/i18n.test.ts | 16 +++----
3 files changed, 108 insertions(+), 106 deletions(-)
delete mode 100644 web/src/agama.js
create mode 100644 web/src/agama.ts
diff --git a/web/src/agama.js b/web/src/agama.js
deleted file mode 100644
index e89e0ded3f..0000000000
--- a/web/src/agama.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) [2024] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This program 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 General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, contact SUSE LLC.
- *
- * To contact SUSE LLC about this file by physical or electronic mail, you may
- * find current contact information at www.suse.com.
- */
-
-/**
- * This module provides a global "agama" object which can be use from other
- * scripts like "po.js".
- */
-
-const agama = {
- // the current language
- language: "en",
-};
-
-// mapping with the current translations
-let translations = {};
-// function used for computing the plural form index
-let plural_fn;
-
-// set the current translations, called from po..js
-agama.locale = function locale(po) {
- if (po) {
- Object.assign(translations, po);
-
- const header = po[""];
- if (header) {
- if (header["plural-forms"]) plural_fn = header["plural-forms"];
- if (header.language) agama.language = header.language;
- }
- } else if (po === null) {
- translations = {};
- plural_fn = undefined;
- agama.language = "en";
- }
-};
-
-/**
- * get a translation for a singular text
- * @param {string} str input text
- * @return translated text or the original text if the translation is not found
- */
-agama.gettext = function gettext(str) {
- if (translations) {
- const translated = translations[str];
- if (translated?.[0]) return translated[0];
- }
-
- // fallback, return the original text
- return str;
-};
-
-/**
- * get a translation for a plural text
- * @param {string} str1 input singular text
- * @param {string} strN input plural text
- * @param {number} n the actual number which decides whether to use the
- * singular or plural form (of which plural form if there are several of them)
- * @return translated text or the original text if the translation is not found
- */
-agama.ngettext = function ngettext(str1, strN, n) {
- if (translations && plural_fn) {
- // plural form translations are indexed by the singular variant
- const translation = translations[str1];
-
- if (translation) {
- const plural_index = plural_fn(n);
-
- // the plural function either returns direct index (integer) in the plural
- // translations or a boolean indicating simple plural form which
- // needs to be converted to index 0 (singular) or 1 (plural)
- const index = plural_index === true ? 1 : plural_index || 0;
-
- if (translation[index]) return translation[index];
- }
- }
-
- // fallback, return the original text
- return n === 1 ? str1 : strN;
-};
-
-export default agama;
diff --git a/web/src/agama.ts b/web/src/agama.ts
new file mode 100644
index 0000000000..9ecc8a57cf
--- /dev/null
+++ b/web/src/agama.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program 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 General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE LLC about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+/**
+ * This module provides a global "agama" object which can be use from other
+ * scripts like "po.js".
+ */
+
+// mapping with the current translations
+let translations = {};
+// function used for computing the plural form index
+let plural_fn: (n: number) => boolean;
+
+const agama = {
+ // the current language
+ language: "en",
+
+ // set the current translations, called from po..js
+ locale: (po) => {
+ if (po) {
+ Object.assign(translations, po);
+
+ const header = po[""];
+ if (header) {
+ if (header["plural-forms"]) plural_fn = header["plural-forms"];
+ if (header.language) agama.language = header.language;
+ }
+ } else if (po === null) {
+ translations = {};
+ plural_fn = undefined;
+ agama.language = "en";
+ }
+ },
+
+ /**
+ * Get a translation for a singular text
+ * @param str input text
+ * @return translated text or the original text if the translation is not found
+ */
+ gettext: (str: string): string => {
+ if (translations) {
+ const translated = translations[str];
+ if (translated?.[0]) return translated[0];
+ }
+
+ // fallback, return the original text
+ return str;
+ },
+
+ /**
+ * get a translation for a plural text
+ * @param str1 input singular text
+ * @param strN input plural text
+ * @param n the actual number which decides whether to use the
+ * singular or plural form (of which plural form if there are several of them)
+ * @return translated text or the original text if the translation is not found
+ */
+ ngettext: (str1: string, strN: string, n: number) => {
+ if (translations && plural_fn) {
+ // plural form translations are indexed by the singular variant
+ const translation = translations[str1];
+
+ if (translation) {
+ const plural_index = plural_fn(n);
+
+ // the plural function either returns direct index (integer) in the plural
+ // translations or a boolean indicating simple plural form which
+ // needs to be converted to index 0 (singular) or 1 (plural)
+ const index = plural_index === true ? 1 : plural_index || 0;
+
+ if (translation[index]) return translation[index];
+ }
+ }
+
+ // fallback, return the original text
+ return n === 1 ? str1 : strN;
+ },
+};
+
+export default agama;
diff --git a/web/src/i18n.test.ts b/web/src/i18n.test.ts
index 6c620a30b1..c3caef75e2 100644
--- a/web/src/i18n.test.ts
+++ b/web/src/i18n.test.ts
@@ -20,15 +20,17 @@
* find current contact information at www.suse.com.
*/
+/* eslint-disable agama-i18n/string-literals */
+
import { _, n_, N_, Nn_ } from "~/i18n";
import agama from "~/agama";
// mock the cockpit gettext functions
-jest.mock("~/agama");
-const gettextFn = jest.fn();
-agama.gettext.mockImplementation(gettextFn);
-const ngettextFn = jest.fn();
-agama.ngettext.mockImplementation(ngettextFn);
+jest.mock("~/agama", () => ({
+ ...jest.requireActual("~/agama"),
+ gettext: jest.fn(),
+ ngettext: jest.fn(),
+}));
// some testing texts
const text = "text to translate";
@@ -40,7 +42,7 @@ describe("i18n", () => {
it("calls the agama.gettext() implementation", () => {
_(text);
- expect(gettextFn).toHaveBeenCalledWith(text);
+ expect(agama.gettext).toHaveBeenCalledWith(text);
});
});
@@ -48,7 +50,7 @@ describe("i18n", () => {
it("calls the agama.ngettext() implementation", () => {
n_(singularText, pluralText, 1);
- expect(ngettextFn).toHaveBeenCalledWith(singularText, pluralText, 1);
+ expect(agama.ngettext).toHaveBeenCalledWith(singularText, pluralText, 1);
});
});
From fd989ffa23e50cdc00308e3722f7ede87f5df085 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 09:00:29 +0000
Subject: [PATCH 07/31] refactor(web): migrate src/utils to TypeScript
---
web/src/components/l10n/TimezoneSelection.tsx | 2 +-
web/src/{utils.test.js => utils.test.ts} | 25 +---
web/src/{utils.js => utils.ts} | 137 ++++++++----------
3 files changed, 62 insertions(+), 102 deletions(-)
rename web/src/{utils.test.js => utils.test.ts} (88%)
rename web/src/{utils.js => utils.ts} (75%)
diff --git a/web/src/components/l10n/TimezoneSelection.tsx b/web/src/components/l10n/TimezoneSelection.tsx
index 0ea385a2ba..614ba8dc5a 100644
--- a/web/src/components/l10n/TimezoneSelection.tsx
+++ b/web/src/components/l10n/TimezoneSelection.tsx
@@ -98,7 +98,7 @@ export default function TimezoneSelection() {
}
description={
- {timezoneTime(id, { date }) || ""}
+ {timezoneTime(id, date) || ""}
{details}
diff --git a/web/src/utils.test.js b/web/src/utils.test.ts
similarity index 88%
rename from web/src/utils.test.js
rename to web/src/utils.test.ts
index 80003d8356..99ced7f4e0 100644
--- a/web/src/utils.test.js
+++ b/web/src/utils.test.ts
@@ -28,7 +28,6 @@ import {
noop,
toValidationError,
localConnection,
- remoteConnection,
isObject,
slugify,
} from "./utils";
@@ -43,7 +42,7 @@ describe("noop", () => {
describe("partition", () => {
it("returns two groups of elements that do and do not satisfy provided filter", () => {
const numbers = [1, 2, 3, 4, 5, 6];
- const [odd, even] = partition(numbers, (number) => number % 2);
+ const [odd, even] = partition(numbers, (number) => number % 2 !== 0);
expect(odd).toEqual([1, 3, 5]);
expect(even).toEqual([2, 4, 6]);
@@ -126,26 +125,6 @@ describe("localConnection", () => {
});
});
-describe("remoteConnection", () => {
- describe("when the page URL is " + localURL, () => {
- it("returns true", () => {
- expect(remoteConnection(localURL)).toEqual(false);
- });
- });
-
- describe("when the page URL is " + localURL2, () => {
- it("returns true", () => {
- expect(remoteConnection(localURL2)).toEqual(false);
- });
- });
-
- describe("when the page URL is " + remoteURL, () => {
- it("returns false", () => {
- expect(remoteConnection(remoteURL)).toEqual(true);
- });
- });
-});
-
describe("isObject", () => {
it("returns true when called with an object", () => {
expect(isObject({ dummy: "object" })).toBe(true);
@@ -156,7 +135,7 @@ describe("isObject", () => {
});
it("returns false when called with undefined", () => {
- expect(isObject()).toBe(false);
+ expect(isObject(undefined)).toBe(false);
});
it("returns false when called with a string", () => {
diff --git a/web/src/utils.js b/web/src/utils.ts
similarity index 75%
rename from web/src/utils.js
rename to web/src/utils.ts
index 50feed2b79..457ef5b99a 100644
--- a/web/src/utils.js
+++ b/web/src/utils.ts
@@ -28,8 +28,8 @@ import { useEffect, useRef, useCallback, useState } from "react";
*
* Borrowed from https://dev.to/alesm0101/how-to-check-if-a-value-is-an-object-in-javascript-3pin
*
- * @param {any} value - the value to be checked
- * @return {boolean} true when given value is an object; false otherwise
+ * @param value - the value to be checked
+ * @return true when given value is an object; false otherwise
*/
const isObject = (value) =>
typeof value === "object" &&
@@ -43,18 +43,18 @@ const isObject = (value) =>
/**
* Whether given object is empty or not
*
- * @param {object} value - the value to be checked
- * @return {boolean} true when given value is an empty object; false otherwise
+ * @param value - the value to be checked
+ * @return true when given value is an empty object; false otherwise
*/
-const isObjectEmpty = (value) => {
+const isObjectEmpty = (value: object) => {
return Object.keys(value).length === 0;
};
/**
* Whether given value is empty or not
*
- * @param {object} value - the value to be checked
- * @return {boolean} false if value is a function, a not empty object, or a not
+ * @param value - the value to be checked
+ * @return false if value is a function, a not empty object, or a not
* empty string; true otherwise
*/
const isEmpty = (value) => {
@@ -84,12 +84,12 @@ const isEmpty = (value) => {
/**
* Returns an empty function useful to be used as a default callback.
*
- * @return {function} empty function
+ * @return empty function
*/
const noop = () => undefined;
/**
- * @return {function} identity function
+ * @return identity function
*/
const identity = (i) => i;
@@ -97,11 +97,14 @@ const identity = (i) => i;
* Returns a new array with a given collection split into two groups, the first holding elements
* satisfying the filter and the second with those which do not.
*
- * @param {Array} collection - the collection to be filtered
- * @param {function} filter - the function to be used as filter
- * @return {Array[]} a pair of arrays, [passing, failing]
+ * @param collection - the collection to be filtered
+ * @param filter - the function to be used as filter
+ * @return a pair of arrays, [passing, failing]
*/
-const partition = (collection, filter) => {
+const partition = (
+ collection: Array,
+ filter: (element: T) => boolean,
+): [Array, Array] => {
const pass = [];
const fail = [];
@@ -114,23 +117,17 @@ const partition = (collection, filter) => {
/**
* Generates a new array without null and undefined values.
- *
- * @param {Array} collection
- * @returns {Array}
*/
-function compact(collection) {
+const compact = (collection: Array) => {
return collection.filter((e) => e !== null && e !== undefined);
-}
+};
/**
* Generates a new array without duplicates.
- *
- * @param {Array} collection
- * @returns {Array}
*/
-function uniq(collection) {
+const uniq = (collection: Array) => {
return [...new Set(collection)];
-}
+};
/**
* Simple utility function to help building className conditionally
@@ -141,12 +138,12 @@ function uniq(collection) {
*
* @todo Use https://github.com/JedWatson/classnames instead?
*
- * @param {...*} classes - CSS classes to join
- * @returns {String} - CSS classes joined together after ignoring falsy values
+ * @param classes - CSS classes to join
+ * @returns CSS classes joined together after ignoring falsy values
*/
-function classNames(...classes) {
+const classNames = (...classes) => {
return classes.filter((item) => !!item).join(" ");
-}
+};
/**
* Convert any string into a slug
@@ -157,10 +154,10 @@ function classNames(...classes) {
* slugify("Agama! / Network 1");
* // returns "agama-network-1"
*
- * @param {string} input - the string to slugify
- * @returns {string} - the slug
+ * @param input - the string to slugify
+ * @returns the slug
*/
-function slugify(input) {
+const slugify = (input: string) => {
if (!input) return "";
return (
@@ -177,26 +174,24 @@ function slugify(input) {
// replace multiple spaces or hyphens with a single hyphen
.replace(/[\s-]+/g, "-")
);
-}
+};
-/**
- * @typedef {Object} cancellableWrapper
- * @property {Promise} promise - Cancellable promise
- * @property {function} cancel - Function for canceling the promise
- */
+type CancellableWrapper = {
+ /** Cancellable promise */
+ promise: Promise;
+ /** Function for cancelling the promise */
+ cancel: Function;
+};
/**
* Creates a wrapper object with a cancellable promise and a function for canceling the promise
*
* @see useCancellablePromise
- *
- * @param {Promise} promise
- * @returns {cancellableWrapper}
*/
-function makeCancellable(promise) {
+const makeCancellable = (promise: Promise): CancellableWrapper => {
let isCanceled = false;
- const cancellablePromise = new Promise((resolve, reject) => {
+ const cancellablePromise: Promise = new Promise((resolve, reject) => {
promise
.then((value) => !isCanceled && resolve(value))
.catch((error) => !isCanceled && reject(error));
@@ -208,7 +203,7 @@ function makeCancellable(promise) {
isCanceled = true;
},
};
-}
+};
/**
* Allows using promises in a safer way.
@@ -217,7 +212,7 @@ function makeCancellable(promise) {
* a promise (e.g., setting the component state once a D-Bus call is answered). Note that nothing
* guarantees that a React component is still mounted when a promise is resolved.
*
- * @see {@link https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions|Race conditions}
+ * @see {@link https://overreacted.io/a-complete-guide-to-useeffect/#speaking-of-race-conditions|Race conditions}
*
* The hook provides a function for making promises cancellable. All cancellable promises are
* automatically canceled once the component is unmounted. Note that the promises are not really
@@ -238,8 +233,8 @@ function makeCancellable(promise) {
* cancellablePromise(promise).then(setState);
* }, [setState, cancellablePromise]);
*/
-function useCancellablePromise() {
- const promises = useRef();
+const useCancellablePromise = () => {
+ const promises = useRef>>();
useEffect(() => {
promises.current = [];
@@ -251,22 +246,22 @@ function useCancellablePromise() {
}, []);
const cancellablePromise = useCallback((promise) => {
- const cancellableWrapper = makeCancellable(promise);
+ const cancellableWrapper: CancellableWrapper = makeCancellable(promise);
promises.current.push(cancellableWrapper);
return cancellableWrapper.promise;
}, []);
return { cancellablePromise };
-}
+};
/** Hook for using local storage
*
* @see {@link https://www.robinwieruch.de/react-uselocalstorage-hook/}
*
- * @param {String} storageKey
- * @param {*} fallbackState
+ * @param storageKey
+ * @param fallbackState
*/
-const useLocalStorage = (storageKey, fallbackState) => {
+const useLocalStorage = (storageKey: string, fallbackState) => {
const [value, setValue] = useState(JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState);
useEffect(() => {
@@ -281,9 +276,8 @@ const useLocalStorage = (storageKey, fallbackState) => {
*
* Source {@link https://designtechworld.medium.com/create-a-custom-debounce-hook-in-react-114f3f245260}
*
- * @param {Function} callback - Function to be called after some delay.
- * @param {number} delay - Delay in milliseconds.
- * @returns {Function}
+ * @param callback - Function to be called after some delay.
+ * @param delay - Delay in milliseconds.
*
* @example
*
@@ -291,7 +285,7 @@ const useLocalStorage = (storageKey, fallbackState) => {
* log("test ", 1) // The message will be logged after at least 1 second.
* log("test ", 2) // Subsequent calls cancels pending calls.
*/
-const useDebounce = (callback, delay) => {
+const useDebounce = (callback: Function, delay: number) => {
const timeoutRef = useRef(null);
useEffect(() => {
@@ -317,10 +311,9 @@ const useDebounce = (callback, delay) => {
};
/**
- * @param {string}
- * @returns {number}
+ * Convert given string to a hexadecimal number
*/
-const hex = (value) => {
+const hex = (value: string) => {
const sanitizedValue = value.replaceAll(".", "");
return parseInt(sanitizedValue, 16);
};
@@ -357,14 +350,14 @@ const locationReload = () => {
* - https://github.com/jsdom/jsdom/blob/master/Changelog.md#2100
* - https://github.com/jsdom/jsdom/issues/3492
*
- * @param {string} query
+ * @param query
*/
-const setLocationSearch = (query) => {
+const setLocationSearch = (query: string) => {
window.location.search = query;
};
/**
- * Is the Agama server running locally?
+ * WetherAgama server is running locally or not.
*
* This function should be used only in special cases, the Agama behavior should
* be the same regardless of the user connection.
@@ -373,9 +366,9 @@ const setLocationSearch = (query) => {
* environment variable to `1`. This can be useful for debugging or for
* development.
*
- * @returns {boolean} `true` if the connection is local, `false` otherwise
+ * @returns `true` if the connection is local, `false` otherwise
*/
-const localConnection = (location = window.location) => {
+const localConnection = (location: Location | URL = window.location) => {
// forced local behavior
if (process.env.LOCAL_CONNECTION === "1") return true;
@@ -385,26 +378,15 @@ const localConnection = (location = window.location) => {
return hostname === "localhost" || hostname.startsWith("127.");
};
-/**
- * Is the Agama server running remotely?
- *
- * @see localConnection
- *
- * @returns {boolean} `true` if the connection is remote, `false` otherwise
- */
-const remoteConnection = (...args) => !localConnection(...args);
-
/**
* Time for the given timezone.
*
- * @param {string} timezone - E.g., "Atlantic/Canary".
- * @param {object} [options]
- * @param {Date} options.date - Date to take the time from.
+ * @param timezone - E.g., "Atlantic/Canary".
+ * @param date - Date to take the time from.
*
- * @returns {string|undefined} - Time in 24 hours format (e.g., "23:56"). Undefined for an unknown
- * timezone.
+ * @returns Time in 24 hours format (e.g., "23:56"). Undefined for an unknown timezone.
*/
-const timezoneTime = (timezone, { date = new Date() }) => {
+const timezoneTime = (timezone: string, date: Date = new Date()): string | undefined => {
try {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
@@ -438,7 +420,6 @@ export {
locationReload,
setLocationSearch,
localConnection,
- remoteConnection,
slugify,
timezoneTime,
};
From 5c38fc895696c00b898c459682784bdf6ff75269 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 11:28:37 +0000
Subject: [PATCH 08/31] refactor(web): migrate test-utils to TypeScript
---
.../{test-utils.test.js => test-utils.test.tsx} | 5 -----
web/src/{test-utils.js => test-utils.tsx} | 14 +++++++-------
2 files changed, 7 insertions(+), 12 deletions(-)
rename web/src/{test-utils.test.js => test-utils.test.tsx} (89%)
rename web/src/{test-utils.js => test-utils.tsx} (92%)
diff --git a/web/src/test-utils.test.js b/web/src/test-utils.test.tsx
similarity index 89%
rename from web/src/test-utils.test.js
rename to web/src/test-utils.test.tsx
index 5c89a5181d..f8baaa092e 100644
--- a/web/src/test-utils.test.js
+++ b/web/src/test-utils.test.tsx
@@ -40,11 +40,6 @@ describe("resetLocalStorage", () => {
expect(window.localStorage.setItem).not.toHaveBeenCalled();
});
- it("does not set an initial state if given value is not an object", () => {
- resetLocalStorage(["wrong", "initial state"]);
- expect(window.localStorage.setItem).not.toHaveBeenCalled();
- });
-
it("sets an initial state if given value is an object", () => {
resetLocalStorage({
storage: "something",
diff --git a/web/src/test-utils.js b/web/src/test-utils.tsx
similarity index 92%
rename from web/src/test-utils.js
rename to web/src/test-utils.tsx
index 7580c0be32..01a69857d3 100644
--- a/web/src/test-utils.js
+++ b/web/src/test-utils.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2023] SUSE LLC
+ * Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -112,7 +112,7 @@ const Providers = ({ children, withL10n }) => {
*
* @see #plainRender for rendering without installer providers
*/
-const installerRender = (ui, options = {}) => {
+const installerRender = (ui: React.ReactNode, options: { withL10n?: boolean } = {}) => {
const queryClient = new QueryClient({});
const Wrapper = ({ children }) => (
@@ -159,11 +159,11 @@ const plainRender = (ui, options = {}) => {
* It can be useful to mock functions that might receive a callback that you can
* execute on-demand during the test.
*
- * @return {[() => () => void, Array<(any) => void>]} a tuple with the mocked function and the list of callbacks.
+ * @return a tuple with the mocked function and the list of callbacks.
*/
-const createCallbackMock = () => {
+const createCallbackMock = (): [(callback: Function) => () => void, Array<(arg0: any) => void>] => {
const callbacks = [];
- const on = (callback) => {
+ const on = (callback: Function) => {
callbacks.push(callback);
return () => {
const position = callbacks.indexOf(callback);
@@ -176,10 +176,10 @@ const createCallbackMock = () => {
/**
* Helper for clearing window.localStorage and setting an initial state if needed.
*
- * @param {Object.} [initialState] - a collection of keys/values as
+ * @param [initialState] - a collection of keys/values as
* expected by {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem Web Storage API setItem method}
*/
-const resetLocalStorage = (initialState) => {
+const resetLocalStorage = (initialState?: { [key: string]: string }) => {
window.localStorage.clear();
if (!isObject(initialState)) return;
From 7ecdd9ec96af70aa29009bac4aed16b6aa5f8cea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 12:23:04 +0000
Subject: [PATCH 09/31] refactor utils
---
web/src/components/core/Popup.tsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/web/src/components/core/Popup.tsx b/web/src/components/core/Popup.tsx
index 9135bfcb6c..ec2ba7f6a8 100644
--- a/web/src/components/core/Popup.tsx
+++ b/web/src/components/core/Popup.tsx
@@ -20,7 +20,7 @@
* find current contact information at www.suse.com.
*/
-import React from "react";
+import React, { isValidElement } from "react";
import { Button, ButtonProps, Modal, ModalProps } from "@patternfly/react-core";
import { Loading } from "~/components/layout";
import { _ } from "~/i18n";
@@ -202,9 +202,8 @@ const Popup = ({
children,
...props
}: PopupProps) => {
- const [actions, content] = partition(
- React.Children.toArray(children),
- (child) => child.type === Actions,
+ const [actions, content] = partition(React.Children.toArray(children), (child) =>
+ isValidElement(child) ? child.type === Actions : false,
);
return (
From 4f8dcdb4ad5f49cf822c56ebdcac678d8580d82a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 14:16:17 +0000
Subject: [PATCH 10/31] fix(web): adjust some types in ZFCP components
---
web/src/components/storage/zfcp/ZFCPDiskActivationPage.tsx | 4 ++--
web/src/components/storage/zfcp/ZFCPDiskForm.tsx | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/web/src/components/storage/zfcp/ZFCPDiskActivationPage.tsx b/web/src/components/storage/zfcp/ZFCPDiskActivationPage.tsx
index 4026d5aeea..7ffa368ffb 100644
--- a/web/src/components/storage/zfcp/ZFCPDiskActivationPage.tsx
+++ b/web/src/components/storage/zfcp/ZFCPDiskActivationPage.tsx
@@ -41,9 +41,9 @@ export default function ZFCPDiskActivationPage() {
const onSubmit = async (formData: LUNInfo & { id: string }) => {
setIsAcceptDisabled(true);
- const result = await cancellablePromise(
+ const result = (await cancellablePromise(
activateZFCPDisk(formData.id, formData.wwpn, formData.lun),
- );
+ )) as Awaited>;
if (result.status === 200) navigate(PATHS.zfcp.root);
setIsAcceptDisabled(false);
diff --git a/web/src/components/storage/zfcp/ZFCPDiskForm.tsx b/web/src/components/storage/zfcp/ZFCPDiskForm.tsx
index 2ac341996e..19f5ab3c4f 100644
--- a/web/src/components/storage/zfcp/ZFCPDiskForm.tsx
+++ b/web/src/components/storage/zfcp/ZFCPDiskForm.tsx
@@ -24,7 +24,7 @@
import React, { FormEvent, useEffect, useState } from "react";
import { Alert, Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core";
-import { AxiosResponseHeaders } from "axios";
+import { AxiosResponse } from "axios";
import { Page } from "~/components/core";
import { useZFCPControllers, useZFCPDisks } from "~/queries/storage/zfcp";
import { inactiveLuns } from "~/utils/zfcp";
@@ -46,7 +46,7 @@ export default function ZFCPDiskForm({
onLoading,
}: {
id: string;
- onSubmit: (formData: FormData) => Promise;
+ onSubmit: (formData: FormData) => Promise;
onLoading: (isLoading: boolean) => void;
}) {
const controllers = useZFCPControllers();
From 8c63e13c7fb4cc7431a549d0ecbecbb000e7aa53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 14:22:33 +0000
Subject: [PATCH 11/31] fix(web): type adjustments
---
web/src/components/network/NetworkPage.tsx | 2 +-
web/src/utils.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/components/network/NetworkPage.tsx b/web/src/components/network/NetworkPage.tsx
index f28015222a..83c3a16166 100644
--- a/web/src/components/network/NetworkPage.tsx
+++ b/web/src/components/network/NetworkPage.tsx
@@ -98,7 +98,7 @@ const NoWifiAvailable = () => (
export default function NetworkPage() {
useNetworkConfigChanges();
const { connections, devices, settings } = useNetwork();
- const [wifiConnections, wiredConnections] = partition(connections, (c) => c.wireless);
+ const [wifiConnections, wiredConnections] = partition(connections, (c) => !!c.wireless);
return (
diff --git a/web/src/utils.ts b/web/src/utils.ts
index 457ef5b99a..3290d65117 100644
--- a/web/src/utils.ts
+++ b/web/src/utils.ts
@@ -245,7 +245,7 @@ const useCancellablePromise = () => {
};
}, []);
- const cancellablePromise = useCallback((promise) => {
+ const cancellablePromise = useCallback((promise: Promise): Promise => {
const cancellableWrapper: CancellableWrapper = makeCancellable(promise);
promises.current.push(cancellableWrapper);
return cancellableWrapper.promise;
From b6e89843be13dc455ac2d5ea4cd38119685f6cd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 14:42:56 +0000
Subject: [PATCH 12/31] fix(web): please ESLint
---
web/src/test-utils.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/web/src/test-utils.tsx b/web/src/test-utils.tsx
index 01a69857d3..39493e288e 100644
--- a/web/src/test-utils.tsx
+++ b/web/src/test-utils.tsx
@@ -20,6 +20,8 @@
* find current contact information at www.suse.com.
*/
+/* eslint-disable i18next/no-literal-string */
+
/**
* A module for providing utility functions for testing
*
@@ -161,7 +163,7 @@ const plainRender = (ui, options = {}) => {
*
* @return a tuple with the mocked function and the list of callbacks.
*/
-const createCallbackMock = (): [(callback: Function) => () => void, Array<(arg0: any) => void>] => {
+const createCallbackMock = (): [(callback: Function) => () => void, Array<(arg0) => void>] => {
const callbacks = [];
const on = (callback: Function) => {
callbacks.push(callback);
From 02b4a4bbb8b33e7ec1a8e525d3513e72ae56a428 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 14:55:50 +0000
Subject: [PATCH 13/31] refactor(web): migrate router to TypeScript
---
web/src/{router.js => router.tsx} | 1 -
1 file changed, 1 deletion(-)
rename web/src/{router.js => router.tsx} (99%)
diff --git a/web/src/router.js b/web/src/router.tsx
similarity index 99%
rename from web/src/router.js
rename to web/src/router.tsx
index f949a76652..8a94477885 100644
--- a/web/src/router.js
+++ b/web/src/router.tsx
@@ -99,7 +99,6 @@ const protectedRoutes = () => [
const router = () =>
createHashRouter([
{
- exact: true,
path: PATHS.login,
element: (
From 96f3db659f50bd366d7ef4be3325606314d48ca9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 15:03:49 +0000
Subject: [PATCH 14/31] refactor(web): migrate src/index to TypeScript
---
web/src/{index.js => index.tsx} | 0
web/webpack.config.js | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
rename web/src/{index.js => index.tsx} (100%)
diff --git a/web/src/index.js b/web/src/index.tsx
similarity index 100%
rename from web/src/index.js
rename to web/src/index.tsx
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 1f7db8f1a6..d92d5291f9 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -101,7 +101,7 @@ module.exports = {
ignored: /node_modules/,
},
entry: {
- index: ["./src/index.js"],
+ index: ["./src/index.tsx"],
},
devServer: {
hot: true,
From 6ccb663c2b2a1ee7350e38a23b8270be6bb83f61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 27 Dec 2024 15:13:41 +0000
Subject: [PATCH 15/31] refactor(web): migrate setupTests to TypeScript
---
web/jest.config.js | 2 +-
web/src/{setupTests.js => setupTests.ts} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename web/src/{setupTests.js => setupTests.ts} (100%)
diff --git a/web/jest.config.js b/web/jest.config.js
index 670ff97b57..728e681612 100644
--- a/web/jest.config.js
+++ b/web/jest.config.js
@@ -135,7 +135,7 @@ module.exports = {
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
- setupFilesAfterEnv: ["/src/setupTests.js"],
+ setupFilesAfterEnv: ["/src/setupTests.ts"],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
diff --git a/web/src/setupTests.js b/web/src/setupTests.ts
similarity index 100%
rename from web/src/setupTests.js
rename to web/src/setupTests.ts
From 815415dab66734f542fddf8218cf773e4bb9e16f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Mon, 30 Dec 2024 14:15:22 +0000
Subject: [PATCH 16/31] refactor(web): migrate hooks to TypeScript
---
web/src/hooks/useNodeSiblings.test.js | 63 ----------------
web/src/hooks/useNodeSiblings.test.tsx | 73 +++++++++++++++++++
...{useNodeSiblings.js => useNodeSiblings.ts} | 26 ++-----
3 files changed, 78 insertions(+), 84 deletions(-)
delete mode 100644 web/src/hooks/useNodeSiblings.test.js
create mode 100644 web/src/hooks/useNodeSiblings.test.tsx
rename web/src/hooks/{useNodeSiblings.js => useNodeSiblings.ts} (54%)
diff --git a/web/src/hooks/useNodeSiblings.test.js b/web/src/hooks/useNodeSiblings.test.js
deleted file mode 100644
index b2a569f845..0000000000
--- a/web/src/hooks/useNodeSiblings.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { renderHook } from "@testing-library/react";
-import useNodeSiblings from "./useNodeSiblings";
-
-// Mocked HTMLElement for testing
-const mockNode = {
- parentNode: {
- children: [
- { setAttribute: jest.fn(), removeAttribute: jest.fn() }, // sibling 1
- { setAttribute: jest.fn(), removeAttribute: jest.fn() }, // sibling 2
- { setAttribute: jest.fn(), removeAttribute: jest.fn() }, // sibling 3
- ],
- },
-};
-
-describe("useNodeSiblings", () => {
- it("should return noop functions when node is not provided", () => {
- const { result } = renderHook(() => useNodeSiblings(null));
- const [addAttribute, removeAttribute] = result.current;
-
- expect(addAttribute).toBeInstanceOf(Function);
- expect(removeAttribute).toBeInstanceOf(Function);
- expect(addAttribute).toEqual(expect.any(Function));
- expect(removeAttribute).toEqual(expect.any(Function));
-
- // Call the noop functions to ensure they don't throw any errors
- expect(() => addAttribute("attribute", "value")).not.toThrow();
- expect(() => removeAttribute("attribute")).not.toThrow();
- });
-
- it("should add attribute to all siblings when addAttribute is called", () => {
- const { result } = renderHook(() => useNodeSiblings(mockNode));
- const [addAttribute] = result.current;
- const attributeName = "attribute";
- const attributeValue = "value";
-
- addAttribute(attributeName, attributeValue);
-
- expect(mockNode.parentNode.children[0].setAttribute).toHaveBeenCalledWith(
- attributeName,
- attributeValue,
- );
- expect(mockNode.parentNode.children[1].setAttribute).toHaveBeenCalledWith(
- attributeName,
- attributeValue,
- );
- expect(mockNode.parentNode.children[2].setAttribute).toHaveBeenCalledWith(
- attributeName,
- attributeValue,
- );
- });
-
- it("should remove attribute from all siblings when removeAttribute is called", () => {
- const { result } = renderHook(() => useNodeSiblings(mockNode));
- const [, removeAttribute] = result.current;
- const attributeName = "attribute";
-
- removeAttribute(attributeName);
-
- expect(mockNode.parentNode.children[0].removeAttribute).toHaveBeenCalledWith(attributeName);
- expect(mockNode.parentNode.children[1].removeAttribute).toHaveBeenCalledWith(attributeName);
- expect(mockNode.parentNode.children[2].removeAttribute).toHaveBeenCalledWith(attributeName);
- });
-});
diff --git a/web/src/hooks/useNodeSiblings.test.tsx b/web/src/hooks/useNodeSiblings.test.tsx
new file mode 100644
index 0000000000..5052fd651b
--- /dev/null
+++ b/web/src/hooks/useNodeSiblings.test.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import { screen, renderHook } from "@testing-library/react";
+import useNodeSiblings from "./useNodeSiblings";
+import { plainRender } from "~/test-utils";
+
+const TestingComponent = () => (
+
+);
+
+describe("useNodeSiblings", () => {
+ it("should return noop functions when node is not provided", () => {
+ const { result } = renderHook(() => useNodeSiblings(null));
+ const [addAttribute, removeAttribute] = result.current;
+
+ expect(addAttribute).toBeInstanceOf(Function);
+ expect(removeAttribute).toBeInstanceOf(Function);
+ expect(addAttribute).toEqual(expect.any(Function));
+ expect(removeAttribute).toEqual(expect.any(Function));
+
+ // Call the noop functions to ensure they don't throw any errors
+ expect(() => addAttribute("attribute", "value")).not.toThrow();
+ expect(() => removeAttribute("attribute")).not.toThrow();
+ });
+
+ it("should add attribute to all siblings when addAttribute is called", () => {
+ plainRender();
+ const targetNode = screen.getByRole("region", { name: "Second sibling" });
+ const firstSibling = screen.getByRole("region", { name: "First sibling" });
+ const thirdSibling = screen.getByRole("region", { name: "Third sibling" });
+ const noSibling = screen.getByRole("region", { name: "Not a sibling" });
+ const { result } = renderHook(() => useNodeSiblings(targetNode));
+ const [addAttribute] = result.current;
+ const attributeName = "attribute";
+ const attributeValue = "value";
+
+ expect(firstSibling).not.toHaveAttribute(attributeName, attributeValue);
+ expect(thirdSibling).not.toHaveAttribute(attributeName, attributeValue);
+ expect(noSibling).not.toHaveAttribute(attributeName, attributeValue);
+
+ addAttribute(attributeName, attributeValue);
+
+ expect(firstSibling).toHaveAttribute(attributeName, attributeValue);
+ expect(thirdSibling).toHaveAttribute(attributeName, attributeValue);
+ expect(noSibling).not.toHaveAttribute(attributeName, attributeValue);
+ });
+
+ it("should remove attribute from all siblings when removeAttribute is called", () => {
+ plainRender();
+ const targetNode = screen.getByRole("region", { name: "Second sibling" });
+ const firstSibling = screen.getByRole("region", { name: "First sibling" });
+ const thirdSibling = screen.getByRole("region", { name: "Third sibling" });
+ const noSibling = screen.getByRole("region", { name: "Not a sibling" });
+ const { result } = renderHook(() => useNodeSiblings(targetNode));
+ const [, removeAttribute] = result.current;
+
+ expect(firstSibling).toHaveAttribute("data-foo", "bar");
+ expect(thirdSibling).toHaveAttribute("data-foo", "bar");
+ expect(noSibling).toHaveAttribute("data-foo", "bar");
+
+ removeAttribute("data-foo");
+
+ expect(firstSibling).not.toHaveAttribute("data-foo", "bar");
+ expect(thirdSibling).not.toHaveAttribute("data-foo", "bar");
+ expect(noSibling).toHaveAttribute("data-foo", "bar");
+ });
+});
diff --git a/web/src/hooks/useNodeSiblings.js b/web/src/hooks/useNodeSiblings.ts
similarity index 54%
rename from web/src/hooks/useNodeSiblings.js
rename to web/src/hooks/useNodeSiblings.ts
index cf98a42d5e..1b21cfc34e 100644
--- a/web/src/hooks/useNodeSiblings.js
+++ b/web/src/hooks/useNodeSiblings.ts
@@ -1,42 +1,26 @@
import { noop } from "~/utils";
-/**
- * Function for adding an attribute to a sibling
- *
- * @typedef {function} addAttributeFn
- * @param {string} attribute - attribute name
- * @param {*} value - value to set
- */
-
-/**
- * Function for removing an attribute from a sibling
- *
- * @typedef {function} removeAttributeFn
- * @param {string} attribute - attribute name
- */
-
/**
* A hook for working with siblings of the node passed as parameter
*
* It returns an array with exactly two functions:
* - First for adding given attribute to siblings
* - Second for removing given attributes from siblings
- *
- * @param {HTMLElement} node
- * @returns {[addAttributeFn, removeAttributeFn]}
*/
-const useNodeSiblings = (node) => {
+const useNodeSiblings = (
+ node: HTMLElement,
+): [(attribute: string, value) => void, (attribute: string) => void] => {
if (!node) return [noop, noop];
const siblings = [...node.parentNode.children].filter((n) => n !== node);
- const addAttribute = (attribute, value) => {
+ const addAttribute = (attribute: string, value) => {
siblings.forEach((sibling) => {
sibling.setAttribute(attribute, value);
});
};
- const removeAttribute = (attribute) => {
+ const removeAttribute = (attribute: string) => {
siblings.forEach((sibling) => {
sibling.removeAttribute(attribute);
});
From c1684a8115ebe3ddfc51f9a48cff0935f3509c79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Mon, 30 Dec 2024 14:33:31 +0000
Subject: [PATCH 17/31] refactor(web): migrate user utils to TypeScript
---
.../users/{utils.test.js => utils.test.ts} | 0
web/src/components/users/{utils.js => utils.ts} | 13 +++++++------
2 files changed, 7 insertions(+), 6 deletions(-)
rename web/src/components/users/{utils.test.js => utils.test.ts} (100%)
rename web/src/components/users/{utils.js => utils.ts} (86%)
diff --git a/web/src/components/users/utils.test.js b/web/src/components/users/utils.test.ts
similarity index 100%
rename from web/src/components/users/utils.test.js
rename to web/src/components/users/utils.test.ts
diff --git a/web/src/components/users/utils.js b/web/src/components/users/utils.ts
similarity index 86%
rename from web/src/components/users/utils.js
rename to web/src/components/users/utils.ts
index c1a9ec62aa..233ff584b5 100644
--- a/web/src/components/users/utils.js
+++ b/web/src/components/users/utils.ts
@@ -22,13 +22,14 @@
/**
* Method which generates username suggestions based on given full name.
- * The method cleans the input name by removing non-alphanumeric characters (except spaces),
+ *
+ * The method cleans given name by removing non-alphanumeric characters (except spaces),
* splits the name into parts, and then generates suggestions based on these parts.
*
- * @param {string} fullName The full name used to generate username suggestions.
- * @returns {string[]} An array of username suggestions.
+ * @param fullName The full name used to generate username suggestions.
+ * @returns An array of username suggestions.
*/
-const suggestUsernames = (fullName) => {
+const suggestUsernames = (fullName: string) => {
if (!fullName) return [];
// Cleaning the name.
@@ -41,7 +42,8 @@ const suggestUsernames = (fullName) => {
// Split the cleaned name into parts.
const parts = cleanedName.split(/\s+/);
- const suggestions = new Set();
+ // Uses Set for avoiding duplicates
+ const suggestions = new Set();
const firstLetters = parts.map((p) => p[0]).join("");
const lastPosition = parts.length - 1;
@@ -66,7 +68,6 @@ const suggestUsernames = (fullName) => {
if (s.length < 3) suggestions.delete(s);
});
- // using Set object to remove duplicates, then converting back to array
return [...suggestions];
};
From e97c575d79e970bdcec3193c5f11cd9376a38b9f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 09:20:59 +0000
Subject: [PATCH 18/31] refactor(web): move index files to TypeScript
---
web/src/components/core/{index.js => index.ts} | 0
web/src/components/l10n/{index.js => index.ts} | 0
web/src/components/layout/{index.js => index.ts} | 0
web/src/components/network/{index.js => index.ts} | 0
web/src/components/overview/{index.js => index.ts} | 0
web/src/components/software/{index.js => index.ts} | 0
web/src/components/storage/dasd/{index.js => index.ts} | 0
web/src/components/storage/{index.js => index.ts} | 0
web/src/components/storage/iscsi/{index.js => index.ts} | 0
web/src/components/storage/zfcp/{index.js => index.ts} | 0
web/src/components/users/{index.js => index.ts} | 0
11 files changed, 0 insertions(+), 0 deletions(-)
rename web/src/components/core/{index.js => index.ts} (100%)
rename web/src/components/l10n/{index.js => index.ts} (100%)
rename web/src/components/layout/{index.js => index.ts} (100%)
rename web/src/components/network/{index.js => index.ts} (100%)
rename web/src/components/overview/{index.js => index.ts} (100%)
rename web/src/components/software/{index.js => index.ts} (100%)
rename web/src/components/storage/dasd/{index.js => index.ts} (100%)
rename web/src/components/storage/{index.js => index.ts} (100%)
rename web/src/components/storage/iscsi/{index.js => index.ts} (100%)
rename web/src/components/storage/zfcp/{index.js => index.ts} (100%)
rename web/src/components/users/{index.js => index.ts} (100%)
diff --git a/web/src/components/core/index.js b/web/src/components/core/index.ts
similarity index 100%
rename from web/src/components/core/index.js
rename to web/src/components/core/index.ts
diff --git a/web/src/components/l10n/index.js b/web/src/components/l10n/index.ts
similarity index 100%
rename from web/src/components/l10n/index.js
rename to web/src/components/l10n/index.ts
diff --git a/web/src/components/layout/index.js b/web/src/components/layout/index.ts
similarity index 100%
rename from web/src/components/layout/index.js
rename to web/src/components/layout/index.ts
diff --git a/web/src/components/network/index.js b/web/src/components/network/index.ts
similarity index 100%
rename from web/src/components/network/index.js
rename to web/src/components/network/index.ts
diff --git a/web/src/components/overview/index.js b/web/src/components/overview/index.ts
similarity index 100%
rename from web/src/components/overview/index.js
rename to web/src/components/overview/index.ts
diff --git a/web/src/components/software/index.js b/web/src/components/software/index.ts
similarity index 100%
rename from web/src/components/software/index.js
rename to web/src/components/software/index.ts
diff --git a/web/src/components/storage/dasd/index.js b/web/src/components/storage/dasd/index.ts
similarity index 100%
rename from web/src/components/storage/dasd/index.js
rename to web/src/components/storage/dasd/index.ts
diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.ts
similarity index 100%
rename from web/src/components/storage/index.js
rename to web/src/components/storage/index.ts
diff --git a/web/src/components/storage/iscsi/index.js b/web/src/components/storage/iscsi/index.ts
similarity index 100%
rename from web/src/components/storage/iscsi/index.js
rename to web/src/components/storage/iscsi/index.ts
diff --git a/web/src/components/storage/zfcp/index.js b/web/src/components/storage/zfcp/index.ts
similarity index 100%
rename from web/src/components/storage/zfcp/index.js
rename to web/src/components/storage/zfcp/index.ts
diff --git a/web/src/components/users/index.js b/web/src/components/users/index.ts
similarity index 100%
rename from web/src/components/users/index.js
rename to web/src/components/users/index.ts
From 62e1b04493dbbbb5256a855f5b7bc3d586f00f63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 11:50:51 +0000
Subject: [PATCH 19/31] refactor(web): migrate core/RowActions to TypeScript
---
web/src/components/core/RowActions.test.tsx | 71 +++++++++++++++++++
.../core/{RowActions.jsx => RowActions.tsx} | 32 +++++----
2 files changed, 90 insertions(+), 13 deletions(-)
create mode 100644 web/src/components/core/RowActions.test.tsx
rename web/src/components/core/{RowActions.jsx => RowActions.tsx} (77%)
diff --git a/web/src/components/core/RowActions.test.tsx b/web/src/components/core/RowActions.test.tsx
new file mode 100644
index 0000000000..25c461d5db
--- /dev/null
+++ b/web/src/components/core/RowActions.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program 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 General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE LLC about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+import React from "react";
+import { IAction } from "@patternfly/react-table";
+import { screen } from "@testing-library/react";
+import { plainRender } from "~/test-utils";
+import { RowActions } from "~/components/core";
+import { Icon } from "../layout";
+import { _ } from "~/i18n";
+
+const mockEditFn = jest.fn();
+const mockDeleteFn = jest.fn();
+
+const actions: IAction[] = [
+ {
+ title: _("Edit"),
+ role: "link",
+ "aria-label": _("Dummy edit action"),
+ onClick: mockEditFn,
+ },
+ {
+ title: _("Delete"),
+ "aria-label": _("Dummy delete action"),
+ icon: ,
+ onClick: mockDeleteFn,
+ isDanger: true,
+ },
+];
+
+describe("RowActions", () => {
+ it("allows interacting with given actions from a dropdown menu", async () => {
+ const { user } = plainRender(
+ ,
+ );
+
+ const button = screen.getByRole("button", { name: "Actions for testing" });
+ await user.click(button);
+ screen.getByRole("menu");
+ const editAction = screen.getByRole("menuitem", { name: "Dummy edit action" });
+ await user.click(editAction);
+ expect(mockEditFn).toHaveBeenCalled();
+ await user.click(button);
+ const deleteAction = screen.getByRole("menuitem", { name: "Dummy delete action" });
+ await user.click(deleteAction);
+ expect(mockDeleteFn).toHaveBeenCalled();
+ });
+});
diff --git a/web/src/components/core/RowActions.jsx b/web/src/components/core/RowActions.tsx
similarity index 77%
rename from web/src/components/core/RowActions.jsx
rename to web/src/components/core/RowActions.tsx
index 8a6321bdc4..27f2be1cd7 100644
--- a/web/src/components/core/RowActions.jsx
+++ b/web/src/components/core/RowActions.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -22,13 +22,21 @@
import React from "react";
import { MenuToggle } from "@patternfly/react-core";
-import { ActionsColumn } from "@patternfly/react-table";
-
+import {
+ ActionsColumn,
+ ActionsColumnProps,
+ CustomActionsToggleProps,
+} from "@patternfly/react-table";
import { Icon } from "~/components/layout";
import { _ } from "~/i18n";
+type RowActionsProps = {
+ id: string;
+ actions: ActionsColumnProps["items"];
+} & Omit;
+
/**
- * Renders icon for selecting the options of a row in a table
+ * Renders available options for a row in a table
* @component
*
* @example
@@ -46,16 +54,14 @@ import { _ } from "~/i18n";
* }
* ]}
* />
- *
- * @param {object} props
- * @param {string} props.id
- * @param {Action[]} props.actions
- * @param {object} [props.rest]
- *
- * @typedef {import("@patternfly/react-table").IAction} Action
*/
-export default function RowActions({ id, actions, "aria-label": toggleAriaLabel, ...rest }) {
- const actionsToggle = (props) => (
+export default function RowActions({
+ id,
+ actions,
+ "aria-label": toggleAriaLabel,
+ ...rest
+}: RowActionsProps) {
+ const actionsToggle = (props: CustomActionsToggleProps) => (
Date: Thu, 2 Jan 2025 12:01:45 +0000
Subject: [PATCH 20/31] refactor(web): migrate core/NumericTextInput to
TypeScript
---
...put.test.jsx => NumericTextInput.test.tsx} | 4 +--
...ericTextInput.jsx => NumericTextInput.tsx} | 30 ++++++++-----------
2 files changed, 14 insertions(+), 20 deletions(-)
rename web/src/components/core/{NumericTextInput.test.jsx => NumericTextInput.test.tsx} (95%)
rename web/src/components/core/{NumericTextInput.jsx => NumericTextInput.tsx} (66%)
diff --git a/web/src/components/core/NumericTextInput.test.jsx b/web/src/components/core/NumericTextInput.test.tsx
similarity index 95%
rename from web/src/components/core/NumericTextInput.test.jsx
rename to web/src/components/core/NumericTextInput.test.tsx
index 2078a24e5c..760aac843e 100644
--- a/web/src/components/core/NumericTextInput.test.jsx
+++ b/web/src/components/core/NumericTextInput.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -29,7 +29,7 @@ import { NumericTextInput } from "~/components/core";
// the given onChange callback is called. The former is more aligned with the
// React Testing Library principles, https://testing-library.com/docs/guiding-principles
const Input = ({ value: initialValue = "" }) => {
- const [value, setValue] = useState(initialValue);
+ const [value, setValue] = useState(initialValue);
return ;
};
diff --git a/web/src/components/core/NumericTextInput.jsx b/web/src/components/core/NumericTextInput.tsx
similarity index 66%
rename from web/src/components/core/NumericTextInput.jsx
rename to web/src/components/core/NumericTextInput.tsx
index 1ecd2aa984..50302af7b2 100644
--- a/web/src/components/core/NumericTextInput.jsx
+++ b/web/src/components/core/NumericTextInput.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,19 +20,14 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
-import { TextInput } from "@patternfly/react-core";
+import { TextInput, TextInputProps } from "@patternfly/react-core";
import { noop } from "~/utils";
-/**
- * Callback function for notifying a valid input change
- *
- * @callback onChangeFn
- * @param {string|number} the input value
- * @return {void}
- */
+type NumericTextInputProps = {
+ value: string | number;
+ onChange: (value: string | number) => void;
+} & Omit;
/**
* Helper component for having an input text limited to not signed numbers
@@ -41,17 +36,16 @@ import { noop } from "~/utils";
* Based on {@link https://www.patternfly.org/components/forms/text-input PF/TextInput}
*
* @note It allows empty value too.
- *
- * @param {object} props
- * @param {string|number} props.value - the input value
- * @param {onChangeFn} props.onChange - the callback to be called when the entered value match the input pattern
- * @param {import("@patternfly/react-core").TextInputProps} props.textInputProps
*/
-export default function NumericTextInput({ value = "", onChange = noop, ...textInputProps }) {
+export default function NumericTextInput({
+ value = "",
+ onChange = noop,
+ ...textInputProps
+}: NumericTextInputProps) {
// NOTE: Using \d* instead of \d+ at the beginning to allow empty
const pattern = /^\d*\.?\d*$/;
- const handleOnChange = (_, value) => {
+ const handleOnChange: TextInputProps["onChange"] = (_, value) => {
if (pattern.test(value)) {
onChange(value);
}
From b01c27ea43603fbfd3c47efe30b715ae627a26dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 12:09:26 +0000
Subject: [PATCH 21/31] fix(web): please ESLint after migrating
NumericTextInput
---
web/src/components/storage/VolumeFields.tsx | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/web/src/components/storage/VolumeFields.tsx b/web/src/components/storage/VolumeFields.tsx
index 3d5beb6a0a..aafe93e25b 100644
--- a/web/src/components/storage/VolumeFields.tsx
+++ b/web/src/components/storage/VolumeFields.tsx
@@ -344,7 +344,6 @@ const SizeManual = ({
onChange({ minSize })}
- validated={errors.minSize && "error"}
+ validated={errors.minSize ? "error" : "default"}
isDisabled={isDisabled}
/>
@@ -419,14 +418,13 @@ and maximum. If no maximum is given then the file system will be as big as possi
onChange({ minSize })}
- validated={errors.minSize && "error"}
+ validated={errors.minSize ? "error" : "default"}
isDisabled={isDisabled}
/>
@@ -453,10 +451,9 @@ and maximum. If no maximum is given then the file system will be as big as possi
Date: Thu, 2 Jan 2025 13:15:06 +0000
Subject: [PATCH 22/31] refactor(web): migrate core/ListSearch to TypeScript
---
...istSearch.test.jsx => ListSearch.test.tsx} | 2 +-
.../core/{ListSearch.jsx => ListSearch.tsx} | 31 ++++++++++---------
2 files changed, 18 insertions(+), 15 deletions(-)
rename web/src/components/core/{ListSearch.test.jsx => ListSearch.test.tsx} (98%)
rename web/src/components/core/{ListSearch.jsx => ListSearch.tsx} (76%)
diff --git a/web/src/components/core/ListSearch.test.jsx b/web/src/components/core/ListSearch.test.tsx
similarity index 98%
rename from web/src/components/core/ListSearch.test.jsx
rename to web/src/components/core/ListSearch.test.tsx
index 1baf97cfea..d29e368dfb 100644
--- a/web/src/components/core/ListSearch.test.jsx
+++ b/web/src/components/core/ListSearch.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/core/ListSearch.jsx b/web/src/components/core/ListSearch.tsx
similarity index 76%
rename from web/src/components/core/ListSearch.jsx
rename to web/src/components/core/ListSearch.tsx
index 1b3467f1f0..780feb574b 100644
--- a/web/src/components/core/ListSearch.jsx
+++ b/web/src/components/core/ListSearch.tsx
@@ -25,44 +25,47 @@ import { SearchInput } from "@patternfly/react-core";
import { _ } from "~/i18n";
import { noop, useDebounce } from "~/utils";
-const search = (elements, term) => {
+type ListSearchProps = {
+ /** Text to display as placeholder for the search input. */
+ placeholder?: string;
+ /** List of elements in which to search. */
+ elements: T[];
+ /** Callback to be called with the filtered list of elements. */
+ onChange: (elements: T[]) => void;
+};
+
+function search(elements: T[], term: string): T[] {
const value = term.toLowerCase();
- const match = (element) => {
+ const match = (element: T) => {
return Object.values(element).join("").toLowerCase().includes(value);
};
return elements.filter(match);
-};
+}
/**
- * TODO: Rename and/or refactor?
* Input field for searching in a given list of elements.
* @component
- *
- * @param {object} props
- * @param {string} [props.placeholder]
- * @param {object[]} [props.elements] - List of elements in which to search.
- * @param {(elements: object[]) => void} [props.onChange] - Callback to be called with the filtered list of elements.
*/
-export default function ListSearch({
+export default function ListSearch({
placeholder = _("Search"),
elements = [],
onChange: onChangeProp = noop,
-}) {
+}: ListSearchProps) {
const [value, setValue] = useState("");
const [resultSize, setResultSize] = useState(elements.length);
- const updateResult = (result) => {
+ const updateResult = (result: T[]) => {
setResultSize(result.length);
onChangeProp(result);
};
- const searchHandler = useDebounce((term) => {
+ const searchHandler = useDebounce((term: string) => {
updateResult(search(elements, term));
}, 500);
- const onChange = (value) => {
+ const onChange = (value: string) => {
setValue(value);
searchHandler(value);
};
From 7ec3d978fc7d01c3a756368f29ec5c610c4d7aa5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 13:17:45 +0000
Subject: [PATCH 23/31] refactor(web): drop core/SectionSkeleton
Because it's no longer use.
---
web/src/components/core/SectionSkeleton.jsx | 42 ---------------------
web/src/components/core/index.ts | 1 -
2 files changed, 43 deletions(-)
delete mode 100644 web/src/components/core/SectionSkeleton.jsx
diff --git a/web/src/components/core/SectionSkeleton.jsx b/web/src/components/core/SectionSkeleton.jsx
deleted file mode 100644
index 59337e133b..0000000000
--- a/web/src/components/core/SectionSkeleton.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (c) [2022-2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This program 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 General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, contact SUSE LLC.
- *
- * To contact SUSE LLC about this file by physical or electronic mail, you may
- * find current contact information at www.suse.com.
- */
-
-import React from "react";
-import { Skeleton } from "@patternfly/react-core";
-import { _ } from "~/i18n";
-
-const WaitingSkeleton = ({ width }) => {
- return ;
-};
-
-const SectionSkeleton = ({ numRows = 2 }) => {
- return (
- <>
- {Array.from({ length: numRows }, (_, i) => {
- const width = i % 2 === 0 ? "50%" : "25%";
- return ;
- })}
- >
- );
-};
-
-export default SectionSkeleton;
diff --git a/web/src/components/core/index.ts b/web/src/components/core/index.ts
index 0345080fd4..dd18a31925 100644
--- a/web/src/components/core/index.ts
+++ b/web/src/components/core/index.ts
@@ -32,7 +32,6 @@ export { default as InstallationFinished } from "./InstallationFinished";
export { default as InstallationProgress } from "./InstallationProgress";
export { default as InstallButton } from "./InstallButton";
export { default as IssuesHint } from "./IssuesHint";
-export { default as SectionSkeleton } from "./SectionSkeleton";
export { default as ListSearch } from "./ListSearch";
export { default as LoginPage } from "./LoginPage";
export { default as RowActions } from "./RowActions";
From dfa51aae7ed60a7e6b804ca8ae0ca9fef1bf8982 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 13:20:17 +0000
Subject: [PATCH 24/31] refactor(web): migratre layout/Center to TypeScript
---
web/src/components/layout/{Center.jsx => Center.tsx} | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
rename web/src/components/layout/{Center.jsx => Center.tsx} (91%)
diff --git a/web/src/components/layout/Center.jsx b/web/src/components/layout/Center.tsx
similarity index 91%
rename from web/src/components/layout/Center.jsx
rename to web/src/components/layout/Center.tsx
index 2f3046ec3a..48c4a53512 100644
--- a/web/src/components/layout/Center.jsx
+++ b/web/src/components/layout/Center.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,8 +20,6 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
/**
@@ -44,11 +42,8 @@ import React from "react";
* To know more, read
* - https://www.w3.org/TR/selectors-4/#relational
* - https://ishadeed.com/article/css-has-parent-selector/
- *
- * @param {object} props
- * @param {React.ReactNode} props.children
*/
-const Center = ({ children }) => (
+const Center = ({ children }: React.PropsWithChildren) => (
From 8876cf528a9a8d7961897d3ee044b258bc4fe168 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 13:23:03 +0000
Subject: [PATCH 25/31] refactor(web): migrate to core/IssuesHint to TypeScript
---
.../core/{IssuesHint.test.jsx => IssuesHint.test.tsx} | 2 +-
web/src/components/core/{IssuesHint.jsx => IssuesHint.tsx} | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
rename web/src/components/core/{IssuesHint.test.jsx => IssuesHint.test.tsx} (97%)
rename web/src/components/core/{IssuesHint.jsx => IssuesHint.tsx} (91%)
diff --git a/web/src/components/core/IssuesHint.test.jsx b/web/src/components/core/IssuesHint.test.tsx
similarity index 97%
rename from web/src/components/core/IssuesHint.test.jsx
rename to web/src/components/core/IssuesHint.test.tsx
index a4a0d5a19e..3b654a88f4 100644
--- a/web/src/components/core/IssuesHint.test.jsx
+++ b/web/src/components/core/IssuesHint.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2023] SUSE LLC
+ * Copyright (c) [2022-2025] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/core/IssuesHint.jsx b/web/src/components/core/IssuesHint.tsx
similarity index 91%
rename from web/src/components/core/IssuesHint.jsx
rename to web/src/components/core/IssuesHint.tsx
index 9a9c92a6d9..86ee00cad7 100644
--- a/web/src/components/core/IssuesHint.jsx
+++ b/web/src/components/core/IssuesHint.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -23,6 +23,7 @@
import React from "react";
import { Hint, HintBody, List, ListItem, Stack } from "@patternfly/react-core";
import { _ } from "~/i18n";
+import { Issue } from "~/types/issues";
export default function IssuesHint({ issues }) {
if (issues === undefined || issues.length === 0) return;
@@ -35,7 +36,7 @@ export default function IssuesHint({ issues }) {
{_("Before starting the installation, you need to address the following problems:")}
- {issues.map((i, idx) => (
+ {issues.map((i: Issue, idx: number) => (
{i.description}
))}
From 39bdcb68f5688b895d4395af58682f52df33d0a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 13:26:12 +0000
Subject: [PATCH 26/31] refactor(web): migrate L10n page and sections to
TypeScript
---
.../components/l10n/{L10nPage.test.jsx => L10nPage.test.tsx} | 2 +-
web/src/components/l10n/{L10nPage.jsx => L10nPage.tsx} | 2 +-
.../overview/{L10nSection.test.jsx => L10nSection.test.tsx} | 2 +-
.../components/overview/{L10nSection.jsx => L10nSection.tsx} | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
rename web/src/components/l10n/{L10nPage.test.jsx => L10nPage.test.tsx} (98%)
rename web/src/components/l10n/{L10nPage.jsx => L10nPage.tsx} (98%)
rename web/src/components/overview/{L10nSection.test.jsx => L10nSection.test.tsx} (97%)
rename web/src/components/overview/{L10nSection.jsx => L10nSection.tsx} (97%)
diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.tsx
similarity index 98%
rename from web/src/components/l10n/L10nPage.test.jsx
rename to web/src/components/l10n/L10nPage.test.tsx
index 7229c51903..48f26d6255 100644
--- a/web/src/components/l10n/L10nPage.test.jsx
+++ b/web/src/components/l10n/L10nPage.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2024] SUSE LLC
+ * Copyright (c) [2022-2025] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.tsx
similarity index 98%
rename from web/src/components/l10n/L10nPage.jsx
rename to web/src/components/l10n/L10nPage.tsx
index 5ead5d01f6..4e0121f788 100644
--- a/web/src/components/l10n/L10nPage.jsx
+++ b/web/src/components/l10n/L10nPage.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2023] SUSE LLC
+ * Copyright (c) [2022-2025] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.tsx
similarity index 97%
rename from web/src/components/overview/L10nSection.test.jsx
rename to web/src/components/overview/L10nSection.test.tsx
index c89155fd5a..efb9a41ff0 100644
--- a/web/src/components/overview/L10nSection.test.jsx
+++ b/web/src/components/overview/L10nSection.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.tsx
similarity index 97%
rename from web/src/components/overview/L10nSection.jsx
rename to web/src/components/overview/L10nSection.tsx
index a82d72cb8d..37308e410d 100644
--- a/web/src/components/overview/L10nSection.jsx
+++ b/web/src/components/overview/L10nSection.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
From 0bb51fd248051ad8495db7f73f525d02c191368c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 13:28:57 +0000
Subject: [PATCH 27/31] refactor(web): migrate core/ProgressText to TypeScript
---
...essText.test.jsx => ProgressText.test.tsx} | 2 +-
.../{ProgressText.jsx => ProgressText.tsx} | 22 +++++++++----------
2 files changed, 12 insertions(+), 12 deletions(-)
rename web/src/components/core/{ProgressText.test.jsx => ProgressText.test.tsx} (97%)
rename web/src/components/core/{ProgressText.jsx => ProgressText.tsx} (82%)
diff --git a/web/src/components/core/ProgressText.test.jsx b/web/src/components/core/ProgressText.test.tsx
similarity index 97%
rename from web/src/components/core/ProgressText.test.jsx
rename to web/src/components/core/ProgressText.test.tsx
index 0c7e49034b..8fc5f21206 100644
--- a/web/src/components/core/ProgressText.test.jsx
+++ b/web/src/components/core/ProgressText.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
diff --git a/web/src/components/core/ProgressText.jsx b/web/src/components/core/ProgressText.tsx
similarity index 82%
rename from web/src/components/core/ProgressText.jsx
rename to web/src/components/core/ProgressText.tsx
index 5ac7dcac57..8a31346e2a 100644
--- a/web/src/components/core/ProgressText.jsx
+++ b/web/src/components/core/ProgressText.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,22 +20,22 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
import { Split, Text } from "@patternfly/react-core";
+type ProgressTextProps = {
+ /** Progress message. */
+ message: string;
+ /** Current step. */
+ current: number;
+ /** Total steps. */
+ total: number;
+};
+
/**
* Progress description
- *
- * @component
- *
- * @param {object} props
- * @param {string} [props.message] Progress message
- * @param {number} [props.current] Current step
- * @param {number} [props.total] Number of steps
*/
-export default function ProgressText({ message, current, total }) {
+export default function ProgressText({ message, current, total }: ProgressTextProps) {
const text = current === 0 ? message : `${message} (${current}/${total})`;
return (
From d6815ceb8a49dffd8ee19decb35ff3a2e0a36c13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Thu, 2 Jan 2025 14:06:23 +0000
Subject: [PATCH 28/31] refactor(web): migrate ProductSelectionProgress to
TypeScript
---
.../product/ProductSelectionProgress.test.tsx | 63 +++++++++++++++++++
...gress.jsx => ProductSelectionProgress.tsx} | 4 +-
2 files changed, 64 insertions(+), 3 deletions(-)
create mode 100644 web/src/components/product/ProductSelectionProgress.test.tsx
rename web/src/components/product/{ProductSelectionProgress.jsx => ProductSelectionProgress.tsx} (96%)
diff --git a/web/src/components/product/ProductSelectionProgress.test.tsx b/web/src/components/product/ProductSelectionProgress.test.tsx
new file mode 100644
index 0000000000..082ac388d2
--- /dev/null
+++ b/web/src/components/product/ProductSelectionProgress.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) [2025] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program 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 General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE LLC about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+import React from "react";
+import { screen } from "@testing-library/react";
+import { installerRender } from "~/test-utils";
+import ProductSelectionProgress from "./ProductSelectionProgress";
+import { ROOT } from "~/routes/paths";
+import { Product } from "~/types/software";
+
+jest.mock("~/components/core/ProgressReport", () => () => ProgressReport Mock
);
+
+let isBusy = false;
+const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed" };
+
+jest.mock("~/queries/status", () => ({
+ ...jest.requireActual("~/queries/status"),
+ useInstallerStatus: () => ({ isBusy }),
+}));
+
+jest.mock("~/queries/software", () => ({
+ ...jest.requireActual("~/queries/software"),
+ useProduct: () => ({ selectedProduct: tumbleweed }),
+}));
+
+describe("ProductSelectionProgress", () => {
+ describe("when installer is not busy", () => {
+ it("redirects to the root path", async () => {
+ installerRender();
+ await screen.findByText(`Navigating to ${ROOT.root}`);
+ });
+ });
+
+ describe("when installer in busy", () => {
+ beforeEach(() => {
+ isBusy = true;
+ });
+
+ it("renders progress report", () => {
+ installerRender();
+ screen.getByText("ProgressReport Mock");
+ });
+ });
+});
diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.tsx
similarity index 96%
rename from web/src/components/product/ProductSelectionProgress.jsx
rename to web/src/components/product/ProductSelectionProgress.tsx
index cfc80bed82..09df5286d2 100644
--- a/web/src/components/product/ProductSelectionProgress.jsx
+++ b/web/src/components/product/ProductSelectionProgress.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2024] SUSE LLC
+ * Copyright (c) [2024-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -29,8 +29,6 @@ import { ROOT as PATHS } from "~/routes/paths";
import { _ } from "~/i18n";
/**
- * @component
- *
* Shows progress steps when a product is selected.
*/
function ProductSelectionProgress() {
From b1f89d38d71d5bf3972268c934037ec188adfe5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 3 Jan 2025 11:48:33 +0000
Subject: [PATCH 29/31] fix(web): improve useNodeSiblings type readbility
---
web/src/hooks/useNodeSiblings.ts | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/web/src/hooks/useNodeSiblings.ts b/web/src/hooks/useNodeSiblings.ts
index 1b21cfc34e..d6418c0979 100644
--- a/web/src/hooks/useNodeSiblings.ts
+++ b/web/src/hooks/useNodeSiblings.ts
@@ -1,5 +1,8 @@
import { noop } from "~/utils";
+type AddAttributeFn = HTMLElement["setAttribute"];
+type RemoveAttributeFn = HTMLElement["removeAttribute"];
+
/**
* A hook for working with siblings of the node passed as parameter
*
@@ -7,20 +10,18 @@ import { noop } from "~/utils";
* - First for adding given attribute to siblings
* - Second for removing given attributes from siblings
*/
-const useNodeSiblings = (
- node: HTMLElement,
-): [(attribute: string, value) => void, (attribute: string) => void] => {
+const useNodeSiblings = (node: HTMLElement): [AddAttributeFn, RemoveAttributeFn] => {
if (!node) return [noop, noop];
const siblings = [...node.parentNode.children].filter((n) => n !== node);
- const addAttribute = (attribute: string, value) => {
+ const addAttribute: AddAttributeFn = (attribute, value) => {
siblings.forEach((sibling) => {
sibling.setAttribute(attribute, value);
});
};
- const removeAttribute = (attribute: string) => {
+ const removeAttribute: RemoveAttributeFn = (attribute: string) => {
siblings.forEach((sibling) => {
sibling.removeAttribute(attribute);
});
From 6d803cca3f37aec654b7b929ffe6d26f7b5453c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 3 Jan 2025 11:49:27 +0000
Subject: [PATCH 30/31] fix(web): stop using translations in test files
---
web/src/components/core/Page.test.tsx | 31 +++++++++----------
.../components/core/PasswordInput.test.tsx | 9 +++---
web/src/components/core/RowActions.test.tsx | 11 +++----
.../storage/DevicesTechMenu.test.tsx | 13 ++++----
4 files changed, 30 insertions(+), 34 deletions(-)
diff --git a/web/src/components/core/Page.test.tsx b/web/src/components/core/Page.test.tsx
index be2515d2a3..20f3393fde 100644
--- a/web/src/components/core/Page.test.tsx
+++ b/web/src/components/core/Page.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023-2024] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -24,7 +24,6 @@ import React from "react";
import { screen, within } from "@testing-library/react";
import { plainRender, mockNavigateFn } from "~/test-utils";
import { Page } from "~/components/core";
-import { _ } from "~/i18n";
let consoleErrorSpy: jest.SpyInstance;
@@ -41,7 +40,7 @@ describe("Page", () => {
it("renders given children", () => {
plainRender(
- {_("The Page Component")}
+ The Page Component
,
);
screen.getByRole("heading", { name: "The Page Component" });
@@ -93,7 +92,7 @@ describe("Page", () => {
describe("Page.Content", () => {
it("renders a node that fills all the available space", () => {
- plainRender({_("The Content")});
+ plainRender(The Content);
const content = screen.getByText("The Content");
expect(content.classList.contains("pf-m-fill")).toBe(true);
});
@@ -149,7 +148,7 @@ describe("Page", () => {
});
describe("Page.Header", () => {
it("renders a node that sticks to top", () => {
- plainRender({_("The Header")});
+ plainRender(The Header);
const content = screen.getByText("The Header");
const container = content.parentNode as HTMLElement;
expect(container.classList.contains("pf-m-sticky-top")).toBe(true);
@@ -158,19 +157,19 @@ describe("Page", () => {
describe("Page.Section", () => {
it("outputs to console.error if both are missing, title and aria-label", () => {
- plainRender({_("Content")});
+ plainRender(Content);
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("must have either"));
});
it("renders a section node", () => {
- plainRender({_("The Content")});
+ plainRender(The Content);
const section = screen.getByRole("region");
within(section).getByText("The Content");
});
it("adds the aria-labelledby attribute when title is given but aria-label is not", () => {
const { rerender } = plainRender(
- {_("The Content")},
+ The Content,
);
const section = screen.getByRole("region");
expect(section).toHaveAttribute("aria-labelledby");
@@ -178,7 +177,7 @@ describe("Page", () => {
// aria-label is given through Page.Section props
rerender(
- {_("The Content")}
+ The Content
,
);
expect(section).not.toHaveAttribute("aria-labelledby");
@@ -186,25 +185,25 @@ describe("Page", () => {
// aria-label is given through pfCardProps
rerender(
- {_("The Content")}
+ The Content
,
);
expect(section).not.toHaveAttribute("aria-labelledby");
// None was given, title nor aria-label
- rerender({_("The Content")});
+ rerender(The Content);
expect(section).not.toHaveAttribute("aria-labelledby");
});
it("renders given content props (title, value, description, actions, and children (content)", () => {
plainRender(
{_("Disable")}}
+ title="A section"
+ value="Enabled"
+ description="Testing section with title, value, description, content, and actions"
+ actions={Disable}
>
- {_("The Content")}
+ The Content
,
);
const section = screen.getByRole("region");
diff --git a/web/src/components/core/PasswordInput.test.tsx b/web/src/components/core/PasswordInput.test.tsx
index 8437498107..ad4f5aca27 100644
--- a/web/src/components/core/PasswordInput.test.tsx
+++ b/web/src/components/core/PasswordInput.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023-2024] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -25,18 +25,17 @@ import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import userEvent from "@testing-library/user-event";
import PasswordInput, { PasswordInputProps } from "./PasswordInput";
-import { _ } from "~/i18n";
describe("PasswordInput Component", () => {
it("renders a password input", () => {
- plainRender();
+ plainRender();
const inputField = screen.getByLabelText("User password");
expect(inputField).toHaveAttribute("type", "password");
});
it("allows revealing the password", async () => {
- plainRender();
+ plainRender();
const passwordInput = screen.getByLabelText("User password");
const button = screen.getByRole("button");
@@ -48,7 +47,7 @@ describe("PasswordInput Component", () => {
it("applies autoFocus behavior correctly", () => {
plainRender(
- ,
+ ,
);
const inputField = screen.getByLabelText("User password");
diff --git a/web/src/components/core/RowActions.test.tsx b/web/src/components/core/RowActions.test.tsx
index 25c461d5db..9f5fc525da 100644
--- a/web/src/components/core/RowActions.test.tsx
+++ b/web/src/components/core/RowActions.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2024] SUSE LLC
+ * Copyright (c) [2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -26,21 +26,20 @@ import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { RowActions } from "~/components/core";
import { Icon } from "../layout";
-import { _ } from "~/i18n";
const mockEditFn = jest.fn();
const mockDeleteFn = jest.fn();
const actions: IAction[] = [
{
- title: _("Edit"),
+ title: "Edit",
role: "link",
- "aria-label": _("Dummy edit action"),
+ "aria-label": "Dummy edit action",
onClick: mockEditFn,
},
{
- title: _("Delete"),
- "aria-label": _("Dummy delete action"),
+ title: "Delete",
+ "aria-label": "Dummy delete action",
icon: ,
onClick: mockDeleteFn,
isDanger: true,
diff --git a/web/src/components/storage/DevicesTechMenu.test.tsx b/web/src/components/storage/DevicesTechMenu.test.tsx
index a527322d8a..5b8bf07358 100644
--- a/web/src/components/storage/DevicesTechMenu.test.tsx
+++ b/web/src/components/storage/DevicesTechMenu.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
@@ -24,7 +24,6 @@ import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import DevicesTechMenu from "./DevicesTechMenu";
-import { _ } from "~/i18n";
import { supportedDASD } from "~/api/storage/dasd";
import { supportedZFCP } from "~/api/storage/zfcp";
@@ -37,7 +36,7 @@ beforeEach(() => {
});
it("contains an entry for configuring iSCSI", async () => {
- const { user } = installerRender();
+ const { user } = installerRender();
const toggler = screen.getByRole("button");
await user.click(toggler);
const link = screen.getByRole("option", { name: /iSCSI/ });
@@ -45,7 +44,7 @@ it("contains an entry for configuring iSCSI", async () => {
});
it("does not contain an entry for configuring DASD when is NOT supported", async () => {
- const { user } = installerRender();
+ const { user } = installerRender();
const toggler = screen.getByRole("button");
await user.click(toggler);
expect(screen.queryByRole("option", { name: /DASD/ })).toBeNull();
@@ -53,7 +52,7 @@ it("does not contain an entry for configuring DASD when is NOT supported", async
it("contains an entry for configuring DASD when is supported", async () => {
(supportedDASD as jest.Mock).mockResolvedValue(true);
- const { user } = installerRender();
+ const { user } = installerRender();
const toggler = screen.getByRole("button");
await user.click(toggler);
const link = screen.getByRole("option", { name: /DASD/ });
@@ -61,7 +60,7 @@ it("contains an entry for configuring DASD when is supported", async () => {
});
it("does not contain an entry for configuring zFCP when is NOT supported", async () => {
- const { user } = installerRender();
+ const { user } = installerRender();
const toggler = screen.getByRole("button");
await user.click(toggler);
expect(screen.queryByRole("option", { name: /DASD/ })).toBeNull();
@@ -69,7 +68,7 @@ it("does not contain an entry for configuring zFCP when is NOT supported", async
it("contains an entry for configuring zFCP when is supported", async () => {
(supportedZFCP as jest.Mock).mockResolvedValue(true);
- const { user } = installerRender();
+ const { user } = installerRender();
const toggler = screen.getByRole("button");
await user.click(toggler);
const link = screen.getByRole("option", { name: /zFCP/ });
From 6ecc3161fca9fa0c25800f33ab27d72ca6675e7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?=
Date: Fri, 3 Jan 2025 11:51:17 +0000
Subject: [PATCH 31/31] fix(web): revert copyright miss-updated
---
web/src/components/core/IssuesHint.test.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/components/core/IssuesHint.test.tsx b/web/src/components/core/IssuesHint.test.tsx
index 3b654a88f4..6bead73a40 100644
--- a/web/src/components/core/IssuesHint.test.tsx
+++ b/web/src/components/core/IssuesHint.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2025] SUSE LLC
+ * Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*