Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
-------------------------------------------------------------------
Tue Feb 25 13:08:43 UTC 2025 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- Use the backend's language as fallback, ignoring the browser's one
It reduces the chances to produce unwanted side-effects when connecting
to the web user interface (gh#agama-project/agama#2071).
- Do not block when connecting during system installation.
- Make some small fixes/improvements to the overview, localization
and software pages markup.

-------------------------------------------------------------------
Tue Feb 25 12:36:50 UTC 2025 - Ancor Gonzalez Sosa <ancor@suse.com>

Expand Down
4 changes: 3 additions & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ function App() {
const location = useLocation();
const { isBusy, phase } = useInstallerStatus({ suspense: true });
const { connected, error } = useInstallerClientStatus();
const { selectedProduct, products } = useProduct({ suspense: true });
const { selectedProduct, products } = useProduct({
suspense: phase !== InstallationPhase.Install,
});
const { language } = useInstallerL10n();
useL10nConfigChanges();
useProductChanges();
Expand Down
19 changes: 7 additions & 12 deletions web/src/components/l10n/KeyboardSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
*/

import React, { useState } from "react";
import { Content, Form, FormGroup, Radio } from "@patternfly/react-core";
import { Content, Flex, Form, FormGroup, Radio } from "@patternfly/react-core";
import { useNavigate } from "react-router-dom";
import { ListSearch, Page } from "~/components/core";
import { _ } from "~/i18n";
import { useConfigMutation, useL10n } from "~/queries/l10n";
import textStyles from "@patternfly/react-styles/css/utilities/Text/text";

// TODO: Add documentation
// TODO: Evaluate if worth it extracting the selector
Expand Down Expand Up @@ -55,12 +54,10 @@ export default function KeyboardSelection() {
name="keymap"
onChange={() => setSelected(id)}
label={
<>
<span className={`${textStyles.fontSizeLg}`}>
<b>{name}</b>
</span>{" "}
<Flex columnGap={{ default: "columnGapSm" }}>
<Content isEditorial>{name}</Content>
<Content component="small">{id}</Content>
</>
</Flex>
}
value={id}
isChecked={id === selected}
Expand All @@ -80,11 +77,9 @@ export default function KeyboardSelection() {
</Page.Header>

<Page.Content>
<Page.Section>
<Form id="keymapSelection" onSubmit={onSubmit}>
<FormGroup isStack>{keymapsList}</FormGroup>
</Form>
</Page.Section>
<Form id="keymapSelection" onSubmit={onSubmit}>
<FormGroup isStack>{keymapsList}</FormGroup>
</Form>
</Page.Content>

<Page.Actions>
Expand Down
20 changes: 6 additions & 14 deletions web/src/components/l10n/LocaleSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,9 @@ export default function LocaleSelection() {
onChange={() => setSelected(id)}
label={
<Flex gap={{ default: "gapSm" }}>
<span className={textStyles.fontSizeLg}>
<b>{name}</b>
</span>
<span className={[textStyles.fontSizeMd, textStyles.textColorPlaceholder].join(" ")}>
{territory}
</span>
<span className={[textStyles.fontSizeXs, textStyles.textColorSubtle].join(" ")}>
{id}
</span>
<Content isEditorial>{name}</Content>
<Content className={`${textStyles.textColorPlaceholder}`}>{territory}</Content>
<Content className={`${textStyles.textColorSubtle}`}>{id}</Content>
</Flex>
}
value={id}
Expand All @@ -83,11 +77,9 @@ export default function LocaleSelection() {
</Page.Header>

<Page.Content>
<Page.Section>
<Form id="localeSelection" onSubmit={onSubmit}>
<FormGroup isStack>{localesList}</FormGroup>
</Form>
</Page.Section>
<Form id="localeSelection" onSubmit={onSubmit}>
<FormGroup isStack>{localesList}</FormGroup>
</Form>
</Page.Content>

<Page.Actions>
Expand Down
27 changes: 12 additions & 15 deletions web/src/components/l10n/TimezoneSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
*/

import React, { useState } from "react";
import { Content, Divider, Flex, Form, FormGroup, Radio } from "@patternfly/react-core";
import { Content, Flex, Form, FormGroup, Radio } from "@patternfly/react-core";
import { useNavigate } from "react-router-dom";
import { ListSearch, Page } from "~/components/core";
import { timezoneTime } from "~/utils";
import { useConfigMutation, useL10n } from "~/queries/l10n";
import { Timezone } from "~/types/l10n";
import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { _ } from "~/i18n";

type TimezoneWithDetails = Timezone & { details: string };
Expand Down Expand Up @@ -89,18 +89,17 @@ export default function TimezoneSelection() {
name="timezone"
onChange={() => setSelected(id)}
label={
<>
<span className={`${textStyles.fontSizeLg}`}>
<b>{parts.join("-")}</b>
</span>{" "}
<Flex columnGap={{ default: "columnGapSm" }}>
<Content isEditorial className={`${spacingStyles.m_0}`}>
{parts.join("-")}
</Content>
<Content component="small">{country}</Content>
</>
</Flex>
}
description={
<Flex columnGap={{ default: "columnGapXs" }}>
<Flex columnGap={{ default: "columnGapSm" }}>
<Content component="small">{timezoneTime(id, date) || ""}</Content>
<Divider orientation={{ default: "vertical" }} />
<div>{details}</div>
<Content>{details}</Content>
</Flex>
}
value={id}
Expand All @@ -126,11 +125,9 @@ export default function TimezoneSelection() {
</Page.Header>

<Page.Content>
<Page.Section>
<Form id="timezoneSelection" onSubmit={onSubmit}>
<FormGroup isStack>{timezonesList}</FormGroup>
</Form>
</Page.Section>
<Form id="timezoneSelection" onSubmit={onSubmit}>
<FormGroup isStack>{timezonesList}</FormGroup>
</Form>
</Page.Content>

<Page.Actions>
Expand Down
7 changes: 2 additions & 5 deletions web/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,10 @@ const Layout = ({
mountSidebar && setIsSidebarOpen(newWindowSize >= agamaWidthBreakpoints.lg);
};

const pageProps: Omit<PageProps, keyof React.HTMLProps<HTMLDivElement>> = {
isManagedSidebar: true,
};
const pageProps: Omit<PageProps, keyof React.HTMLProps<HTMLDivElement>> = {};

if (mountSidebar) {
pageProps.sidebar = <Sidebar isManagedSidebar={false} isSidebarOpen={isSidebarOpen} />;
pageProps.isManagedSidebar = false;
pageProps.sidebar = <Sidebar isSidebarOpen={isSidebarOpen} />;
}
if (mountHeader) {
pageProps.masthead = (
Expand Down
22 changes: 10 additions & 12 deletions web/src/components/overview/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,16 @@ export default function OverviewPage() {
<Page.Content>
<Grid hasGutter>
<GridItem sm={12}>
<Page.Section
aria-label={_("Overview")}
description={_(
"These are the most relevant installation settings. Feel free to browse the sections in the menu for further details.",
)}
>
<Stack hasGutter>
<L10nSection />
<StorageSection />
<SoftwareSection />
</Stack>
</Page.Section>
<Stack hasGutter>
<Content>
{_(
"These are the most relevant installation settings. Feel free to browse the sections in the menu for further details.",
)}
</Content>
<L10nSection />
<StorageSection />
<SoftwareSection />
</Stack>
</GridItem>
</Grid>
</Page.Content>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/software/SoftwarePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function SoftwarePage(): React.ReactNode {
</GridItem>
{proposal.size && (
<GridItem sm={12} xl={6}>
<Page.Section>
<Page.Section aria-label={_("Used space")}>
<UsedSize size={proposal.size} />
</Page.Section>
</GridItem>
Expand Down
63 changes: 2 additions & 61 deletions web/src/context/installerL10n.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2023] SUSE LLC
* Copyright (c) [2023-2025] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -120,37 +120,6 @@ describe("InstallerL10nProvider", () => {
});
});

describe("when the language is set to an unsupported language", () => {
beforeEach(() => {
document.cookie = "agamaLang=de-DE; path=/;";
mockFetchConfigFn.mockResolvedValue({ uiLocale: "de_DE.UTF-8" });
});

it("uses the first supported language from the browser", async () => {
render(
<InstallerClientProvider client={client}>
<InstallerL10nProvider>
<TranslatedContent />
</InstallerL10nProvider>
</InstallerClientProvider>,
);

await waitFor(() => expect(utils.locationReload).toHaveBeenCalled());

// renders again after reloading
render(
<InstallerClientProvider client={client}>
<InstallerL10nProvider>
<TranslatedContent />
</InstallerL10nProvider>
</InstallerClientProvider>,
);

await waitFor(() => screen.getByText("hola"));
expect(mockUpdateConfigFn).toHaveBeenCalledWith({ uiLocale: "es_ES.UTF-8" });
});
});

describe("when the language is not set", () => {
beforeEach(() => {
// Ensure both, UI and backend mock languages, are in sync since
Expand All @@ -159,7 +128,7 @@ describe("InstallerL10nProvider", () => {
mockFetchConfigFn.mockResolvedValue({ uiLocale: "es_ES.UTF-8" });
});

it("sets the preferred language from browser and reloads", async () => {
it("sets the language from backend", async () => {
render(
<InstallerClientProvider client={client}>
<InstallerL10nProvider>
Expand All @@ -180,34 +149,6 @@ describe("InstallerL10nProvider", () => {
);
await waitFor(() => screen.getByText("hola"));
});

describe("when the browser language does not contain the full locale", () => {
beforeEach(() => {
jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["es", "cs-CZ"]);
});

it("sets the first which language matches", async () => {
render(
<InstallerClientProvider client={client}>
<InstallerL10nProvider>
<TranslatedContent />
</InstallerL10nProvider>
</InstallerClientProvider>,
);

await waitFor(() => expect(utils.locationReload).toHaveBeenCalled());

// renders again after reloading
render(
<InstallerClientProvider client={client}>
<InstallerL10nProvider>
<TranslatedContent />
</InstallerL10nProvider>
</InstallerClientProvider>,
);
await waitFor(() => screen.getByText("hola!"));
});
});
});
});

Expand Down
26 changes: 20 additions & 6 deletions web/src/context/installerL10n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ function languageToLocale(language: string): string {
return `${locale}.UTF-8`;
}

/**
* Returns the language tag from the backend.
*
* @return Language tag from the backend locale.
*/
async function languageFromBackend(): Promise<string> {
const config = await fetchConfig();
return languageFromLocale(config.uiLocale);
}

/**
* Returns the first supported language from the given list.
*
Expand Down Expand Up @@ -224,14 +234,19 @@ async function loadTranslations(locale: string) {
*
* @see useInstallerL10n
*/
function InstallerL10nProvider({ children }: { children?: React.ReactNode }) {
function InstallerL10nProvider({
initialLanguage,
children,
}: {
initialLanguage?: string;
children?: React.ReactNode;
}) {
const { connected } = useInstallerClientStatus();
const [language, setLanguage] = useState(undefined);
const [language, setLanguage] = useState(initialLanguage);
const [keymap, setKeymap] = useState(undefined);

const syncBackendLanguage = useCallback(async () => {
const config = await fetchConfig();
const backendLanguage = languageFromLocale(config.uiLocale);
const backendLanguage = await languageFromBackend();
if (backendLanguage === language) return;

// FIXME: fallback to en-US if the language is not supported.
Expand All @@ -240,7 +255,7 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) {

const changeLanguage = useCallback(
async (lang?: string) => {
const wanted = lang || languageFromQuery();
const wanted = lang || languageFromQuery() || (await languageFromBackend());

// Just for development purposes
if (wanted === "xx" || wanted === "xx-XX") {
Expand All @@ -253,7 +268,6 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) {
wanted,
wanted?.split("-")[0], // fallback to the language (e.g., "es" for "es-AR")
agamaLanguage(),
...navigator.languages,
].filter((l) => l);
const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US";
const mustReload = storeAgamaLanguage(newLanguage);
Expand Down
5 changes: 4 additions & 1 deletion web/src/queries/software.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ const useProduct = (
}) as [{ data: string; isPending: boolean }, { data: Product[]; isPending: boolean }];

if (isSelectedPending || isProductsPending) {
return {};
return {
products: [],
selectedProduct: undefined,
};
}

const selectedProduct = products.find((p: Product) => p.id === selected);
Expand Down
2 changes: 1 addition & 1 deletion web/src/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const Providers = ({ children, withL10n }) => {
if (withL10n) {
return (
<InstallerClientProvider client={client}>
<InstallerL10nProvider>{children}</InstallerL10nProvider>
<InstallerL10nProvider initialLanguage="en-US">{children}</InstallerL10nProvider>
</InstallerClientProvider>
);
}
Expand Down