diff --git a/web/src/App.jsx b/web/src/App.jsx
index bef32b5860..d65379b109 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -29,11 +29,8 @@ import { useInstallerClientStatus } from "~/context/installer";
import { useProduct } from "./context/product";
import { CONFIG, INSTALL, STARTUP } from "~/client/phase";
import { BUSY } from "~/client/status";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useL10nConfigChanges } from "~/queries/l10n";
-const queryClient = new QueryClient();
-
/**
* Main application component.
*
diff --git a/web/src/MainLayout.jsx b/web/src/MainLayout.jsx
index e68beb40fe..b830c96a84 100644
--- a/web/src/MainLayout.jsx
+++ b/web/src/MainLayout.jsx
@@ -19,7 +19,7 @@
* find current contact information at www.suse.com.
*/
-import React from "react";
+import React, { Suspense } from "react";
import { Outlet, NavLink, useNavigate } from "react-router-dom";
import {
Button,
@@ -28,7 +28,7 @@ import {
Page, PageSidebar, PageSidebarBody, PageToggleButton,
Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem
} from "@patternfly/react-core";
-import { Icon } from "~/components/layout";
+import { Icon, Loading } from "~/components/layout";
import { About, InstallerOptions, LogsButton } from "~/components/core";
import { _ } from "~/i18n";
import { rootRoutes } from "~/router";
@@ -134,7 +134,9 @@ export default function Root() {
header={}
sidebar={}
>
-
+ }>
+
+
);
}
diff --git a/web/src/components/l10n/KeyboardSelection.jsx b/web/src/components/l10n/KeyboardSelection.jsx
index 12dddfe100..fa58525b08 100644
--- a/web/src/components/l10n/KeyboardSelection.jsx
+++ b/web/src/components/l10n/KeyboardSelection.jsx
@@ -21,10 +21,10 @@
import React, { useState } from "react";
import { Form, FormGroup, Radio, Text } from "@patternfly/react-core";
-import { useLoaderData, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { ListSearch, Page } from "~/components/core";
import { _ } from "~/i18n";
-import { useConfigMutation } from "~/queries/l10n";
+import { useConfigMutation, useL10n } from "~/queries/l10n";
import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
// TODO: Add documentation and typechecking
@@ -32,7 +32,7 @@ import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
export default function KeyboardSelection() {
const navigate = useNavigate();
const setConfig = useConfigMutation();
- const { keymaps, keymap: currentKeymap } = useLoaderData();
+ const { keymaps, selectedKeymap: currentKeymap } = useL10n();
const [selected, setSelected] = useState(currentKeymap.id);
const [filteredKeymaps, setFilteredKeymaps] = useState(
keymaps.sort((k1, k2) => k1.name > k2.name ? 1 : -1)
diff --git a/web/src/components/l10n/KeyboardSelection.test.jsx b/web/src/components/l10n/KeyboardSelection.test.jsx
index fd733dde25..b08daf2659 100644
--- a/web/src/components/l10n/KeyboardSelection.test.jsx
+++ b/web/src/components/l10n/KeyboardSelection.test.jsx
@@ -36,13 +36,13 @@ const mockConfigMutation = {
jest.mock("~/queries/l10n", () => ({
...jest.requireActual("~/queries/l10n"),
- useConfigMutation: () => mockConfigMutation
+ useConfigMutation: () => mockConfigMutation,
+ useL10n: () => ({ keymaps, selectedKeymap: keymaps[0] })
}));
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockNavigateFn,
- useLoaderData: () => ({ keymaps, keymap: keymaps[0] })
}));
it("allows changing the keyboard", async () => {
diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.jsx
index 57a0c6b41e..2eaa70babd 100644
--- a/web/src/components/l10n/L10nPage.jsx
+++ b/web/src/components/l10n/L10nPage.jsx
@@ -25,6 +25,7 @@ import { useLoaderData } from "react-router-dom";
import { ButtonLink, CardField, Page } from "~/components/core";
import { LOCALE_SELECTION_PATH, KEYMAP_SELECTION_PATH, TIMEZONE_SELECTION_PATH } from "~/routes/l10n";
import { _ } from "~/i18n";
+import { useL10n } from "~/queries/l10n";
const Section = ({ label, value, children }) => {
return (
@@ -45,7 +46,11 @@ const Section = ({ label, value, children }) => {
* @component
*/
export default function L10nPage() {
- const { locale, timezone, keymap } = useLoaderData();
+ const {
+ selectedLocale: locale,
+ selectedTimezone: timezone,
+ selectedKeymap: keymap
+ } = useL10n();
return (
<>
diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx
index 1406d0a99c..6ad324dc6e 100644
--- a/web/src/components/l10n/L10nPage.test.jsx
+++ b/web/src/components/l10n/L10nPage.test.jsx
@@ -25,18 +25,39 @@ import L10nPage from "~/components/l10n/L10nPage";
let mockLoadedData;
+const locales = [
+ { id: "en_US.UTF-8", name: "English", territory: "United States" },
+ { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }
+];
+
+const keymaps = [
+ { id: "us", name: "English" },
+ { id: "es", name: "Spanish" }
+];
+
+const timezones = [
+ { id: "Europe/Berlin", parts: ["Europe", "Berlin"] },
+ { id: "Europe/Madrid", parts: ["Europe", "Madrid"] }
+];
+
jest.mock('react-router-dom', () => ({
...jest.requireActual("react-router-dom"),
- useLoaderData: () => mockLoadedData,
// TODO: mock the link because it needs a working router.
Link: ({ children }) =>
}));
+jest.mock("~/queries/l10n", () => ({
+ useL10n: () => mockLoadedData
+}));
+
beforeEach(() => {
mockLoadedData = {
- locale: { id: "en_US.UTF-8", name: "English", territory: "United States" },
- keymap: { id: "us", name: "English" },
- timezone: { id: "Europe/Berlin", parts: ["Europe", "Berlin"] }
+ locales,
+ keymaps,
+ timezones,
+ selectedLocale: locales[0],
+ selectedKeymap: keymaps[0],
+ selectedTimezone: timezones[0],
};
});
@@ -49,7 +70,7 @@ it("renders a section for configuring the language", () => {
describe("if there is no selected language", () => {
beforeEach(() => {
- mockLoadedData.locale = undefined;
+ mockLoadedData.selectedLocale = undefined;
});
it("renders a button for selecting a language", () => {
@@ -69,7 +90,7 @@ it("renders a section for configuring the keyboard", () => {
describe("if there is no selected keyboard", () => {
beforeEach(() => {
- mockLoadedData.keymap = undefined;
+ mockLoadedData.selectedKeymap = undefined;
});
it("renders a button for selecting a keyboard", () => {
@@ -89,7 +110,7 @@ it("renders a section for configuring the time zone", () => {
describe("if there is no selected time zone", () => {
beforeEach(() => {
- mockLoadedData.timezone = undefined;
+ mockLoadedData.selectedTimezone = undefined;
});
it("renders a button for selecting a time zone", () => {
diff --git a/web/src/components/l10n/LocaleSelection.jsx b/web/src/components/l10n/LocaleSelection.jsx
index 9f143b76dd..543c18336a 100644
--- a/web/src/components/l10n/LocaleSelection.jsx
+++ b/web/src/components/l10n/LocaleSelection.jsx
@@ -21,10 +21,10 @@
import React, { useState } from "react";
import { Flex, Form, FormGroup, Radio } from "@patternfly/react-core";
-import { useLoaderData, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { ListSearch, Page } from "~/components/core";
import { _ } from "~/i18n";
-import { useConfigMutation } from "~/queries/l10n";
+import { useConfigMutation, useL10n } from "~/queries/l10n";
import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
// TODO: Add documentation and typechecking
@@ -32,7 +32,7 @@ import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
export default function LocaleSelection() {
const navigate = useNavigate();
const setConfig = useConfigMutation();
- const { locales, locale: currentLocale } = useLoaderData();
+ const { locales, selectedLocale: currentLocale } = useL10n();
const [selected, setSelected] = useState(currentLocale.id);
const [filteredLocales, setFilteredLocales] = useState(locales);
diff --git a/web/src/components/l10n/LocaleSelection.test.jsx b/web/src/components/l10n/LocaleSelection.test.jsx
index 1fc001578f..a104ddcd30 100644
--- a/web/src/components/l10n/LocaleSelection.test.jsx
+++ b/web/src/components/l10n/LocaleSelection.test.jsx
@@ -36,13 +36,13 @@ const mockConfigMutation = {
jest.mock("~/queries/l10n", () => ({
...jest.requireActual("~/queries/l10n"),
+ useL10n: () => ({ locales, selectedLocale: locales[0] }),
useConfigMutation: () => mockConfigMutation
}));
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
- useNavigate: () => mockNavigateFn,
- useLoaderData: () => ({ locales, locale: locales[0] })
+ useNavigate: () => mockNavigateFn
}));
it("allows changing the keyboard", async () => {
diff --git a/web/src/components/l10n/TimezoneSelection.jsx b/web/src/components/l10n/TimezoneSelection.jsx
index 4650bfa52b..a67c65fc5a 100644
--- a/web/src/components/l10n/TimezoneSelection.jsx
+++ b/web/src/components/l10n/TimezoneSelection.jsx
@@ -21,11 +21,11 @@
import React, { useState } from "react";
import { Divider, Flex, Form, FormGroup, Radio, Text } from "@patternfly/react-core";
-import { useLoaderData, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { ListSearch, Page } from "~/components/core";
import { _ } from "~/i18n";
import { timezoneTime } from "~/utils";
-import { useConfigMutation } from "~/queries/l10n";
+import { useConfigMutation, useL10n } from "~/queries/l10n";
import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
let date;
@@ -56,7 +56,7 @@ export default function TimezoneSelection() {
date = new Date();
const navigate = useNavigate();
const setConfig = useConfigMutation();
- const { timezones, timezone: currentTimezone } = useLoaderData();
+ const { timezones, selectedTimezone: currentTimezone } = useL10n();
const displayTimezones = timezones.map(timezoneWithDetails);
const [selected, setSelected] = useState(currentTimezone.id);
const [filteredTimezones, setFilteredTimezones] = useState(sortedTimezones(displayTimezones));
diff --git a/web/src/components/l10n/TimezoneSelection.test.jsx b/web/src/components/l10n/TimezoneSelection.test.jsx
index efecd089fb..61e882fc8f 100644
--- a/web/src/components/l10n/TimezoneSelection.test.jsx
+++ b/web/src/components/l10n/TimezoneSelection.test.jsx
@@ -36,13 +36,13 @@ const mockConfigMutation = {
jest.mock("~/queries/l10n", () => ({
...jest.requireActual("~/queries/l10n"),
- useConfigMutation: () => mockConfigMutation
+ useConfigMutation: () => mockConfigMutation,
+ useL10n: () => ({ timezones, selectedTimezone: timezones[0] })
}));
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
- useNavigate: () => mockNavigateFn,
- useLoaderData: () => ({ timezones, timezone: timezones[0] })
+ useNavigate: () => mockNavigateFn
}));
it("allows changing the keyboard", async () => {
diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.jsx
index 1b2c8112dc..b909e79e07 100644
--- a/web/src/components/overview/L10nSection.jsx
+++ b/web/src/components/overview/L10nSection.jsx
@@ -23,16 +23,10 @@ import React from "react";
import { TextContent, Text, TextVariants } from "@patternfly/react-core";
import { Em } from "~/components/core";
import { _ } from "~/i18n";
-import { localesQuery, configQuery } from "~/queries/l10n";
-import { useQuery } from "@tanstack/react-query";
+import { useL10n } from "~/queries/l10n";
export default function L10nSection() {
- const { isPending: isLocalesPending, data: locales } = useQuery(localesQuery());
- const { isPending: isConfigPending, data: config } = useQuery(configQuery());
-
- if (isLocalesPending || isConfigPending) return;
-
- const locale = locales.find((l) => l.id === config?.locales[0]);
+ const { selectedLocale: locale } = useL10n();
// TRANSLATORS: %s will be replaced by a language name and territory, example:
// "English (United States)".
diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx
index dad4cde147..a3f29f6945 100644
--- a/web/src/components/overview/L10nSection.test.jsx
+++ b/web/src/components/overview/L10nSection.test.jsx
@@ -30,14 +30,7 @@ const locales = [
];
jest.mock("~/queries/l10n", () => ({
- localesQuery: () => ({
- queryKey: ["l10n", "locales"],
- queryFn: jest.fn().mockResolvedValue(locales)
- }),
- configQuery: () => ({
- queryKey: ["l10n", "config"],
- queryFn: jest.fn().mockResolvedValue({ locales: ["en_US.UTF-8"] })
- })
+ useL10n: () => ({ locales, selectedLocale: locales[0] }),
}));
it("displays the selected locale", async () => {
diff --git a/web/src/context/app.jsx b/web/src/context/app.jsx
index d22ccce51e..3b9a83f0bc 100644
--- a/web/src/context/app.jsx
+++ b/web/src/context/app.jsx
@@ -52,4 +52,4 @@ function AppProviders({ children }) {
);
}
-export { AppProviders, queryClient };
+export { AppProviders };
diff --git a/web/src/queries/hooks.js b/web/src/queries/hooks.js
deleted file mode 100644
index 1c64660b2f..0000000000
--- a/web/src/queries/hooks.js
+++ /dev/null
@@ -1,65 +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 version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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 { useRevalidator } from "react-router-dom";
-import { useQueryClient } from "@tanstack/react-query";
-
-/**
- * Allows invalidating cached data
- *
- * This hook is useful for marking data as outdated and retrieve it again. To do so, it performs two important steps
- * - ask @tanstack/react-query to invalidate query matching given key
- * - ask react-router-dom for a revalidation of loaded data
- *
- * TODO: rethink the revalidation; we may decide to keep the outdated data
- * instead, but warning the user about it (as Github does when reviewing a PR,
- * for example)
- *
- * TODO: allow to specify more than one queryKey
- *
- * To know more, please visit the documentation of these dependencies
- *
- * - https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation
- * - https://reactrouter.com/en/main/hooks/use-revalidator#userevalidator
- *
- * @example
- *
- * const dataInvalidator = useDataInvalidator();
- *
- * useEffect(() => {
- * dataInvalidator({ queryKey: ["user", "auth"] })
- * }, [dataInvalidator]);
- */
-const useDataInvalidator = () => {
- const queryClient = useQueryClient();
- const revalidator = useRevalidator();
-
- const dataInvalidator = ({ queryKey }) => {
- if (queryKey) queryClient.invalidateQueries({ queryKey });
- revalidator.revalidate();
- };
-
- return dataInvalidator;
-};
-
-export {
- useDataInvalidator
-};
diff --git a/web/src/queries/hooks.test.js b/web/src/queries/hooks.test.js
deleted file mode 100644
index 6db7d4b3ee..0000000000
--- a/web/src/queries/hooks.test.js
+++ /dev/null
@@ -1,49 +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 version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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 { renderHook } from "@testing-library/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { useDataInvalidator } from "~/queries/hooks.js";
-
-const mockRevalidateFn = jest.fn();
-jest.mock("react-router-dom", () => ({
- ...jest.requireActual("react-router-dom"),
- useRevalidator: () => ({ revalidate: mockRevalidateFn })
-}));
-
-const queryClient = new QueryClient();
-jest.spyOn(queryClient, "invalidateQueries");
-const wrapper = ({ children }) => (
-
- {children}
-
-);
-
-describe("useDataInvalidator", () => {
- it("forces a data/cache refresh", () => {
- const { result } = renderHook(() => useDataInvalidator(), { wrapper });
- const { current: dataInvalidator } = result;
- dataInvalidator({ queryKey: "fakeQuery" });
- expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: "fakeQuery" });
- expect(mockRevalidateFn).toHaveBeenCalled();
- });
-});
diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js
index 75d193251b..66d81f851c 100644
--- a/web/src/queries/l10n.js
+++ b/web/src/queries/l10n.js
@@ -20,9 +20,8 @@
*/
import React from "react";
-import { useQueryClient, useMutation } from "@tanstack/react-query";
+import { useQueryClient, useMutation, useSuspenseQueries } from "@tanstack/react-query";
import { useInstallerClient } from "~/context/installer";
-import { useDataInvalidator } from "~/queries/hooks";
import { timezoneUTCOffset } from "~/utils";
/**
@@ -30,7 +29,7 @@ import { timezoneUTCOffset } from "~/utils";
*/
const configQuery = () => {
return {
- queryKey: ["l10n", "config"],
+ queryKey: ["l10n/config"],
queryFn: () => fetch("/api/l10n/config").then((res) => res.json()),
};
};
@@ -39,7 +38,7 @@ const configQuery = () => {
* Returns a query for retrieving the list of known locales
*/
const localesQuery = () => ({
- queryKey: ["l10n", "locales"],
+ queryKey: ["l10n/locales"],
queryFn: async () => {
const response = await fetch("/api/l10n/locales");
const locales = await response.json();
@@ -54,7 +53,7 @@ const localesQuery = () => ({
* Returns a query for retrieving the list of known timezones
*/
const timezonesQuery = () => ({
- queryKey: ["l10n", "timezones"],
+ queryKey: ["l10n/timezones"],
queryFn: async () => {
const response = await fetch("/api/l10n/timezones");
const timezones = await response.json();
@@ -70,7 +69,7 @@ const timezonesQuery = () => ({
* Returns a query for retrieving the list of known keymaps
*/
const keymapsQuery = () => ({
- queryKey: ["l10n", "keymaps"],
+ queryKey: ["l10n/keymaps"],
queryFn: async () => {
const response = await fetch("/api/l10n/keymaps");
const json = await response.json();
@@ -108,7 +107,7 @@ const useConfigMutation = () => {
* revalidate its data (executing the loaders again).
*/
const useL10nConfigChanges = () => {
- const dataInvalidator = useDataInvalidator();
+ const queryClient = useQueryClient();
const client = useInstallerClient();
React.useEffect(() => {
@@ -116,10 +115,35 @@ const useL10nConfigChanges = () => {
return client.ws().onEvent(event => {
if (event.type === "L10nConfigChanged") {
- dataInvalidator({ queryKey: ["l10n", "config"] });
+ queryClient.invalidateQueries({ queryKey: ["l10n/config"] });
}
});
- }, [client, dataInvalidator]);
+ }, [client, queryClient]);
+};
+
+/// Returns the l10n data.
+const useL10n = () => {
+ const [
+ { data: config },
+ { data: locales },
+ { data: keymaps },
+ { data: timezones }
+ ] = useSuspenseQueries({
+ queries: [
+ configQuery(),
+ localesQuery(),
+ keymapsQuery(),
+ timezonesQuery()
+ ]
+ });
+
+ const selectedLocale = locales.find((l) => l.id === config.locales[0]);
+ const selectedKeymap = keymaps.find((k) => k.id === config.keymap);
+ const selectedTimezone = timezones.find((t) => t.id === config.timezone);
+
+ return {
+ locales, keymaps, timezones, selectedLocale, selectedKeymap, selectedTimezone
+ };
};
export {
@@ -128,5 +152,6 @@ export {
localesQuery,
timezonesQuery,
useConfigMutation,
+ useL10n,
useL10nConfigChanges
};
diff --git a/web/src/routes/l10n.js b/web/src/routes/l10n.js
index b6d26c10ff..3e0df54833 100644
--- a/web/src/routes/l10n.js
+++ b/web/src/routes/l10n.js
@@ -36,20 +36,6 @@ const LOCALE_SELECTION_PATH = "locale/select";
const KEYMAP_SELECTION_PATH = "keymap/select";
const TIMEZONE_SELECTION_PATH = "timezone/select";
-const l10nLoader = async () => {
- const config = await queryClient.fetchQuery(configQuery());
- const locales = await queryClient.fetchQuery(localesQuery());
- const keymaps = await queryClient.fetchQuery(keymapsQuery());
- const timezones = await queryClient.fetchQuery(timezonesQuery());
-
- const { locales: [localeId], keymap: keymapId, timezone: timezoneId } = config;
- const locale = locales.find((l) => l.id === localeId);
- const keymap = keymaps.find((k) => k.id === keymapId);
- const timezone = timezones.find((t) => t.id === timezoneId);
-
- return { locales, locale, keymaps, keymap, timezones, timezone };
-};
-
const routes = {
path: L10N_PATH,
element: ,
@@ -60,22 +46,18 @@ const routes = {
children: [
{
index: true,
- loader: l10nLoader,
element:
},
{
path: LOCALE_SELECTION_PATH,
- loader: l10nLoader,
element: ,
},
{
path: KEYMAP_SELECTION_PATH,
- loader: l10nLoader,
element: ,
},
{
path: TIMEZONE_SELECTION_PATH,
- loader: l10nLoader,
element: ,
}
]