+));
describe("when a product is selected", () => {
it("renders the overview page content", async () => {
- plainRender();
+ installerRender();
await screen.findByText("Localization Section");
await screen.findByText("Storage Section");
await screen.findByText("Software Section");
diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx
index ecbe0c5951..17f0efadc9 100644
--- a/web/src/components/overview/OverviewPage.tsx
+++ b/web/src/components/overview/OverviewPage.tsx
@@ -21,44 +21,35 @@
*/
import React from "react";
-import { Grid, GridItem, Hint, HintBody, Stack } from "@patternfly/react-core";
+import { Grid, GridItem, Stack } from "@patternfly/react-core";
import { Page } from "~/components/core";
import L10nSection from "./L10nSection";
import StorageSection from "./StorageSection";
import SoftwareSection from "./SoftwareSection";
import { _ } from "~/i18n";
-const OverviewSection = () => (
-
-
-
-
-
-
-
-);
-
export default function OverviewPage() {
return (
+
+
{_("Overview")}
+
+
-
-
- {_(
- "Take your time to check your configuration before starting the installation process.",
- )}
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx
index 7c02aae9ca..d070a07093 100644
--- a/web/src/components/overview/StorageSection.tsx
+++ b/web/src/components/overview/StorageSection.tsx
@@ -45,7 +45,6 @@ const Content = ({ children }) => (
export default function StorageSection() {
const configModel = useConfigModel();
const devices = useDevices("system", { suspense: true });
-
const drives = configModel?.drives || [];
const label = (drive) => {
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/product/ProductRegistrationAlert.test.tsx b/web/src/components/product/ProductRegistrationAlert.test.tsx
new file mode 100644
index 0000000000..856e47737a
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationAlert.test.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 { screen } from "@testing-library/react";
+import { installerRender, mockRoutes } from "~/test-utils";
+import ProductRegistrationAlert from "./ProductRegistrationAlert";
+import { Product, RegistrationInfo } from "~/types/software";
+import { useProduct, useRegistration } from "~/queries/software";
+import { PRODUCT, REGISTRATION, ROOT, USER } from "~/routes/paths";
+
+jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
);
+
+const tw: Product = {
+ id: "Tumbleweed",
+ name: "openSUSE Tumbleweed",
+ registration: "no",
+};
+
+const sle: Product = {
+ id: "sle",
+ name: "SLE",
+ registration: "mandatory",
+};
+
+let selectedProduct: Product;
+let registrationInfoMock: RegistrationInfo;
+
+jest.mock("~/queries/software", () => ({
+ ...jest.requireActual("~/queries/software"),
+ useRegistration: (): ReturnType => registrationInfoMock,
+ useProduct: (): ReturnType => {
+ return {
+ products: [tw, sle],
+ selectedProduct,
+ };
+ },
+}));
+
+const rendersNothingInSomePaths = () => {
+ describe.each([
+ ["login", ROOT.login],
+ ["product selection", PRODUCT.changeProduct],
+ ["product selection progress", PRODUCT.progress],
+ ["installation progress", ROOT.installationProgress],
+ ["installation finished", ROOT.installationFinished],
+ ["root authentication", USER.rootUser.edit],
+ ])(`but at %s path`, (_, path) => {
+ beforeEach(() => {
+ mockRoutes(path);
+ });
+
+ it("renders nothing", () => {
+ const { container } = installerRender();
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+};
+
+describe("ProductRegistrationAlert", () => {
+ describe("when product is registrable and registration code is not set", () => {
+ beforeEach(() => {
+ selectedProduct = sle;
+ registrationInfoMock = { key: "", email: "" };
+ });
+
+ rendersNothingInSomePaths();
+
+ it("renders an alert warning about registration required", () => {
+ installerRender();
+ screen.getByRole("heading", {
+ name: /Warning alert:.*must be registered/,
+ });
+ const link = screen.getByRole("link", { name: "Register it now" });
+ expect(link).toHaveAttribute("href", REGISTRATION.root);
+ });
+
+ describe("but at registration path already", () => {
+ beforeEach(() => {
+ mockRoutes(REGISTRATION.root);
+ });
+
+ it("does not render the link to registration", () => {
+ installerRender();
+ screen.getByRole("heading", {
+ name: /Warning alert:.*must be registered/,
+ });
+ expect(screen.queryAllByRole("link")).toEqual([]);
+ });
+ });
+ });
+
+ describe("when product is registrable and registration code is already set", () => {
+ beforeEach(() => {
+ selectedProduct = sle;
+ registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" };
+ });
+
+ it("renders nothing", () => {
+ const { container } = installerRender();
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+
+ describe("when product is not registrable", () => {
+ beforeEach(() => {
+ selectedProduct = tw;
+ });
+
+ it("renders nothing", () => {
+ const { container } = installerRender();
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+});
diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx
new file mode 100644
index 0000000000..4693a969db
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationAlert.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) [2023-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 { Alert } from "@patternfly/react-core";
+import { useLocation } from "react-router-dom";
+import { Link } from "~/components/core";
+import { useProduct, useRegistration } from "~/queries/software";
+import { REGISTRATION, SUPPORTIVE_PATHS } from "~/routes/paths";
+import { isEmpty } from "~/utils";
+import { _ } from "~/i18n";
+import { sprintf } from "sprintf-js";
+
+const LinkToRegistration = () => {
+ const location = useLocation();
+
+ if (location.pathname === REGISTRATION.root) return;
+
+ return (
+
+ {_("Register it now")}
+
+ );
+};
+
+export default function ProductRegistrationAlert() {
+ const location = useLocation();
+ const { selectedProduct: product } = useProduct();
+ const registration = useRegistration();
+
+ // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering
+ // if so just in case.
+ if (SUPPORTIVE_PATHS.includes(location.pathname)) return;
+ if (["no", undefined].includes(product.registration) || !isEmpty(registration.key)) return;
+
+ return (
+
+
+
+ );
+}
diff --git a/web/src/components/product/ProductRegistrationPage.test.tsx b/web/src/components/product/ProductRegistrationPage.test.tsx
new file mode 100644
index 0000000000..c83d411d35
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationPage.test.tsx
@@ -0,0 +1,166 @@
+/*
+ * 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 { screen } from "@testing-library/react";
+import { installerRender } from "~/test-utils";
+import ProductRegistrationPage from "./ProductRegistrationPage";
+import { Product, RegistrationInfo } from "~/types/software";
+import { useProduct, useRegistration } from "~/queries/software";
+
+const tw: Product = {
+ id: "Tumbleweed",
+ name: "openSUSE Tumbleweed",
+ registration: "no",
+};
+
+const sle: Product = {
+ id: "sle",
+ name: "SLE",
+ registration: "mandatory",
+};
+
+let selectedProduct: Product;
+let registrationInfoMock: RegistrationInfo;
+const registerMutationMock = jest.fn();
+
+jest.mock("~/queries/software", () => ({
+ ...jest.requireActual("~/queries/software"),
+ useRegisterMutation: () => ({ mutate: registerMutationMock }),
+ useRegistration: (): ReturnType => registrationInfoMock,
+ useProduct: (): ReturnType => {
+ return {
+ products: [tw, sle],
+ selectedProduct,
+ };
+ },
+}));
+
+describe("ProductRegistrationPage", () => {
+ describe("when selected product is not registrable", () => {
+ beforeEach(() => {
+ selectedProduct = tw;
+ registrationInfoMock = { key: "", email: "" };
+ });
+
+ it("renders nothing", () => {
+ const { container } = installerRender();
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+
+ describe("when selected product is registrable and registration code is not set", () => {
+ beforeEach(() => {
+ selectedProduct = sle;
+ registrationInfoMock = { key: "", email: "" };
+ });
+
+ it("renders ProductRegistrationAlert component", () => {
+ installerRender();
+ screen.getByText("Warning alert:");
+ });
+
+ it("renders a form to allow user registering the product", async () => {
+ const { user } = installerRender();
+ const registrationCodeInput = screen.getByLabelText("Registration code");
+ const emailInput = screen.getByRole("textbox", { name: /Email/ });
+ const submitButton = screen.getByRole("button", { name: "Register" });
+
+ await user.type(registrationCodeInput, "INTERNAL-USE-ONLY-1234-5678");
+ await user.type(emailInput, "example@company.test");
+ await user.click(submitButton);
+
+ expect(registerMutationMock).toHaveBeenCalledWith(
+ {
+ email: "example@company.test",
+ key: "INTERNAL-USE-ONLY-1234-5678",
+ },
+ expect.anything(),
+ );
+ });
+
+ it.todo("handles and renders errors from server, if any");
+ });
+
+ describe("when selected product is registrable and registration code is set", () => {
+ beforeEach(() => {
+ selectedProduct = sle;
+ registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "example@company.test" };
+ });
+
+ it("does not render ProductRegistrationAlert component", () => {
+ installerRender();
+ expect(screen.queryByText("Warning alert:")).toBeNull();
+ });
+
+ it("renders registration information with code partially hidden", async () => {
+ const { user } = installerRender();
+ const visibilityCodeToggler = screen.getByRole("button", { name: "Show" });
+ screen.getByText(/\*?5678/);
+ expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull();
+ expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull();
+ screen.getByText("example@company.test");
+ await user.click(visibilityCodeToggler);
+ screen.getByText("INTERNAL-USE-ONLY-1234-5678");
+ await user.click(visibilityCodeToggler);
+ expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull();
+ screen.getByText(/\*?5678/);
+ });
+
+ // describe("but at registration path already", () => {
+ // beforeEach(() => {
+ // mockRoutes(REGISTRATION.root);
+ // });
+ //
+ // it("does not render the link to registration", () => {
+ // installerRender();
+ // screen.getByRole("heading", {
+ // name: /Warning alert:.*must be registered/,
+ // });
+ // expect(screen.queryAllByRole("link")).toEqual([]);
+ // });
+ // });
+ // });
+ //
+ // describe("when product is registrable and registration code is already set", () => {
+ // beforeEach(() => {
+ // selectedProduct = sle;
+ // registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" };
+ // });
+ //
+ // it("renders nothing", () => {
+ // const { container } = installerRender();
+ // expect(container).toBeEmptyDOMElement();
+ // });
+ // });
+ //
+ // describe("when product is not registrable", () => {
+ // beforeEach(() => {
+ // selectedProduct = tw;
+ // });
+ //
+ // it("renders nothing", () => {
+ // const { container } = installerRender();
+ // expect(container).toBeEmptyDOMElement();
+ // });
+ });
+});
diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx
new file mode 100644
index 0000000000..2aa23f4f8e
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationPage.tsx
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) [2023-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, { useState } from "react";
+import {
+ ActionGroup,
+ Alert,
+ Button,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Flex,
+ Form,
+ FormGroup,
+ Grid,
+ Stack,
+ TextInput,
+} from "@patternfly/react-core";
+import { Page, PasswordInput } from "~/components/core";
+import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
+import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing";
+import { useProduct, useRegistration, useRegisterMutation } from "~/queries/software";
+import { isEmpty, mask } from "~/utils";
+import { _ } from "~/i18n";
+import { sprintf } from "sprintf-js";
+
+const FORM_ID = "productRegistration";
+const KEY_LABEL = _("Registration code");
+const EMAIL_LABEL = "Email";
+
+const RegisteredProductSection = () => {
+ const { selectedProduct: product } = useProduct();
+ const registration = useRegistration();
+ const [showCode, setShowCode] = useState(false);
+ const toggleCodeVisibility = () => setShowCode(!showCode);
+
+ return (
+
+
+
+ {KEY_LABEL}
+
+
+ {showCode ? registration.key : mask(registration.key)}
+
+
+
+ {!isEmpty(registration.email) && (
+ <>
+ {EMAIL_LABEL}
+ {registration.email}
+ >
+ )}
+
+
+
+ );
+};
+
+const RegistrationFormSection = () => {
+ const { mutate: register } = useRegisterMutation();
+ const [key, setKey] = useState("");
+ const [email, setEmail] = useState("");
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ // FIXME: use the right type for AxiosResponse
+ const onRegisterError = ({ response }) => {
+ const originalMessage = response.data.message;
+ const from = originalMessage.indexOf(":") + 1;
+ setError(originalMessage.slice(from).trim());
+ };
+
+ const submit = async (e: React.SyntheticEvent) => {
+ e.preventDefault();
+ setError(null);
+ setLoading(true);
+ // @ts-ignore
+ register({ key, email }, { onError: onRegisterError, onSettled: () => setLoading(false) });
+ };
+
+ // TODO: adjust texts based of registration "type", mandatory or optional
+
+ return (
+
+
+
+ );
+};
+
+export default function ProductRegistrationPage() {
+ const { selectedProduct: product } = useProduct();
+ const registration = useRegistration();
+
+ // TODO: render something meaningful instead? "Product not registrable"?
+ if (product.registration === "no") return;
+
+ return (
+
+
+
{_("Registration")}
+
+
+
+
+
+ {isEmpty(registration.key) ? : }
+
+
+
+
+ );
+}
diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx
index 4209e2da58..da2b4d9d67 100644
--- a/web/src/components/product/ProductSelectionPage.test.tsx
+++ b/web/src/components/product/ProductSelectionPage.test.tsx
@@ -24,15 +24,21 @@ import React from "react";
import { screen } from "@testing-library/react";
import { installerRender, mockNavigateFn } from "~/test-utils";
import { ProductSelectionPage } from "~/components/product";
-import { Product } from "~/types/software";
-import { useProduct } from "~/queries/software";
+import { Product, RegistrationInfo } from "~/types/software";
+import { useProduct, useRegistration } from "~/queries/software";
+
+jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
+
ProductRegistrationAlert Mock
+));
const mockConfigMutation = jest.fn();
+
const tumbleweed: Product = {
id: "Tumbleweed",
name: "openSUSE Tumbleweed",
icon: "tumbleweed.svg",
description: "Tumbleweed description...",
+ registration: "no",
};
const microOs: Product = {
@@ -40,39 +46,80 @@ const microOs: Product = {
name: "openSUSE MicroOS",
icon: "microos.svg",
description: "MicroOS description",
+ registration: "no",
};
+let mockSelectedProduct: Product;
+let registrationInfoMock: RegistrationInfo;
+
jest.mock("~/queries/software", () => ({
...jest.requireActual("~/queries/software"),
useProduct: (): ReturnType => {
return {
products: [tumbleweed, microOs],
- selectedProduct: tumbleweed,
+ selectedProduct: mockSelectedProduct,
};
},
useProductChanges: () => jest.fn(),
useConfigMutation: () => ({ mutate: mockConfigMutation }),
+ useRegistration: (): ReturnType => registrationInfoMock,
}));
-describe("when the user chooses a product and hits the confirmation button", () => {
- it("triggers the product selection", async () => {
- const { user } = installerRender();
- const productOption = screen.getByRole("radio", { name: microOs.name });
- const selectButton = screen.getByRole("button", { name: "Select" });
- await user.click(productOption);
- await user.click(selectButton);
- expect(mockConfigMutation).toHaveBeenCalledWith({ product: microOs.id });
+describe("ProductSelectionPage", () => {
+ beforeEach(() => {
+ mockSelectedProduct = tumbleweed;
+ registrationInfoMock = { key: "", email: "" };
+ });
+
+ describe("when there is a registration code set", () => {
+ beforeEach(() => {
+ registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" };
+ });
+
+ it("navigates to root path", async () => {
+ installerRender();
+ await screen.findByText("Navigating to /");
+ });
+ });
+
+ describe("when there is a product already selected", () => {
+ it("renders the Cancel button", () => {
+ installerRender();
+ screen.getByRole("button", { name: "Cancel" });
+ });
+ });
+
+ describe("when there is not a product selected yet", () => {
+ beforeEach(() => {
+ mockSelectedProduct = undefined;
+ });
+
+ it("does not render the Cancel button", () => {
+ installerRender();
+ expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
+ });
+ });
+
+ describe("when the user chooses a product and hits the confirmation button", () => {
+ it("triggers the product selection", async () => {
+ const { user } = installerRender();
+ const productOption = screen.getByRole("radio", { name: microOs.name });
+ const selectButton = screen.getByRole("button", { name: "Select" });
+ await user.click(productOption);
+ await user.click(selectButton);
+ expect(mockConfigMutation).toHaveBeenCalledWith({ product: microOs.id });
+ });
});
-});
-describe("when the user chooses a product but hits the cancel button", () => {
- it("does not trigger the product selection and goes back", async () => {
- const { user } = installerRender();
- const productOption = screen.getByRole("radio", { name: microOs.name });
- const cancelButton = screen.getByRole("button", { name: "Cancel" });
- await user.click(productOption);
- await user.click(cancelButton);
- expect(mockConfigMutation).not.toHaveBeenCalled();
- expect(mockNavigateFn).toHaveBeenCalledWith("/");
+ describe("when the user chooses a product but hits the cancel button", () => {
+ it("does not trigger the product selection and goes back", async () => {
+ const { user } = installerRender();
+ const productOption = screen.getByRole("radio", { name: microOs.name });
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ await user.click(productOption);
+ await user.click(cancelButton);
+ expect(mockConfigMutation).not.toHaveBeenCalled();
+ expect(mockNavigateFn).toHaveBeenCalledWith("/");
+ });
});
});
diff --git a/web/src/components/product/ProductSelectionPage.tsx b/web/src/components/product/ProductSelectionPage.tsx
index c24135c712..994f4ac269 100644
--- a/web/src/components/product/ProductSelectionPage.tsx
+++ b/web/src/components/product/ProductSelectionPage.tsx
@@ -34,15 +34,16 @@ import {
FormGroup,
Button,
} from "@patternfly/react-core";
+import { Navigate, useNavigate } from "react-router-dom";
import { Page } from "~/components/core";
import { Center } from "~/components/layout";
-import { useConfigMutation, useProduct } from "~/queries/software";
+import { useConfigMutation, useProduct, useRegistration } from "~/queries/software";
import pfTextStyles from "@patternfly/react-styles/css/utilities/Text/text";
import pfRadioStyles from "@patternfly/react-styles/css/components/Radio/radio";
-import { slugify } from "~/utils";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
-import { useNavigate } from "react-router-dom";
+import { PATHS } from "~/router";
+import { isEmpty } from "~/utils";
const ResponsiveGridItem = ({ children }) => (
@@ -51,8 +52,7 @@ const ResponsiveGridItem = ({ children }) => (
);
const Option = ({ product, isChecked, onChange }) => {
- const id = slugify(product.name);
- const detailsId = `${id}-details`;
+ const detailsId = `${product.id}-details`;
const logoSrc = `assets/logos/${product.icon}`;
// TRANSLATORS: %s will be replaced by a product name. E.g., "openSUSE Tumbleweed"
const logoAltText = sprintf(_("%s logo"), product.name);
@@ -63,7 +63,7 @@ const Option = ({ product, isChecked, onChange }) => {
{
+
+
+ {_("Back to device selection")}
+
+
);
}
diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx
index 68e6d235eb..f16f32d8fc 100644
--- a/web/src/components/storage/ProposalPage.test.tsx
+++ b/web/src/components/storage/ProposalPage.test.tsx
@@ -27,7 +27,7 @@
*/
import React from "react";
import { screen } from "@testing-library/react";
-import { plainRender } from "~/test-utils";
+import { installerRender } from "~/test-utils";
import { ProposalPage } from "~/components/storage";
import {
ProposalResult,
@@ -140,6 +140,6 @@ jest.mock("~/queries/storage", () => ({
}));
it("renders the device, settings and result sections", () => {
- plainRender();
+ installerRender();
screen.findByText("Device");
});
diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx
index 2a7b8023c3..194420ed50 100644
--- a/web/src/components/storage/ProposalPage.tsx
+++ b/web/src/components/storage/ProposalPage.tsx
@@ -20,21 +20,22 @@
* find current contact information at www.suse.com.
*/
-import React from "react";
+import React, { useEffect, useState } from "react";
import { Grid, GridItem, SplitItem } from "@patternfly/react-core";
+import { useQueryClient } from "@tanstack/react-query";
import { Page } from "~/components/core/";
+import { Loading } from "~/components/layout";
+import EncryptionField from "~/components/storage/EncryptionField";
import ProposalResultSection from "./ProposalResultSection";
import ProposalTransactionalInfo from "./ProposalTransactionalInfo";
import ConfigEditor from "./ConfigEditor";
import ConfigEditorMenu from "./ConfigEditorMenu";
-import EncryptionField from "~/components/storage/EncryptionField";
-import { _ } from "~/i18n";
import { toValidationError } from "~/utils";
import { useIssues } from "~/queries/issues";
import { IssueSeverity } from "~/types/issues";
-import { useDeprecated, useDevices, useProposalResult } from "~/queries/storage";
-import { useQueryClient } from "@tanstack/react-query";
+import { useDevices, useProposalResult, useDeprecated, useRefresh } from "~/queries/storage";
import { refresh } from "~/api/storage";
+import { _ } from "~/i18n";
/**
* Which UI item is being changed by user
@@ -60,13 +61,14 @@ export const NOT_AFFECTED = {
};
export default function ProposalPage() {
+ const [isLoading, setIsLoading] = useState(false);
const systemDevices = useDevices("system");
const stagingDevices = useDevices("result");
const { actions } = useProposalResult();
const deprecated = useDeprecated();
const queryClient = useQueryClient();
- React.useEffect(() => {
+ useEffect(() => {
if (deprecated) {
refresh().then(() => {
queryClient.invalidateQueries({ queryKey: ["storage"] });
@@ -74,10 +76,29 @@ export default function ProposalPage() {
}
}, [deprecated, queryClient]);
+ useRefresh({
+ onStart: () => setIsLoading(true),
+ onFinish: () => setIsLoading(false),
+ });
+
const errors = useIssues("storage")
.filter((s) => s.severity === IssueSeverity.Error)
.map(toValidationError);
+ if (isLoading) {
+ return (
+
+
+
{_("Storage")}
+
+
+
+
+
+
+ );
+ }
+
return (
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
{
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();
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/RootAuthMethodsPage.test.tsx b/web/src/components/users/RootAuthMethodsPage.test.tsx
index 480c097668..153dc8b574 100644
--- a/web/src/components/users/RootAuthMethodsPage.test.tsx
+++ b/web/src/components/users/RootAuthMethodsPage.test.tsx
@@ -27,6 +27,10 @@ import { RootAuthMethodsPage } from "~/components/users";
const mockRootUserMutation = { mutateAsync: jest.fn() };
+jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
+
ProductRegistrationAlert Mock
+));
+
jest.mock("~/queries/users", () => ({
...jest.requireActual("~/queries/users"),
useRootUserMutation: () => mockRootUserMutation,
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
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];
};
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}
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..d6418c0979 100644
--- a/web/src/hooks/useNodeSiblings.js
+++ b/web/src/hooks/useNodeSiblings.ts
@@ -1,19 +1,7 @@
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
- */
+type AddAttributeFn = HTMLElement["setAttribute"];
+type RemoveAttributeFn = HTMLElement["removeAttribute"];
/**
* A hook for working with siblings of the node passed as parameter
@@ -21,22 +9,19 @@ import { noop } from "~/utils";
* 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): [AddAttributeFn, RemoveAttributeFn] => {
if (!node) return [noop, noop];
const siblings = [...node.parentNode.children].filter((n) => n !== node);
- const addAttribute = (attribute, value) => {
+ const addAttribute: AddAttributeFn = (attribute, value) => {
siblings.forEach((sibling) => {
sibling.setAttribute(attribute, value);
});
};
- const removeAttribute = (attribute) => {
+ const removeAttribute: RemoveAttributeFn = (attribute: string) => {
siblings.forEach((sibling) => {
sibling.removeAttribute(attribute);
});
diff --git a/web/src/i18n.test.js b/web/src/i18n.test.ts
similarity index 85%
rename from web/src/i18n.test.js
rename to web/src/i18n.test.ts
index 483e8ae0fc..c3caef75e2 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.
*
@@ -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);
});
});
diff --git a/web/src/i18n.js b/web/src/i18n.ts
similarity index 77%
rename from web/src/i18n.js
rename to web/src/i18n.ts
index 3b88d7ac5f..4cd13049c0 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,23 +118,23 @@ 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);
/**
* Wrapper around Intl.ListFormat to get a language-specific representation of the given list of
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/src/languages.json b/web/src/languages.json
index 11b8afb8a3..3bb8b12376 100644
--- a/web/src/languages.json
+++ b/web/src/languages.json
@@ -5,6 +5,7 @@
"en-US": "English",
"es-ES": "Español",
"fr-FR": "Français",
+ "id-ID": "Indonesia",
"ja-JP": "日本語",
"nb-NO": "Norsk bokmål",
"pt-BR": "Português",
diff --git a/web/src/po/po.ca.js b/web/src/po/po.ca.js
index cc98043e22..34f7a351be 100644
--- a/web/src/po/po.ca.js
+++ b/web/src/po/po.ca.js
@@ -34,6 +34,9 @@ export default {
"%s disk": [
"Disc %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s és un sistema immutable amb actualitzacions atòmiques. Usa un sistema de fitxers Btrfs només de lectura actualitzat a través d'instantànies."
],
@@ -43,6 +46,9 @@ export default {
"%s with %d partitions": [
"%s amb %d particions"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -455,6 +461,9 @@ export default {
"Encryption Password": [
"Contrasenya d'encriptació"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Mida exacta"
],
@@ -542,6 +551,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Amaga %d acció de subvolum",
"Amaga %d accions de subvolum"
@@ -996,6 +1008,15 @@ export default {
"Reboot": [
"Reinicia"
],
+ "Register": [
+ "Registra"
+ ],
+ "Registration": [
+ "Registre"
+ ],
+ "Registration code": [
+ "Codi de registre"
+ ],
"Reload": [
"Torna a carregar"
],
@@ -1174,9 +1195,6 @@ export default {
"Table with mount points": [
"Taula amb punts de muntatge"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Dediqueu el temps que calgui a comprovar la configuració abans de començar el procés d'instal·lació."
- ],
"Target Password": [
"Contrasenya de destinació"
],
@@ -1267,6 +1285,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"El sistema encara no s'ha configurat per connectar-se a una xarxa de wifi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"El sistema usarà el %s com a llengua per defecte."
],
@@ -1390,9 +1411,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Escrivint"
- ],
"Waiting for actions information...": [
"Esperant la informació de les accions..."
],
diff --git a/web/src/po/po.cs.js b/web/src/po/po.cs.js
index dc9cb32ecd..22189c6cd7 100644
--- a/web/src/po/po.cs.js
+++ b/web/src/po/po.cs.js
@@ -35,15 +35,24 @@ export default {
"%s disk": [
"%s disk"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s je neměnný systém s atomickými aktualizacemi. Používá souborový systém Btrfs pouze pro čtení aktualizovaný pomocí snímků."
],
"%s logo": [
"%s logo"
],
+ "%s must be registered.": [
+ ""
+ ],
"%s with %d partitions": [
"%s s %d oddíly"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -457,6 +466,9 @@ export default {
"Encryption Password": [
"Heslo pro šifrování"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Přesná velikost"
],
@@ -544,6 +556,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Skrýt %d akci podsvazku",
"Skrýt %d akce podsvazku",
@@ -618,6 +633,9 @@ export default {
"Install new system on": [
"Instalace nového systému na"
],
+ "Install using an advanced configuration.": [
+ ""
+ ],
"Install using device %s and deleting all its content": [
"Instalace pomocí zařízení %s a odstranění veškerého jeho obsahu"
],
@@ -978,6 +996,9 @@ export default {
"Presence of other volumes (%s)": [
"Přítomnost dalších svazků (%s)"
],
+ "Product registered": [
+ ""
+ ],
"Protection for the information stored at the device, including data, programs, and system files.": [
"Ochrana informací uložených v zařízení, včetně dat, programů a systémových souborů."
],
@@ -993,6 +1014,9 @@ export default {
"Reboot": [
"Restartovat systém"
],
+ "Registration code": [
+ "Registrační kód"
+ ],
"Reload": [
"Znovu načíst"
],
@@ -1095,6 +1119,9 @@ export default {
"Set root SSH public key": [
"Nastavte veřejný klíč SSH pro roota"
],
+ "Show": [
+ ""
+ ],
"Show %d subvolume action": [
"Zobrazit %d akci podsvazku",
"Zobrazit %d akce podsvazku",
@@ -1169,9 +1196,6 @@ export default {
"Table with mount points": [
"Tabulka s přípojnými body"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Před zahájením instalace zkontrolujte konfiguraci."
- ],
"Target Password": [
"Cílové heslo"
],
@@ -1262,6 +1286,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Systém zatím nebyl konfigurován pro připojení k síti Wi-Fi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Systém použije jako výchozí jazyk %s."
],
@@ -1385,9 +1412,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Čekám"
- ],
"Waiting for actions information...": [
"Čekáme na informace o akcích..."
],
diff --git a/web/src/po/po.de.js b/web/src/po/po.de.js
index 48f3518623..2b9d0862ac 100644
--- a/web/src/po/po.de.js
+++ b/web/src/po/po.de.js
@@ -22,6 +22,9 @@ export default {
"%s disk": [
"Festplatte %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s ist ein unveränderliches System mit atomaren Aktualisierungen. Es verwendet ein schreibgeschütztes Btrfs-Dateisystem, das über Schnappschüsse aktualisiert wird."
],
@@ -31,6 +34,9 @@ export default {
"%s with %d partitions": [
"%s mit %d Partitionen"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -422,6 +428,9 @@ export default {
"Encryption Password": [
"Verschlüsselungspasswort"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Exakte Größe"
],
@@ -503,6 +512,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"",
""
@@ -933,6 +945,15 @@ export default {
"Reboot": [
"Neustart"
],
+ "Register": [
+ "Registrieren"
+ ],
+ "Registration": [
+ "Registrierung"
+ ],
+ "Registration code": [
+ "Registrierungscode"
+ ],
"Reload": [
"Neu laden"
],
@@ -1105,9 +1126,6 @@ export default {
"Table with mount points": [
"Tabelle mit Einhängepunkten"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Nehmen Sie sich die Zeit, Ihre Konfiguration zu überprüfen, bevor Sie mit der Installation beginnen."
- ],
"Target Password": [
"Ziel-Passwort"
],
@@ -1186,6 +1204,9 @@ export default {
"The size of the file system cannot be edited": [
"Die Größe des Dateisystems kann nicht bearbeitet werden"
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Das System wird %s als Standardsprache verwenden."
],
@@ -1297,9 +1318,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Warten"
- ],
"Waiting for actions information...": [
"Warten auf Informationen zu Aktionen ..."
],
diff --git a/web/src/po/po.es.js b/web/src/po/po.es.js
index 25542512e7..095fdbfdee 100644
--- a/web/src/po/po.es.js
+++ b/web/src/po/po.es.js
@@ -34,6 +34,9 @@ export default {
"%s disk": [
"disco %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s es un sistema inmutable con actualizaciones atómicas. Utiliza un sistema de archivos Btrfs de solo lectura actualizado mediante instantáneas."
],
@@ -43,6 +46,9 @@ export default {
"%s with %d partitions": [
"%s con %d particiones"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -455,6 +461,9 @@ export default {
"Encryption Password": [
"Contraseña de cifrado"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Tamaño exacto"
],
@@ -542,6 +551,9 @@ export default {
"GiB": [
"GB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Ocultar %d acción de subvolumen",
"Ocultar %d acciones de subvolumen"
@@ -996,6 +1008,15 @@ export default {
"Reboot": [
"Reiniciar"
],
+ "Register": [
+ "Registrar"
+ ],
+ "Registration": [
+ "Registro"
+ ],
+ "Registration code": [
+ "Código de registro"
+ ],
"Reload": [
"Recargar"
],
@@ -1174,9 +1195,6 @@ export default {
"Table with mount points": [
"Tabla con puntos de montaje"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Dedica un tiempo para verificar la configuración antes de iniciar el proceso de instalación."
- ],
"Target Password": [
"Contraseña de destino"
],
@@ -1267,6 +1285,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"El sistema aún no se ha configurado para conectarse a una red WiFi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"El sistema utilizará %s como su idioma predeterminado."
],
@@ -1390,9 +1411,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Esperar"
- ],
"Waiting for actions information...": [
"Esperando información de acciones..."
],
diff --git a/web/src/po/po.fr.js b/web/src/po/po.fr.js
index 8ba05207d9..f54d2f710f 100644
--- a/web/src/po/po.fr.js
+++ b/web/src/po/po.fr.js
@@ -25,6 +25,9 @@ export default {
"%s disk": [
"Disque %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s est un système immuable avec des mises à jour atomiques. Il utilise un système de fichiers Btrfs en lecture seule mis à jour via des clichés."
],
@@ -34,6 +37,9 @@ export default {
"%s with %d partitions": [
"%s avec %d partitions"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -376,6 +382,9 @@ export default {
"Encryption Password": [
"Mot de passe de chiffrement"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Taille exacte"
],
@@ -454,6 +463,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Masquer l'action du sous-volume %d",
"Masquer les actions du sous-volume %d"
@@ -863,6 +875,15 @@ export default {
"Reboot": [
"Redémarrer"
],
+ "Register": [
+ "Enregistrer"
+ ],
+ "Registration": [
+ "Enregistrement"
+ ],
+ "Registration code": [
+ "Code d'inscription"
+ ],
"Reload": [
"Recharger"
],
@@ -1014,9 +1035,6 @@ export default {
"Table with mount points": [
"Table avec points de montage"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Prenez le temps de vérifier votre configuration avant de lancer le processus d'installation."
- ],
"Target Password": [
"Mot de passe cible"
],
@@ -1086,6 +1104,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Le système n'a pas encore été configuré pour se connecter à un réseau Wi-Fi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Le système utilisera %s comme langue par défaut."
],
@@ -1188,9 +1209,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "En attente"
- ],
"Wi-Fi": [
"Wi-Fi"
],
diff --git a/web/src/po/po.id.js b/web/src/po/po.id.js
new file mode 100644
index 0000000000..95c12a2777
--- /dev/null
+++ b/web/src/po/po.id.js
@@ -0,0 +1,1503 @@
+export default {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "id"
+ },
+ " Timezone selection": [
+ " Pemilihan zona waktu"
+ ],
+ " and ": [
+ " dan "
+ ],
+ "%1$s %2$s at %3$s (%4$s)": [
+ "%1$s %2$s pada %3$s (%4$s)"
+ ],
+ "%1$s %2$s partition (%3$s)": [
+ "Partisi %2$s %1$s (%3$s)"
+ ],
+ "%1$s %2$s volume (%3$s)": [
+ "Volume %2$s %1$s (%3$s)"
+ ],
+ "%1$s root at %2$s (%3$s)": [
+ "Root %1$s pada %2$s (%3$s)"
+ ],
+ "%1$s root partition (%2$s)": [
+ "Partisi root %1$s (%2$s)"
+ ],
+ "%1$s root volume (%2$s)": [
+ "Volume root %1$s (%2$s)"
+ ],
+ "%d partition will be shrunk": [
+ "Partisi %d akan menyusut"
+ ],
+ "%s disk": [
+ "%s diska"
+ ],
+ "%s has been registered with below information.": [
+ ""
+ ],
+ "%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
+ "%s adalah sistem yang tidak dapat diubah (immutable) dan mendukung pembaruan atomik. Sistem ini menggunakan file system Btrfs yang hanya-baca dan diperbarui melalui snapshot."
+ ],
+ "%s logo": [
+ "logo %s"
+ ],
+ "%s with %d partitions": [
+ "%s dengan partisi %d"
+ ],
+ "(optional)": [
+ ""
+ ],
+ ", ": [
+ ", "
+ ],
+ "A mount point is required": [
+ "Diperlukan mount point"
+ ],
+ "A new LVM Volume Group": [
+ "Grup Volume LVM baru"
+ ],
+ "A new volume group will be allocated in the selected disk and the file system will be created as a logical volume.": [
+ "Grup volume baru akan dialokasikan di disk yang dipilih dan sistem file akan dibuat sebagai volume logis."
+ ],
+ "A size value is required": [
+ "Diperlukan nilai ukuran"
+ ],
+ "Accept": [
+ "Terima"
+ ],
+ "Action": [
+ "Tindakan"
+ ],
+ "Actions": [
+ "Tindakan"
+ ],
+ "Actions for connection %s": [
+ "Tindakan untuk koneksi %s"
+ ],
+ "Actions to find space": [
+ "Tindakan untuk mencari ruang"
+ ],
+ "Activate": [
+ "Mengaktifkan"
+ ],
+ "Activate disks": [
+ "Mengaktifkan disk"
+ ],
+ "Activate new disk": [
+ "Aktifkan disk baru"
+ ],
+ "Activate zFCP disk": [
+ "Mengaktifkan disk zFCP"
+ ],
+ "Activated": [
+ "Diaktifkan"
+ ],
+ "Add %s file system": [
+ "Tambahkan sistem file %s"
+ ],
+ "Add DNS": [
+ "Tambahkan DNS"
+ ],
+ "Add a SSH Public Key for root": [
+ "Tambahkan Kunci Publik SSH untuk root"
+ ],
+ "Add an address": [
+ "Tambahkan alamat"
+ ],
+ "Add another DNS": [
+ "Tambahkan DNS lain"
+ ],
+ "Add another address": [
+ "Tambahkan alamat lain"
+ ],
+ "Add file system": [
+ "Tambahkan sistem berkas"
+ ],
+ "Address": [
+ "Alamat"
+ ],
+ "Addresses": [
+ "Alamat"
+ ],
+ "Addresses data list": [
+ "Daftar data alamat"
+ ],
+ "All fields are required": [
+ "Semua bidang wajib diisi"
+ ],
+ "All partitions will be removed and any data in the disks will be lost.": [
+ "Semua partisi akan dihapus dan semua data dalam disk akan hilang."
+ ],
+ "Allows to boot to a previous version of the system after configuration changes or software upgrades.": [
+ "Memungkinkan untuk melakukan booting ke versi sistem sebelumnya setelah perubahan konfigurasi atau peningkatan perangkat lunak."
+ ],
+ "Already set": [
+ "Sudah ditetapkan"
+ ],
+ "An existing disk": [
+ "Disk yang ada"
+ ],
+ "At least one address must be provided for selected mode": [
+ "Setidaknya satu alamat harus disediakan untuk mode yang dipilih"
+ ],
+ "At this point you can power off the machine.": [
+ "Pada titik ini, Anda dapat mematikan mesin."
+ ],
+ "At this point you can reboot the machine to log in to the new system.": [
+ "Pada titik ini, Anda dapat menyalakan ulang mesin untuk masuk ke sistem yang baru."
+ ],
+ "Authentication by initiator": [
+ "Otentikasi oleh inisiator"
+ ],
+ "Authentication by target": [
+ "Otentikasi berdasarkan target"
+ ],
+ "Authentication failed, please try again": [
+ "Otentikasi gagal, coba lagi"
+ ],
+ "Auto": [
+ "Otomatis"
+ ],
+ "Auto LUNs Scan": [
+ "Pemindaian LUN Otomatis"
+ ],
+ "Auto-login": [
+ "Masuk otomatis"
+ ],
+ "Automatic": [
+ "Otomatis"
+ ],
+ "Automatic (DHCP)": [
+ "Otomatis (DHCP)"
+ ],
+ "Automatic LUN scan is [disabled]. LUNs have to be manually configured after activating a controller.": [
+ "Pemindaian LUN otomatis [dinonaktifkan]. LUN harus dikonfigurasi secara manual setelah mengaktifkan pengontrol."
+ ],
+ "Automatic LUN scan is [enabled]. Activating a controller which is running in NPIV mode will automatically configures all its LUNs.": [
+ "Pemindaian LUN otomatis [diaktifkan]. Mengaktifkan pengontrol yang berjalan dalam mode NPIV akan secara otomatis mengonfigurasi semua LUN."
+ ],
+ "Automatically calculated size according to the selected product.": [
+ "Ukuran yang dihitung secara otomatis menurut produk yang dipilih."
+ ],
+ "Available products": [
+ "Produk yang tersedia"
+ ],
+ "Back": [
+ "Kembali"
+ ],
+ "Back to device selection": [
+ "Kembali ke pemilihan perangkat"
+ ],
+ "Before %s": [
+ "Sebelum %s"
+ ],
+ "Before installing, you have to make some decisions. Click on each section to review the settings.": [
+ "Sebelum menginstal, Anda harus membuat beberapa keputusan. Klik pada setiap bagian untuk meninjau pengaturan."
+ ],
+ "Before starting the installation, you need to address the following problems:": [
+ "Sebelum memulai penginstalan, Anda perlu mengatasi masalah berikut ini:"
+ ],
+ "Boot partitions at %s": [
+ "Partisi boot pada %s"
+ ],
+ "Boot partitions at installation disk": [
+ "Partisi boot pada disk instalasi"
+ ],
+ "Btrfs root partition with snapshots (%s)": [
+ "Partisi root btrfs dengan snapshot (%s)"
+ ],
+ "Btrfs root volume with snapshots (%s)": [
+ "Volume root Btrfs dengan snapshot (%s)"
+ ],
+ "Btrfs with snapshots": [
+ "Btrfs dengan snapshot"
+ ],
+ "Cancel": [
+ "Batal"
+ ],
+ "Cannot accommodate the required file systems for installation": [
+ "Tidak dapat mengakomodasi sistem file yang diperlukan untuk instalasi"
+ ],
+ "Cannot be changed in remote installation": [
+ "Tidak dapat diubah dalam instalasi jarak jauh"
+ ],
+ "Cannot connect to Agama server": [
+ "Tidak dapat terhubung ke server Agama"
+ ],
+ "Cannot format all selected devices": [
+ "Tidak dapat memformat semua perangkat yang dipilih"
+ ],
+ "Change": [
+ "Ubah"
+ ],
+ "Change boot options": [
+ "Mengubah opsi boot"
+ ],
+ "Change location": [
+ "Ubah lokasi"
+ ],
+ "Change product": [
+ "Mengubah produk"
+ ],
+ "Change selection": [
+ "Mengubah pilihan"
+ ],
+ "Change the root password": [
+ "Ubah kata sandi root"
+ ],
+ "Channel ID": [
+ "ID saluran"
+ ],
+ "Check the planned action": [
+ "Periksa %d tindakan yang direncanakan"
+ ],
+ "Choose a disk for placing the boot loader": [
+ "Pilih disk untuk menempatkan boot loader"
+ ],
+ "Clear": [
+ "Hapus"
+ ],
+ "Close": [
+ "Tutup"
+ ],
+ "Configuring the product, please wait ...": [
+ "Mengkonfigurasi produk, harap tunggu ..."
+ ],
+ "Confirm": [
+ "Konfirmasi"
+ ],
+ "Confirm Installation": [
+ "Konfirmasi Instalasi"
+ ],
+ "Congratulations!": [
+ "Selamat!"
+ ],
+ "Connect": [
+ "Sambungkan"
+ ],
+ "Connect to a Wi-Fi network": [
+ "Sambungkan ke jaringan Wi-Fi"
+ ],
+ "Connect to hidden network": [
+ "Sambungkan ke jaringan tersembunyi"
+ ],
+ "Connect to iSCSI targets": [
+ "Menyambungkan ke target iSCSI"
+ ],
+ "Connected": [
+ "Tersambung"
+ ],
+ "Connected (%s)": [
+ "Tersambung (%s)"
+ ],
+ "Connected to %s": [
+ "Terhubung ke %s"
+ ],
+ "Connecting": [
+ "Menyambung"
+ ],
+ "Connection actions": [
+ "Tindakan koneksi"
+ ],
+ "Continue": [
+ "Lanjutkan"
+ ],
+ "Controllers": [
+ "Kontroler"
+ ],
+ "Could not authenticate against the server, please check it.": [
+ "Tidak dapat mengautentikasi server, silakan periksa."
+ ],
+ "Could not log in. Please, make sure that the password is correct.": [
+ "Tidak dapat masuk. Harap pastikan kata sandi sudah benar."
+ ],
+ "Create a dedicated LVM volume group": [
+ "Membuat grup volume LVM khusus"
+ ],
+ "Create a new partition": [
+ "Membuat partisi baru"
+ ],
+ "Create user": [
+ "Membuat pengguna"
+ ],
+ "Custom": [
+ "Khusus"
+ ],
+ "DASD": [
+ "DASD"
+ ],
+ "DASD %s": [
+ "DASD %s"
+ ],
+ "DASD devices selection table": [
+ "Tabel pemilihan perangkat DASD"
+ ],
+ "DASDs table section": [
+ "Bagian tabel DASD"
+ ],
+ "DIAG": [
+ "DIAG"
+ ],
+ "DNS": [
+ "DNS"
+ ],
+ "Deactivate": [
+ "Nonaktifkan"
+ ],
+ "Deactivated": [
+ "Dinonaktifkan"
+ ],
+ "Define a user now": [
+ "Tentukan pengguna sekarang"
+ ],
+ "Delete": [
+ "Menghapus"
+ ],
+ "Delete current content": [
+ "Hapus konten saat ini"
+ ],
+ "Destructive actions are allowed": [
+ "Tindakan destruktif diizinkan"
+ ],
+ "Destructive actions are not allowed": [
+ "Tindakan destruktif tidak diizinkan"
+ ],
+ "Details": [
+ "Detail"
+ ],
+ "Device": [
+ "Perangkat"
+ ],
+ "Device selector for new LVM volume group": [
+ "Pemilih perangkat untuk grup volume LVM baru"
+ ],
+ "Device selector for target disk": [
+ "Pemilih perangkat untuk disk target"
+ ],
+ "Devices: %s": [
+ "Perangkat: %s"
+ ],
+ "Discard": [
+ "Buang"
+ ],
+ "Disconnect": [
+ "Putuskan sambungan"
+ ],
+ "Disconnected": [
+ "Terputus"
+ ],
+ "Discover": [
+ "Temukan"
+ ],
+ "Discover iSCSI Targets": [
+ "Menemukan Target iSCSI"
+ ],
+ "Discover iSCSI targets": [
+ "Menemukan target iSCSI"
+ ],
+ "Disk": [
+ "Disk"
+ ],
+ "Disks": [
+ "Disk"
+ ],
+ "Do not configure": [
+ "Jangan konfigurasikan"
+ ],
+ "Do not configure partitions for booting": [
+ "Jangan konfigurasikan partisi untuk booting"
+ ],
+ "Do you want to add it?": [
+ "Apakah Anda ingin menambahkannya?"
+ ],
+ "Do you want to edit it?": [
+ "Apakah Anda ingin mengeditnya?"
+ ],
+ "Download logs": [
+ "Unduh log"
+ ],
+ "Edit": [
+ "Edit"
+ ],
+ "Edit %s": [
+ "Edit %s"
+ ],
+ "Edit %s file system": [
+ "Mengedit sistem file %s"
+ ],
+ "Edit connection %s": [
+ "Edit koneksi %s"
+ ],
+ "Edit file system": [
+ "Edit sistem file"
+ ],
+ "Edit iSCSI Initiator": [
+ "Mengedit Inisiator iSCSI"
+ ],
+ "Edit password too": [
+ "Edit kata sandi juga"
+ ],
+ "Edit the SSH Public Key for root": [
+ "Edit Kunci Publik SSH untuk root"
+ ],
+ "Edit user": [
+ "Edit pengguna"
+ ],
+ "Enable": [
+ "Diaktifkan"
+ ],
+ "Encrypt the system": [
+ "Mengenkripsi sistem"
+ ],
+ "Encrypted Device": [
+ "Perangkat Terenkripsi"
+ ],
+ "Encryption": [
+ "Enkripsi"
+ ],
+ "Encryption Password": [
+ "Kata Sandi Enkripsi"
+ ],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
+ "Exact size": [
+ "Ukuran yang tepat"
+ ],
+ "Exact size for the file system.": [
+ "Ukuran yang tepat untuk sistem berkas."
+ ],
+ "File system type": [
+ "Jenis sistem file"
+ ],
+ "File systems created as new partitions at %s": [
+ "Sistem file yang dibuat sebagai partisi baru di %s"
+ ],
+ "File systems created at a new LVM volume group": [
+ "Sistem file yang dibuat di grup volume LVM baru"
+ ],
+ "File systems created at a new LVM volume group on %s": [
+ "Sistem file yang dibuat di grup volume LVM baru pada %s"
+ ],
+ "Filter by description or keymap code": [
+ "Filter berdasarkan deskripsi atau kode peta kunci"
+ ],
+ "Filter by language, territory or locale code": [
+ "Memfilter berdasarkan bahasa, wilayah, atau kode lokal"
+ ],
+ "Filter by max channel": [
+ "Memfilter menurut saluran maks"
+ ],
+ "Filter by min channel": [
+ "Memfilter menurut saluran min"
+ ],
+ "Filter by pattern title or description": [
+ "Memfilter berdasarkan judul atau deskripsi pola"
+ ],
+ "Filter by territory, time zone code or UTC offset": [
+ "Memfilter berdasarkan wilayah, kode zona waktu, atau offset UTC"
+ ],
+ "Final layout": [
+ "Tata letak akhir"
+ ],
+ "Finish": [
+ "Selesai"
+ ],
+ "Finished": [
+ "Selesai"
+ ],
+ "First user": [
+ "Pengguna pertama"
+ ],
+ "Fixed": [
+ "Tetap"
+ ],
+ "Forget": [
+ "Lupakan"
+ ],
+ "Forget connection %s": [
+ "Lupakan koneksi %s"
+ ],
+ "Format": [
+ "Format"
+ ],
+ "Format selected devices?": [
+ "Memformat perangkat yang dipilih?"
+ ],
+ "Format the device": [
+ "Memformat perangkat"
+ ],
+ "Formatted": [
+ "Diformat"
+ ],
+ "Formatting DASD devices": [
+ "Memformat perangkat DASD"
+ ],
+ "Full Disk Encryption (FDE) allows to protect the information stored at the device, including data, programs, and system files.": [
+ "Full Disk Encryption (FDE) memungkinkan untuk melindungi informasi yang tersimpan di perangkat, termasuk data, program, dan file sistem."
+ ],
+ "Full name": [
+ "Nama lengkap"
+ ],
+ "Gateway": [
+ "Gateway"
+ ],
+ "Gateway can be defined only in 'Manual' mode": [
+ "Gateway hanya dapat ditentukan dalam mode 'Manual'"
+ ],
+ "GiB": [
+ "GiB"
+ ],
+ "Hide": [
+ ""
+ ],
+ "Hide %d subvolume action": [
+ "Menyembunyikan tindakan subvolume %d"
+ ],
+ "Hide details": [
+ "Sembunyikan detail"
+ ],
+ "IP Address": [
+ "Alamat IP"
+ ],
+ "IP address": [
+ "Alamat IP"
+ ],
+ "IP addresses": [
+ "Alamat IP"
+ ],
+ "If a local media was used to run this installer, remove it before the next boot.": [
+ "Jika media lokal digunakan untuk menjalankan penginstalasi ini, hapus media tersebut sebelum boot berikutnya."
+ ],
+ "If you continue, partitions on your hard disk will be modified according to the provided installation settings.": [
+ "Jika Anda melanjutkan, partisi pada hard disk Anda akan dimodifikasi sesuai dengan pengaturan instalasi yang disediakan."
+ ],
+ "In progress": [
+ "Dalam proses"
+ ],
+ "Incorrect IP address": [
+ "Alamat IP salah"
+ ],
+ "Incorrect password": [
+ "Kata sandi salah"
+ ],
+ "Incorrect port": [
+ "Port salah"
+ ],
+ "Incorrect user name": [
+ "Nama pengguna salah"
+ ],
+ "Initiator": [
+ "Inisiator"
+ ],
+ "Initiator name": [
+ "Nama inisiator"
+ ],
+ "Install": [
+ "Instal"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group deleting all the content of the underlying devices": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru dan hapus semua konten perangkat yang mendasarinya"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group on %s deleting all its content": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru pada %s menghapus semua kontennya"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group on %s shrinking existing partitions as needed": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru pada %s menyusutkan partisi yang ada sesuai kebutuhan"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group on %s using a custom strategy to find the needed space": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru di %s menggunakan strategi khusus untuk menemukan ruang yang dibutuhkan"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group on %s without modifying existing partitions": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru di %s tanpa memodifikasi partisi yang ada"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group shrinking existing partitions at the underlying devices as needed": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru yang menyusutkan partisi yang ada di perangkat yang mendasarinya sesuai kebutuhan"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group using a custom strategy to find the needed space at the underlying devices": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru menggunakan strategi khusus untuk menemukan ruang yang dibutuhkan di perangkat yang mendasarinya"
+ ],
+ "Install in a new Logical Volume Manager (LVM) volume group without modifying the partitions at the underlying devices": [
+ "Instal di grup volume Logical Volume Manager (LVM) baru tanpa memodifikasi partisi di perangkat yang mendasarinya"
+ ],
+ "Install new system on": [
+ "Instal sistem baru pada"
+ ],
+ "Install using device %s and deleting all its content": [
+ "Menginstal menggunakan perangkat %s dan menghapus semua isinya"
+ ],
+ "Install using device %s shrinking existing partitions as needed": [
+ "Instal menggunakan perangkat %s mengecilkan partisi yang ada sesuai kebutuhan"
+ ],
+ "Install using device %s with a custom strategy to find the needed space": [
+ "Instal menggunakan perangkat %s dengan strategi khusus untuk menemukan ruang yang dibutuhkan"
+ ],
+ "Install using device %s without modifying existing partitions": [
+ "Menginstal menggunakan perangkat %s tanpa mengubah partisi yang ada"
+ ],
+ "Installation device": [
+ "Perangkat penginstalan"
+ ],
+ "Installation will configure partitions for booting at %s.": [
+ "Instalasi akan mengkonfigurasi partisi untuk booting pada %s."
+ ],
+ "Installation will configure partitions for booting at the installation disk.": [
+ "Instalasi akan mengonfigurasi partisi untuk boot pada disk instalasi."
+ ],
+ "Installation will not configure partitions for booting.": [
+ "Instalasi tidak akan mengkonfigurasi partisi untuk booting."
+ ],
+ "Installation will take %s.": [
+ "Penginstalan akan memakan waktu %s."
+ ],
+ "Installer Options": [
+ "Opsi Penginstal"
+ ],
+ "Installer options": [
+ "Opsi penginstal"
+ ],
+ "Installing the system, please wait...": [
+ "Menginstal sistem, harap tunggu..."
+ ],
+ "Interface": [
+ "Antarmuka"
+ ],
+ "Ip prefix or netmask": [
+ "Awalan IP atau netmask"
+ ],
+ "Keyboard": [
+ "Papan ketik"
+ ],
+ "Keyboard layout": [
+ "Tata letak keyboard"
+ ],
+ "Keyboard selection": [
+ "Pemilihan keyboard"
+ ],
+ "KiB": [
+ "KiB"
+ ],
+ "LUN": [
+ "LUN"
+ ],
+ "Language": [
+ "Bahasa"
+ ],
+ "Limits for the file system size. The final size will be a value between the given minimum and maximum. If no maximum is given then the file system will be as big as possible.": [
+ "Batas untuk ukuran sistem berkas. Ukuran akhir akan berupa nilai antara minimum dan maksimum yang diberikan. Jika tidak ada nilai maksimum yang diberikan, maka sistem berkas akan sebesar mungkin."
+ ],
+ "Loading data...": [
+ "Memuat data..."
+ ],
+ "Loading installation environment, please wait.": [
+ "Memuat lingkungan penginstalan, harap tunggu."
+ ],
+ "Locale selection": [
+ "Pemilihan lokasi"
+ ],
+ "Localization": [
+ "Pelokalan"
+ ],
+ "Location": [
+ "Lokasi"
+ ],
+ "Location for %s file system": [
+ "Lokasi untuk sistem file %s"
+ ],
+ "Log in": [
+ "Masuk"
+ ],
+ "Log in as %s": [
+ "Masuk sebagai %s"
+ ],
+ "Logical volume at system LVM": [
+ "Volume logis pada LVM sistem"
+ ],
+ "Login": [
+ "Masuk"
+ ],
+ "Login %s": [
+ "Login %s"
+ ],
+ "Login form": [
+ "Formulir masuk"
+ ],
+ "Logout": [
+ "Keluar"
+ ],
+ "Main disk or LVM Volume Group for installation.": [
+ "Disk utama atau Grup Volume LVM untuk instalasi."
+ ],
+ "Main navigation": [
+ "Navigasi utama"
+ ],
+ "Make sure you provide the correct values": [
+ "Pastikan Anda memberikan nilai yang benar"
+ ],
+ "Manage and format": [
+ "Mengelola dan memformat"
+ ],
+ "Manual": [
+ "Manual"
+ ],
+ "Maximum": [
+ "Maksimum"
+ ],
+ "Maximum desired size": [
+ "Ukuran maksimum yang diinginkan"
+ ],
+ "Maximum must be greater than minimum": [
+ "Maksimum harus lebih besar dari minimum"
+ ],
+ "Members: %s": [
+ "Anggota: %s"
+ ],
+ "Method": [
+ "Metode"
+ ],
+ "MiB": [
+ "MiB"
+ ],
+ "Minimum": [
+ "Minimum"
+ ],
+ "Minimum desired size": [
+ "Ukuran minimum yang diinginkan"
+ ],
+ "Minimum size is required": [
+ "Diperlukan ukuran minimum"
+ ],
+ "Mode": [
+ "Mode"
+ ],
+ "Modify": [
+ "Ubah"
+ ],
+ "More info for file system types": [
+ "Info lebih lanjut untuk jenis sistem file"
+ ],
+ "Mount %1$s at %2$s (%3$s)": [
+ "Mount %1$s di %2$s (%3$s)"
+ ],
+ "Mount Point": [
+ "Mount Point"
+ ],
+ "Mount point": [
+ "Titik pemasangan"
+ ],
+ "Mount the file system": [
+ "Mount sistem file"
+ ],
+ "Multipath": [
+ "Multipath"
+ ],
+ "Name": [
+ "Nama"
+ ],
+ "Network": [
+ "Jaringan"
+ ],
+ "New": [
+ "Baru"
+ ],
+ "No": [
+ "Tidak"
+ ],
+ "No Wi-Fi supported": [
+ "Tidak mendukung Wi-Fi"
+ ],
+ "No additional software was selected.": [
+ "Tidak ada perangkat lunak tambahan yang dipilih."
+ ],
+ "No connected yet": [
+ "Belum terhubung"
+ ],
+ "No content found": [
+ "Tidak ada konten yang ditemukan"
+ ],
+ "No device selected yet": [
+ "Belum ada perangkat yang dipilih"
+ ],
+ "No iSCSI targets found.": [
+ "Tidak ditemukan target iSCSI."
+ ],
+ "No partitions will be automatically configured for booting. Use with caution.": [
+ "Tidak ada partisi yang akan dikonfigurasi secara otomatis untuk booting. Gunakan dengan hati-hati."
+ ],
+ "No root authentication method defined yet.": [
+ "Belum ada metode autentikasi root yang ditetapkan."
+ ],
+ "No user defined yet.": [
+ "Belum ada pengguna yang ditetapkan."
+ ],
+ "No visible Wi-Fi networks found": [
+ "Tidak ditemukan jaringan Wi-Fi"
+ ],
+ "No wired connections found": [
+ "Tidak ditemukan koneksi kabel"
+ ],
+ "No zFCP controllers found.": [
+ "Tidak ditemukan pengontrol zFCP."
+ ],
+ "No zFCP disks found.": [
+ "Tidak ditemukan disk zFCP."
+ ],
+ "None": [
+ "Tidak ada"
+ ],
+ "None of the keymaps match the filter.": [
+ "Tidak ada keymap yang cocok dengan filter."
+ ],
+ "None of the locales match the filter.": [
+ "Tidak ada lokasi yang cocok dengan filter."
+ ],
+ "None of the patterns match the filter.": [
+ "Tidak ada pola yang cocok dengan filter."
+ ],
+ "None of the time zones match the filter.": [
+ "Tidak ada zona waktu yang cocok dengan filter."
+ ],
+ "Not possible with the current setup. Click to know more.": [
+ "Tidak memungkinkan dengan pengaturan saat ini. Klik untuk mengetahui lebih lanjut."
+ ],
+ "Not selected yet": [
+ "Belum dipilih"
+ ],
+ "Not set": [
+ "Belum ditetapkan"
+ ],
+ "Offline devices must be activated before formatting them. Please, unselect or activate the devices listed below and try it again": [
+ "Perangkat offline harus diaktifkan sebelum memformatnya. Batalkan pilihan atau aktifkan perangkat yang tercantum di bawah ini dan coba lagi"
+ ],
+ "Offload card": [
+ "Bongkar kartu"
+ ],
+ "On boot": [
+ "Saat boot"
+ ],
+ "Only available if authentication by target is provided": [
+ "Hanya tersedia jika autentikasi berdasarkan target disediakan"
+ ],
+ "Options toggle": [
+ "Sakelar opsi"
+ ],
+ "Other": [
+ "Lainnya"
+ ],
+ "Overview": [
+ "Ikhtisar"
+ ],
+ "Partition Info": [
+ "Info Partisi"
+ ],
+ "Partition at %s": [
+ "Partisi pada %s"
+ ],
+ "Partition at installation disk": [
+ "Partisi pada disk instalasi"
+ ],
+ "Partitions and file systems": [
+ "Partisi dan sistem file"
+ ],
+ "Partitions to boot will be allocated at the following device.": [
+ "Partisi untuk boot akan dialokasikan pada perangkat berikut."
+ ],
+ "Partitions to boot will be allocated at the installation disk (%s).": [
+ "Partisi untuk boot akan dialokasikan pada disk instalasi (%s)."
+ ],
+ "Partitions to boot will be allocated at the installation disk.": [
+ "Partisi untuk boot akan dialokasikan pada disk instalasi."
+ ],
+ "Password": [
+ "Kata sandi"
+ ],
+ "Password Required": [
+ "Diperlukan Kata Sandi"
+ ],
+ "Password confirmation": [
+ "Konfirmasi kata sandi"
+ ],
+ "Password for root user": [
+ "Kata sandi untuk pengguna root"
+ ],
+ "Password input": [
+ "Masukan kata sandi"
+ ],
+ "Password visibility button": [
+ "Tombol visibilitas kata sandi"
+ ],
+ "Passwords do not match": [
+ "Kata sandi tidak cocok"
+ ],
+ "Pending": [
+ "Tertunda"
+ ],
+ "Perform an action": [
+ "Melakukan tindakan"
+ ],
+ "PiB": [
+ "PiB"
+ ],
+ "Planned Actions": [
+ "Tindakan yang Direncanakan"
+ ],
+ "Please, be aware that a user must be defined before installing the system to be able to log into it.": [
+ "Perlu diketahui bahwa pengguna harus ditetapkan sebelum menginstal sistem agar dapat masuk ke dalamnya."
+ ],
+ "Please, cancel and check the settings if you are unsure.": [
+ "Mohon batalkan dan periksa pengaturan jika Anda tidak yakin."
+ ],
+ "Please, check whether it is running.": [
+ "Silakan periksa apakah sudah berjalan."
+ ],
+ "Please, define at least one authentication method for logging into the system as root.": [
+ "Tentukan setidaknya satu metode autentikasi untuk masuk ke sistem sebagai root."
+ ],
+ "Please, perform an iSCSI discovery in order to find available iSCSI targets.": [
+ "Lakukan penemuan iSCSI untuk menemukan target iSCSI yang tersedia."
+ ],
+ "Please, provide its password to log in to the system.": [
+ "Harap berikan kata sandi untuk masuk ke sistem."
+ ],
+ "Please, review provided settings and try again.": [
+ "Harap tinjau pengaturan yang tersedia dan coba lagi."
+ ],
+ "Please, try to activate a zFCP controller.": [
+ "Silakan coba aktifkan pengontrol zFCP."
+ ],
+ "Please, try to activate a zFCP disk.": [
+ "Silakan coba aktifkan disk zFCP."
+ ],
+ "Port": [
+ "Port"
+ ],
+ "Portal": [
+ "Portal"
+ ],
+ "Pre-installation checks": [
+ "Pemeriksaan pra-instalasi"
+ ],
+ "Prefix length or netmask": [
+ "Panjang awalan atau netmask"
+ ],
+ "Prepare more devices by configuring advanced": [
+ "Siapkan lebih banyak perangkat dengan menyiapkan konfigurasi lanjutan"
+ ],
+ "Presence of other volumes (%s)": [
+ "Keberadaan volume lain (%s)"
+ ],
+ "Protection for the information stored at the device, including data, programs, and system files.": [
+ "Perlindungan untuk informasi yang tersimpan di perangkat, termasuk data, program, dan file sistem."
+ ],
+ "Provide a password to ensure administrative access to the system.": [
+ "Berikan kata sandi untuk memastikan akses administratif ke sistem."
+ ],
+ "Question": [
+ "Pertanyaan"
+ ],
+ "Range": [
+ "Rentang"
+ ],
+ "Read zFCP devices": [
+ "Baca perangkat zFCP"
+ ],
+ "Reboot": [
+ "Maut ulang"
+ ],
+ "Register": [
+ "Mendaftar"
+ ],
+ "Registration": [
+ "Pendaftaran"
+ ],
+ "Registration code": [
+ "Kode pendaftaran"
+ ],
+ "Reload": [
+ "Muat ulang"
+ ],
+ "Remove": [
+ "Menghapus"
+ ],
+ "Remove max channel filter": [
+ "Menghapus filter saluran maks"
+ ],
+ "Remove min channel filter": [
+ "Menghapus filter saluran min"
+ ],
+ "Reset location": [
+ "Atur ulang lokasi"
+ ],
+ "Reset to defaults": [
+ "Mengatur ulang ke default"
+ ],
+ "Reused %s": [
+ "Digunakan kembali %s"
+ ],
+ "Root SSH public key": [
+ "Kunci publik SSH root"
+ ],
+ "Root authentication": [
+ "Autentikasi root"
+ ],
+ "Root password": [
+ "Kata sandi root"
+ ],
+ "SD Card": [
+ "Kartu SD"
+ ],
+ "SSH Key": [
+ "Kunci SSH"
+ ],
+ "SSID": [
+ "SSID"
+ ],
+ "Search": [
+ "Cari"
+ ],
+ "Security": [
+ "Keamanan"
+ ],
+ "See more details": [
+ "Lihat detail lebih lanjut"
+ ],
+ "Select": [
+ "Pilih"
+ ],
+ "Select a disk": [
+ "Pilih disk"
+ ],
+ "Select a location": [
+ "Pilih lokasi"
+ ],
+ "Select a product": [
+ "Pilih produk"
+ ],
+ "Select booting partition": [
+ "Pilih partisi booting"
+ ],
+ "Select how to allocate the file system": [
+ "Pilih cara mengalokasikan sistem file"
+ ],
+ "Select in which device to allocate the file system": [
+ "Pilih perangkat mana yang akan dialokasikan sebagai sistem file"
+ ],
+ "Select installation device": [
+ "Pilih perangkat instalasi"
+ ],
+ "Select what to do with each partition.": [
+ "Pilih apa yang akan dilakukan dengan setiap partisi."
+ ],
+ "Selected patterns": [
+ "Pola yang dipilih"
+ ],
+ "Separate LVM at %s": [
+ "Pisahkan LVM pada %s"
+ ],
+ "Server IP": [
+ "IP Server"
+ ],
+ "Set": [
+ "Tetapkan"
+ ],
+ "Set DIAG Off": [
+ "Mengatur DIAG Tidak Aktif"
+ ],
+ "Set DIAG On": [
+ "Mengatur DIAG Aktif"
+ ],
+ "Set a password": [
+ "Tetapkan kata sandi"
+ ],
+ "Set a root password": [
+ "Atur kata sandi root"
+ ],
+ "Set root SSH public key": [
+ "Atur kunci publik SSH root"
+ ],
+ "Setup root user authentication": [
+ "Menyiapkan autentikasi pengguna root"
+ ],
+ "Show %d subvolume action": [
+ "Tampilkan tindakan subvolume %d"
+ ],
+ "Show information about %s": [
+ "Menampilkan informasi tentang %s"
+ ],
+ "Show partitions and file-systems actions": [
+ "Menampilkan tindakan partisi dan sistem file"
+ ],
+ "Shrink existing partitions": [
+ "Perkecil partisi yang ada"
+ ],
+ "Shrinking partitions is allowed": [
+ "Menyusutkan partisi diizinkan"
+ ],
+ "Shrinking partitions is not allowed": [
+ "Menyusutkan partisi tidak diizinkan"
+ ],
+ "Shrinking some partitions is allowed but not needed": [
+ "Menyusutkan beberapa partisi diizinkan tetapi tidak diperlukan"
+ ],
+ "Size": [
+ "Ukuran"
+ ],
+ "Size unit": [
+ "Satuan ukuran"
+ ],
+ "Software": [
+ "Perangkat lunak"
+ ],
+ "Software %s": [
+ "Perangkat Lunak %s"
+ ],
+ "Software selection": [
+ "Pemilihan perangkat lunak"
+ ],
+ "Something went wrong": [
+ "Ada yang tidak beres"
+ ],
+ "Space policy": [
+ "Kebijakan ruang"
+ ],
+ "Startup": [
+ "Startup"
+ ],
+ "Status": [
+ "Status"
+ ],
+ "Storage": [
+ "Penyimpanan"
+ ],
+ "Storage proposal not possible": [
+ "Proposal penyimpanan tidak memungkinkan"
+ ],
+ "Structure of the new system, including any additional partition needed for booting": [
+ "Struktur sistem baru, termasuk partisi tambahan yang diperlukan untuk booting"
+ ],
+ "Swap at %1$s (%2$s)": [
+ "Swap di %1$s (%2$s)"
+ ],
+ "Swap partition (%s)": [
+ "Partisi swap (%s)"
+ ],
+ "Swap volume (%s)": [
+ "Volume swap (%s)"
+ ],
+ "TPM sealing requires the new system to be booted directly.": [
+ "Penyegelan TPM mengharuskan sistem baru untuk di-boot secara langsung."
+ ],
+ "Table with mount points": [
+ "Tabel dengan titik pemasangan"
+ ],
+ "Target Password": [
+ "Kata sandi target"
+ ],
+ "Targets": [
+ "Target"
+ ],
+ "The amount of RAM in the system": [
+ "Jumlah RAM dalam sistem"
+ ],
+ "The configuration of snapshots": [
+ "Konfigurasi snapshot"
+ ],
+ "The content may be deleted": [
+ "Konten mungkin akan dihapus"
+ ],
+ "The current file system on %s is selected to be mounted at %s.": [
+ "Sistem file saat ini pada %s dipilih untuk dimount pada %s."
+ ],
+ "The current file system on the selected device will be mounted without formatting the device.": [
+ "Sistem file saat ini pada perangkat yang dipilih akan dipasang tanpa memformat perangkat."
+ ],
+ "The data is kept, but the current partitions will be resized as needed.": [
+ "Data tetap dipertahankan, tetapi partisi saat ini akan diubah ukurannya sesuai kebutuhan."
+ ],
+ "The data is kept. Only the space not assigned to any partition will be used.": [
+ "Data tetap dipertahankan. Hanya ruang yang tidak ditetapkan ke partisi mana pun yang akan digunakan."
+ ],
+ "The device cannot be shrunk:": [
+ "Perangkat tidak dapat dikecilkan:"
+ ],
+ "The encryption password did not work": [
+ "Kata sandi enkripsi tidak berfungsi"
+ ],
+ "The file system is allocated at the device %s.": [
+ "Sistem file dialokasikan pada perangkat %s."
+ ],
+ "The file system will be allocated as a new partition at the selected disk.": [
+ "Sistem file akan dialokasikan sebagai partisi baru pada disk yang dipilih."
+ ],
+ "The file systems are allocated at the installation device by default. Indicate a custom location to create the file system at a specific device.": [
+ "Sistem file dialokasikan pada perangkat instalasi secara default. Tunjukkan lokasi khusus untuk membuat sistem file di perangkat tertentu."
+ ],
+ "The file systems will be allocated by default as [logical volumes of a new LVM Volume Group]. The corresponding physical volumes will be created on demand as new partitions at the selected devices.": [
+ "Sistem file akan dialokasikan secara default sebagai [volume logis dari Grup Volume LVM baru]. Volume fisik yang sesuai akan dibuat sesuai permintaan sebagai partisi baru pada perangkat yang dipilih."
+ ],
+ "The file systems will be allocated by default as [new partitions in the selected device].": [
+ "Sistem file akan dialokasikan secara default sebagai [partisi baru di perangkat yang dipilih]."
+ ],
+ "The final size depends on %s.": [
+ "Ukuran akhir tergantung pada %s."
+ ],
+ "The final step to configure the Trusted Platform Module (TPM) to automatically open encrypted devices will take place during the first boot of the new system. For that to work, the machine needs to boot directly to the new boot loader.": [
+ "Langkah terakhir untuk mengonfigurasi Trusted Platform Module (TPM) agar dapat membuka perangkat yang dienkripsi secara otomatis selama booting pertama dari sistem yang baru. Agar dapat berfungsi, mesin harus melakukan booting secara langsung ke boot loader yang baru."
+ ],
+ "The following software patterns are selected for installation:": [
+ "Pola perangkat lunak berikut ini dipilih untuk instalasi:"
+ ],
+ "The installation on your machine is complete.": [
+ "Penginstalan pada mesin Anda sudah selesai."
+ ],
+ "The installation will take": [
+ "Instalasi akan memakan waktu"
+ ],
+ "The installation will take %s including:": [
+ "Instalasi akan memakan waktu %s termasuk:"
+ ],
+ "The installer requires [root] user privileges.": [
+ "Penginstal memerlukan hak akses pengguna [root]."
+ ],
+ "The mount point is invalid": [
+ "Mount point tidak valid"
+ ],
+ "The options for the file system type depends on the product and the mount point.": [
+ "Opsi untuk jenis sistem file tergantung pada produk dan mount point."
+ ],
+ "The password will not be needed to boot and access the data if the TPM can verify the integrity of the system. TPM sealing requires the new system to be booted directly on its first run.": [
+ "Kata sandi tidak akan diperlukan untuk mem-boot dan mengakses data jika TPM dapat memverifikasi integritas sistem. Penyegelan TPM mengharuskan sistem baru untuk di-boot secara langsung saat pertama kali dijalankan."
+ ],
+ "The selected device will be formatted as %s file system.": [
+ "Perangkat yang dipilih akan diformat sebagai sistem file %s."
+ ],
+ "The size of the file system cannot be edited": [
+ "Ukuran sistem file tidak dapat diedit"
+ ],
+ "The system does not support Wi-Fi connections, probably because of missing or disabled hardware.": [
+ "Sistem tidak mendukung koneksi Wi-Fi, mungkin karena perangkat keras yang tidak ada atau dinonaktifkan."
+ ],
+ "The system has not been configured for connecting to a Wi-Fi network yet.": [
+ "Sistem belum dikonfigurasi untuk menghubungkan ke jaringan Wi-Fi."
+ ],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
+ "The system will use %s as its default language.": [
+ "Sistem akan menggunakan %s sebagai bahasa default."
+ ],
+ "The systems will be configured as displayed below.": [
+ "Sistem akan dikonfigurasikan seperti yang ditampilkan di bawah ini."
+ ],
+ "The type and size of the file system cannot be edited.": [
+ "Jenis dan ukuran sistem file tidak dapat diedit."
+ ],
+ "The zFCP disk was not activated.": [
+ "Disk zFCP tidak diaktifkan."
+ ],
+ "There is a predefined file system for %s.": [
+ "Ada sistem file yang telah ditentukan untuk %s."
+ ],
+ "There is already a file system for %s.": [
+ "Sudah ada sistem file untuk %s."
+ ],
+ "These are the most relevant installation settings. Feel free to browse the sections in the menu for further details.": [
+ "Berikut ini adalah pengaturan instalasi yang paling relevan. Silakan telusuri bagian dalam menu untuk rincian lebih lanjut."
+ ],
+ "These limits are affected by:": [
+ "Batasan ini dipengaruhi oleh:"
+ ],
+ "This action could destroy any data stored on the devices listed below. Please, confirm that you really want to continue.": [
+ "Tindakan ini dapat menghapus data yang tersimpan pada perangkat yang tercantum di bawah ini. Harap konfirmasikan bahwa Anda benar-benar ingin melanjutkan."
+ ],
+ "This product does not allow to select software patterns during installation. However, you can add additional software once the installation is finished.": [
+ "Produk ini tidak memungkinkan untuk memilih pola perangkat lunak selama instalasi. Namun demikian, Anda dapat menambahkan perangkat lunak tambahan setelah penginstalan selesai."
+ ],
+ "This space includes the base system and the selected software patterns, if any.": [
+ "Ruang ini mencakup sistem dasar dan pola perangkat lunak yang dipilih, jika ada."
+ ],
+ "TiB": [
+ "TiB"
+ ],
+ "Time zone": [
+ "Zona waktu"
+ ],
+ "To ensure the new system is able to boot, the installer may need to create or configure some partitions in the appropriate disk.": [
+ "Untuk memastikan sistem baru dapat melakukan booting, penginstal mungkin perlu membuat atau mengonfigurasi beberapa partisi di disk yang sesuai."
+ ],
+ "Transactional Btrfs": [
+ "Btrfs Transaksional"
+ ],
+ "Transactional Btrfs root partition (%s)": [
+ "Partisi root Btrfs transaksional (%s)"
+ ],
+ "Transactional Btrfs root volume (%s)": [
+ "Volume root Btrfs transaksional (%s)"
+ ],
+ "Transactional root file system": [
+ "Sistem file root transaksional"
+ ],
+ "Type": [
+ "Jenis"
+ ],
+ "Unit for the maximum size": [
+ "Satuan untuk ukuran maksimum"
+ ],
+ "Unit for the minimum size": [
+ "Satuan untuk ukuran minimum"
+ ],
+ "Unselect": [
+ "Batalkan pilihan"
+ ],
+ "Unused space": [
+ "Ruang tidak terpakai"
+ ],
+ "Up to %s can be recovered by shrinking the device.": [
+ "Hingga %s dapat dipulihkan dengan mengecilkan perangkat."
+ ],
+ "Upload": [
+ "Unggah"
+ ],
+ "Upload a SSH Public Key": [
+ "Unggah Kunci Publik SSH"
+ ],
+ "Upload, paste, or drop an SSH public key": [
+ "Unggah, tempel, atau jatuhkan kunci publik SSH"
+ ],
+ "Usage": [
+ "Penggunaan"
+ ],
+ "Use Btrfs snapshots for the root file system": [
+ "Gunakan snapshot Btrfs untuk sistem file root"
+ ],
+ "Use available space": [
+ "Gunakan ruang yang tersedia"
+ ],
+ "Use suggested username": [
+ "Gunakan nama pengguna yang disarankan"
+ ],
+ "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot": [
+ "Gunakan Modul Platform Tepercaya (TPM) untuk mendekripsi secara otomatis pada setiap boot"
+ ],
+ "User full name": [
+ "Nama lengkap pengguna"
+ ],
+ "User name": [
+ "Nama pengguna"
+ ],
+ "Username": [
+ "Nama pengguna"
+ ],
+ "Username suggestion dropdown": [
+ "Tarik-turun saran nama pengguna"
+ ],
+ "Users": [
+ "Pengguna"
+ ],
+ "Visible Wi-Fi networks": [
+ "Jaringan Wi-Fi yang terlihat"
+ ],
+ "WPA & WPA2 Personal": [
+ "WPA & WPA2 Pribadi"
+ ],
+ "WPA Password": [
+ "Kata Sandi WPA"
+ ],
+ "WWPN": [
+ "WWPN"
+ ],
+ "Waiting for actions information...": [
+ "Menunggu informasi tindakan..."
+ ],
+ "Waiting for information about storage configuration": [
+ "Menunggu informasi tentang konfigurasi penyimpanan"
+ ],
+ "Wi-Fi": [
+ "Wi-Fi"
+ ],
+ "WiFi connection form": [
+ "Formulir koneksi WiFi"
+ ],
+ "Wired": [
+ "Kabel"
+ ],
+ "Wires: %s": [
+ "Kabel: %s"
+ ],
+ "Yes": [
+ "Ya"
+ ],
+ "You can change it or select another authentication method in the 'Users' section before installing.": [
+ "Anda dapat mengubahnya atau memilih metode autentikasi lain di bagian 'Pengguna' sebelum menginstal."
+ ],
+ "ZFCP": [
+ "ZFCP"
+ ],
+ "affecting": [
+ "mempengaruhi"
+ ],
+ "at least %s": [
+ "setidaknya %s"
+ ],
+ "auto": [
+ "otomatis"
+ ],
+ "auto selected": [
+ "otomatis dipilih"
+ ],
+ "configured": [
+ "dikonfigurasi"
+ ],
+ "deleting current content": [
+ "menghapus konten saat ini"
+ ],
+ "disabled": [
+ "dinonaktifkan"
+ ],
+ "enabled": [
+ "diaktifkan"
+ ],
+ "iBFT": [
+ "iBFT"
+ ],
+ "iSCSI": [
+ "iSCSI"
+ ],
+ "shrinking partitions": [
+ "menyusutkan partisi"
+ ],
+ "storage techs": [
+ "teknisi penyimpanan"
+ ],
+ "the amount of RAM in the system": [
+ "jumlah RAM dalam sistem"
+ ],
+ "the configuration of snapshots": [
+ "konfigurasi snapshot"
+ ],
+ "the presence of the file system for %s": [
+ "keberadaan sistem berkas untuk %s"
+ ],
+ "user autologin": [
+ "autologin pengguna"
+ ],
+ "using TPM unlocking": [
+ "menggunakan pembukaan kunci TPM"
+ ],
+ "with custom actions": [
+ "dengan tindakan khusus"
+ ],
+ "without modifying any partition": [
+ "tanpa memodifikasi partisi apa pun"
+ ],
+ "zFCP": [
+ "zFCP"
+ ],
+ "zFCP Disk Activation": [
+ "Aktivasi Disk zFCP"
+ ],
+ "zFCP Disk activation form": [
+ "Formulir aktivasi Disk zFCP"
+ ]
+};
diff --git a/web/src/po/po.ja.js b/web/src/po/po.ja.js
index 366b047ee4..43db56cd46 100644
--- a/web/src/po/po.ja.js
+++ b/web/src/po/po.ja.js
@@ -33,6 +33,9 @@ export default {
"%s disk": [
"%s ディスク"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s は一括更新のできる不可変なシステムです。読み込み専用の btrfs ルートファイルシステムを利用して更新を適用します。"
],
@@ -42,6 +45,9 @@ export default {
"%s with %d partitions": [
"%s (%d 個のパーティション)"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -453,6 +459,9 @@ export default {
"Encryption Password": [
"暗号化パスワード"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"正確なサイズ"
],
@@ -540,6 +549,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"%d 個のサブボリューム処理を隠す"
],
@@ -637,7 +649,7 @@ export default {
"インストール作業では起動用のパーティションを設定しません。"
],
"Installation will take %s.": [
- "インストールを行うには %s が必要です。"
+ "インストールするには %s が必要です。"
],
"Installer Options": [
"インストーラのオプション"
@@ -993,6 +1005,15 @@ export default {
"Reboot": [
"再起動"
],
+ "Register": [
+ "登録"
+ ],
+ "Registration": [
+ "登録"
+ ],
+ "Registration code": [
+ "登録コード"
+ ],
"Reload": [
"再読み込み"
],
@@ -1170,9 +1191,6 @@ export default {
"Table with mount points": [
"マウントポイントの一覧"
],
- "Take your time to check your configuration before starting the installation process.": [
- "インストール処理を開始する前に、設定内容をよくご確認ください。"
- ],
"Target Password": [
"ターゲットのパスワード"
],
@@ -1237,7 +1255,7 @@ export default {
"インストールで占有する容量は"
],
"The installation will take %s including:": [
- "インストールを行うには、下記を含めた %s が必要です:"
+ "下記の構成をインストールするには %s が必要です:"
],
"The installer requires [root] user privileges.": [
"インストーラを使用するには [root] 権限が必要です。"
@@ -1263,6 +1281,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"このシステムでは、まだ WiFi ネットワークへの接続設定を実施していません。"
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"システムは %s を既定の言語として使用します。"
],
@@ -1386,9 +1407,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "お待ちください"
- ],
"Waiting for actions information...": [
"処理に関する情報を待機しています..."
],
diff --git a/web/src/po/po.nb_NO.js b/web/src/po/po.nb_NO.js
index 2c10ab4eb2..c05c51ad40 100644
--- a/web/src/po/po.nb_NO.js
+++ b/web/src/po/po.nb_NO.js
@@ -34,6 +34,9 @@ export default {
"%s disk": [
"%s disk"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s er et uforanderlig system med atomiske oppdateringer. Den bruker et skrivebeskyttet Btrfs filsystem oppdatert via øyeblikksbilder."
],
@@ -43,6 +46,9 @@ export default {
"%s with %d partitions": [
"%s med %d partisjoner"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -428,6 +434,9 @@ export default {
"Encryption Password": [
"Krypteringspassord"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Nøyaktig størrelse"
],
@@ -512,6 +521,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Skjul %d undervolum handling",
"Skjul %d undervolumers handlinger"
@@ -585,6 +597,9 @@ export default {
"Install new system on": [
"Installer nytt system på"
],
+ "Install using an advanced configuration.": [
+ ""
+ ],
"Install using device %s and deleting all its content": [
"Installer med enheten %s og slett alt av dens innhold"
],
@@ -942,6 +957,15 @@ export default {
"Reboot": [
"Start om"
],
+ "Register": [
+ "Registrer"
+ ],
+ "Registration": [
+ "Registrering"
+ ],
+ "Registration code": [
+ "Registrerings kode"
+ ],
"Reload": [
"Last på nytt"
],
@@ -1114,9 +1138,6 @@ export default {
"Table with mount points": [
"Tabell med monteringspunkter"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Ta god tid til å sjekke din konfigurasjon før du begynner installasjons prosessen."
- ],
"Target Password": [
"Målets Passord"
],
@@ -1204,6 +1225,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Systemet har ikke blitt konfigurert for å koble til et Wi-Fi nettverk ennå."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Systemet vil bruke %s som standardspråk."
],
@@ -1318,9 +1342,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Venter"
- ],
"Waiting for actions information...": [
"Venter på handlingsinformasjon..."
],
diff --git a/web/src/po/po.pt_BR.js b/web/src/po/po.pt_BR.js
index 87cae8e3c0..bc3fde86b0 100644
--- a/web/src/po/po.pt_BR.js
+++ b/web/src/po/po.pt_BR.js
@@ -34,6 +34,9 @@ export default {
"%s disk": [
"disco %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s é um sistema imutável com atualizações atômicas. Ele usa um sistema de arquivos Btrfs somente leitura atualizado via instantâneos."
],
@@ -43,6 +46,9 @@ export default {
"%s with %d partitions": [
"%s com %d partições"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -188,7 +194,7 @@ export default {
"Antes %s"
],
"Before installing, you have to make some decisions. Click on each section to review the settings.": [
- ""
+ "Antes de instalar, você precisa tomar algumas decisões. Clique em cada seção para revisar as configurações."
],
"Before starting the installation, you need to address the following problems:": [
"Antes de iniciar a instalação, você precisa resolver os seguintes problemas:"
@@ -455,6 +461,9 @@ export default {
"Encryption Password": [
"Senha de criptografia"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Tamanho exato"
],
@@ -542,6 +551,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Ocultar %d ação do subvolume",
"Ocultar %d ações de subvolume"
@@ -615,6 +627,9 @@ export default {
"Install new system on": [
"Instalar novo sistema em"
],
+ "Install using an advanced configuration.": [
+ ""
+ ],
"Install using device %s and deleting all its content": [
"Instalar usando o dispositivo %s e excluindo todo o seu conteúdo"
],
@@ -850,7 +865,7 @@ export default {
"Nenhum dos fusos horários corresponde ao filtro."
],
"Not possible with the current setup. Click to know more.": [
- ""
+ "Não é possível com a configuração atual. Clique para saber mais."
],
"Not selected yet": [
"Ainda não selecionado"
@@ -909,6 +924,9 @@ export default {
"Password confirmation": [
"Confirmação de senha"
],
+ "Password for root user": [
+ "Senha para usuário root"
+ ],
"Password input": [
"Entrada de senha"
],
@@ -963,6 +981,9 @@ export default {
"Portal": [
"Portal"
],
+ "Pre-installation checks": [
+ "Verificações de pré-instalação"
+ ],
"Prefix length or netmask": [
"Comprimento do prefixo ou máscara de rede"
],
@@ -975,6 +996,9 @@ export default {
"Protection for the information stored at the device, including data, programs, and system files.": [
"Proteção para as informações armazenadas no dispositivo, incluindo dados, programas e arquivos de sistema."
],
+ "Provide a password to ensure administrative access to the system.": [
+ "Forneça uma senha para garantir acesso administrativo ao sistema."
+ ],
"Question": [
"Pergunta"
],
@@ -987,6 +1011,15 @@ export default {
"Reboot": [
"Reiniciar"
],
+ "Register": [
+ "Registro"
+ ],
+ "Registration": [
+ "Cadastro"
+ ],
+ "Registration code": [
+ "Código de registro"
+ ],
"Reload": [
"Recarregar"
],
@@ -1089,6 +1122,9 @@ export default {
"Set root SSH public key": [
"Definir chave pública SSH raiz"
],
+ "Setup root user authentication": [
+ "Configurar autenticação do usuário root"
+ ],
"Show %d subvolume action": [
"Mostrar ação do subvolume %d",
"Mostrar %d ações de subvolume"
@@ -1162,9 +1198,6 @@ export default {
"Table with mount points": [
"Tabela com pontos de montagem"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Reserve um tempo para verificar sua configuração antes de iniciar o processo de instalação."
- ],
"Target Password": [
"Senha de destino"
],
@@ -1255,6 +1288,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"O sistema ainda não foi configurado para se conectar a uma rede Wi-Fi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"O sistema usará %s como idioma padrão."
],
@@ -1378,9 +1414,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Aguardando"
- ],
"Waiting for actions information...": [
"Aguardando informações sobre ações..."
],
@@ -1402,6 +1435,9 @@ export default {
"Yes": [
"Sim"
],
+ "You can change it or select another authentication method in the 'Users' section before installing.": [
+ "Você pode alterá-lo ou selecionar outro método de autenticação na seção \"Usuários\" antes de instalar."
+ ],
"ZFCP": [
"ZFCP"
],
diff --git a/web/src/po/po.ru.js b/web/src/po/po.ru.js
index 2849d490b2..c76e50dfcb 100644
--- a/web/src/po/po.ru.js
+++ b/web/src/po/po.ru.js
@@ -35,15 +35,21 @@ export default {
"%s disk": [
"Диск %s"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s - это неизменяемая система с атомарными обновлениями. Она использует файловую систему Btrfs, доступную только для чтения и обновляемую с помощью моментальных снимков."
],
"%s logo": [
- ""
+ "Логотип %s"
],
"%s with %d partitions": [
"%s с %d разделами"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -149,6 +155,9 @@ export default {
"Authentication by target": [
"Аутентификация по цели"
],
+ "Authentication failed, please try again": [
+ "Сбой аутентификации, попробуйте еще раз"
+ ],
"Auto": [
"Автоматически"
],
@@ -164,6 +173,12 @@ export default {
"Automatic (DHCP)": [
"Автоматически (DHCP)"
],
+ "Automatic LUN scan is [disabled]. LUNs have to be manually configured after activating a controller.": [
+ "Автоматическое сканирование LUN [отключено]. LUN должны быть настроены вручную после активации контроллера."
+ ],
+ "Automatic LUN scan is [enabled]. Activating a controller which is running in NPIV mode will automatically configures all its LUNs.": [
+ "Автоматическое сканирование LUN [включено]. Активация контроллера, работающего в режиме NPIV, автоматически сконфигурирует все его LUNы."
+ ],
"Automatically calculated size according to the selected product.": [
"Автоматический расчет размера в соответствии с выбранным продуктом."
],
@@ -173,11 +188,14 @@ export default {
"Back": [
"Назад"
],
+ "Back to device selection": [
+ "Назад к выбору устройств"
+ ],
"Before %s": [
"До %s"
],
"Before installing, you have to make some decisions. Click on each section to review the settings.": [
- ""
+ "Перед установкой необходимо принять некоторые решения. Щелкните на каждом разделе, чтобы просмотреть настройки."
],
"Before starting the installation, you need to address the following problems:": [
"До начала установки нужно устранить следующие проблемы:"
@@ -210,7 +228,7 @@ export default {
"Не удалось подключиться к серверу Agama"
],
"Cannot format all selected devices": [
- ""
+ "Невозможно отформатировать все выбранные устройства"
],
"Change": [
"Изменить"
@@ -277,6 +295,9 @@ export default {
"Connected (%s)": [
"Подключено (%s)"
],
+ "Connected to %s": [
+ "Подключено к %s"
+ ],
"Connecting": [
"Подключение"
],
@@ -287,7 +308,7 @@ export default {
"Продолжить"
],
"Controllers": [
- ""
+ "Контроллеры"
],
"Could not authenticate against the server, please check it.": [
"Не удалось пройти аутентификацию на сервере, пожалуйста, проверьте его."
@@ -307,9 +328,18 @@ export default {
"Custom": [
"По-своему"
],
+ "DASD": [
+ "DASD"
+ ],
"DASD %s": [
"DASD %s"
],
+ "DASD devices selection table": [
+ "Таблица выбора устройств DASD"
+ ],
+ "DASDs table section": [
+ "Раздел таблицы DASD"
+ ],
"DIAG": [
"Режим DIAG"
],
@@ -433,6 +463,9 @@ export default {
"Encryption Password": [
"Пароль шифрования"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Точный размер"
],
@@ -493,6 +526,9 @@ export default {
"Format": [
"Формат"
],
+ "Format selected devices?": [
+ "Отформатировать выбранные устройства?"
+ ],
"Format the device": [
"Отформатировать устройство"
],
@@ -517,6 +553,9 @@ export default {
"GiB": [
"ГиБ"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Скрыть %d действие подтома",
"Скрыть %d действия подтома",
@@ -591,6 +630,9 @@ export default {
"Install new system on": [
"Установить новую систему на"
],
+ "Install using an advanced configuration.": [
+ ""
+ ],
"Install using device %s and deleting all its content": [
"Установить с использованием устройства %s и удалить все его содержимое"
],
@@ -618,12 +660,21 @@ export default {
"Installation will take %s.": [
"Установка займёт %s."
],
+ "Installer Options": [
+ "Параметры установщика"
+ ],
"Installer options": [
"Параметры установщика"
],
+ "Installing the system, please wait...": [
+ "Установка системы, подождите..."
+ ],
"Interface": [
"Интерфейс"
],
+ "Ip prefix or netmask": [
+ "Ip префикс или маска сети"
+ ],
"Keyboard": [
"Клавиатура"
],
@@ -688,7 +739,7 @@ export default {
"Основной диск или группа томов LVM для установки."
],
"Main navigation": [
- ""
+ "Навигация"
],
"Make sure you provide the correct values": [
"Убедитесь, что вы указали правильные значения"
@@ -789,6 +840,9 @@ export default {
"No user defined yet.": [
"Пользователь еще не определен."
],
+ "No visible Wi-Fi networks found": [
+ "Сети WiFi не найдены"
+ ],
"No wired connections found": [
"Проводные соединения не обнаружены"
],
@@ -814,7 +868,7 @@ export default {
"Ни один из часовых поясов не соответствует фильтру."
],
"Not possible with the current setup. Click to know more.": [
- ""
+ "Невозможно с текущей конфигурацией. Нажмите, чтобы узнать подробнее."
],
"Not selected yet": [
"Ещё не выбрано"
@@ -823,7 +877,7 @@ export default {
"Не установлен"
],
"Offline devices must be activated before formatting them. Please, unselect or activate the devices listed below and try it again": [
- ""
+ "Перед форматированием автономных устройств их необходимо активировать. Пожалуйста, отмените выбор или активацию устройств, перечисленных ниже, и повторите попытку"
],
"Offload card": [
"Разгрузочная карта"
@@ -834,6 +888,9 @@ export default {
"Only available if authentication by target is provided": [
"Доступно только при условии аутентификации по цели"
],
+ "Options toggle": [
+ "Показ настроек"
+ ],
"Other": [
"Другая"
],
@@ -870,6 +927,9 @@ export default {
"Password confirmation": [
"Подтверждение пароля"
],
+ "Password for root user": [
+ "Пароль для пользователя root"
+ ],
"Password input": [
"Ввод пароля"
],
@@ -924,6 +984,9 @@ export default {
"Portal": [
"Портал"
],
+ "Pre-installation checks": [
+ "Проверка перед установкой"
+ ],
"Prefix length or netmask": [
"Длина префикса или маска сети"
],
@@ -936,6 +999,9 @@ export default {
"Protection for the information stored at the device, including data, programs, and system files.": [
"Защита информации, хранящейся на устройстве, включая данные, программы и системные файлы."
],
+ "Provide a password to ensure administrative access to the system.": [
+ "Укажите пароль для обеспечения административного доступа к системе."
+ ],
"Question": [
"Вопрос"
],
@@ -948,6 +1014,15 @@ export default {
"Reboot": [
"Перезагрузка"
],
+ "Register": [
+ "Зарегистрировать"
+ ],
+ "Registration": [
+ "Регистрация"
+ ],
+ "Registration code": [
+ "Код регистрации"
+ ],
"Reload": [
"Обновить"
],
@@ -1005,6 +1080,9 @@ export default {
"Select a location": [
"Выберите расположение"
],
+ "Select a product": [
+ "Выберите продукт"
+ ],
"Select booting partition": [
"Выберите загрузочный раздел"
],
@@ -1047,6 +1125,9 @@ export default {
"Set root SSH public key": [
"Установить публичный ключ SSH для root"
],
+ "Setup root user authentication": [
+ "Настройка аутентификации пользователя root"
+ ],
"Show %d subvolume action": [
"Показать %d действие подтома",
"Показать %d действия подтома",
@@ -1121,9 +1202,6 @@ export default {
"Table with mount points": [
"Таблица с точками монтирования"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Проверьте свои настройки до начала процесса установки."
- ],
"Target Password": [
"Пароль цели"
],
@@ -1154,6 +1232,9 @@ export default {
"The device cannot be shrunk:": [
"Устройство не может быть сокращено:"
],
+ "The encryption password did not work": [
+ "Пароль шифрования не сработал"
+ ],
"The file system is allocated at the device %s.": [
"Файловая система выделена на устройстве %s."
],
@@ -1211,6 +1292,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Система ещё не настроена на подключение к сети Wi-Fi."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Система будет использовать %s в качестве языка по умолчанию."
],
@@ -1236,7 +1320,7 @@ export default {
"На эти ограничения влияют:"
],
"This action could destroy any data stored on the devices listed below. Please, confirm that you really want to continue.": [
- ""
+ "Это действие может уничтожить все данные, хранящиеся на перечисленных ниже устройствах. Пожалуйста, подтвердите, что Вы действительно хотите продолжить."
],
"This product does not allow to select software patterns during installation. However, you can add additional software once the installation is finished.": [
"Данный продукт не позволяет выбирать шаблоны программного обеспечения во время установки. Однако Вы можете добавить дополнительное программное обеспечение после завершения установки."
@@ -1274,6 +1358,9 @@ export default {
"Unit for the minimum size": [
"Единица для минимального размера"
],
+ "Unselect": [
+ "Отменить выбор"
+ ],
"Unused space": [
"Неиспользуемое пространство"
],
@@ -1319,6 +1406,9 @@ export default {
"Users": [
"Пользователи"
],
+ "Visible Wi-Fi networks": [
+ "Видимые сети WiFi"
+ ],
"WPA & WPA2 Personal": [
"WPA и WPA2 Personal"
],
@@ -1328,9 +1418,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Ожидание"
- ],
"Waiting for actions information...": [
"Ожидание информации о действиях..."
],
@@ -1340,6 +1427,9 @@ export default {
"Wi-Fi": [
"Wi-Fi"
],
+ "WiFi connection form": [
+ "Форма WiFi соединения"
+ ],
"Wired": [
"Проводное"
],
@@ -1349,8 +1439,11 @@ export default {
"Yes": [
"Да"
],
+ "You can change it or select another authentication method in the 'Users' section before installing.": [
+ "Вы можете изменить его или выбрать другой метод аутентификации в разделе \"Пользователи\" перед установкой."
+ ],
"ZFCP": [
- ""
+ "ZFCP"
],
"affecting": [
"влияя на"
@@ -1413,9 +1506,9 @@ export default {
"zFCP"
],
"zFCP Disk Activation": [
- ""
+ "Активация дисков zFCP"
],
"zFCP Disk activation form": [
- ""
+ "Форма активации диска zFCP"
]
};
diff --git a/web/src/po/po.sv.js b/web/src/po/po.sv.js
index d84d6c5630..abda56350e 100644
--- a/web/src/po/po.sv.js
+++ b/web/src/po/po.sv.js
@@ -34,6 +34,9 @@ export default {
"%s disk": [
"%s disk"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s är ett oföränderligt system med atomära uppdateringar. Det använder ett skrivskyddat Btrfs filsystem som uppdateras via ögonblicksavbilder."
],
@@ -43,6 +46,9 @@ export default {
"%s with %d partitions": [
"%s med %d partitioner"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -455,6 +461,9 @@ export default {
"Encryption Password": [
"Krypteringslösenord"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Exakt storlek"
],
@@ -542,6 +551,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"Dölj %d undervolym åtgärd",
"Dölj %d undervolymer åtgärder"
@@ -996,6 +1008,15 @@ export default {
"Reboot": [
"Starta om"
],
+ "Register": [
+ "Registrera"
+ ],
+ "Registration": [
+ "Registrering"
+ ],
+ "Registration code": [
+ "Registreringskod"
+ ],
"Reload": [
"Ladda om"
],
@@ -1174,9 +1195,6 @@ export default {
"Table with mount points": [
"Tabell med monteringspunkter"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Ta dig tid att kontrollera din konfiguration innan du startar installationsprocessen."
- ],
"Target Password": [
"Mål lösenord"
],
@@ -1267,6 +1285,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Systemet har inte konfigurerats för att ansluta till ett WiFi-nätverk än."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Systemet kommer att använda %s som dess standardspråk."
],
@@ -1390,9 +1411,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Väntande"
- ],
"Waiting for actions information...": [
"Väntar på åtgärdsinformation..."
],
diff --git a/web/src/po/po.tr.js b/web/src/po/po.tr.js
index 5123c5456d..4781eb8562 100644
--- a/web/src/po/po.tr.js
+++ b/web/src/po/po.tr.js
@@ -34,15 +34,24 @@ export default {
"%s disk": [
"%s disk"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s atomik güncellemelere sahip değişmez bir sistemdir. Anlık imajlar aracılığıyla güncellenen salt okunur bir Btrfs dosya sistemi kullanır."
],
"%s logo": [
"%s logosu"
],
+ "%s must be registered.": [
+ ""
+ ],
"%s with %d partitions": [
"%s ile %d bölümler"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -455,6 +464,9 @@ export default {
"Encryption Password": [
"Şifreleme Şifresi"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"Tam boyut"
],
@@ -542,6 +554,9 @@ export default {
"GiB": [
"GB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"%d alt birim eylemini gizle",
"%d alt birim eylemlerini gizle"
@@ -615,6 +630,9 @@ export default {
"Install new system on": [
"Yeni sistemi buraya kur"
],
+ "Install using an advanced configuration.": [
+ ""
+ ],
"Install using device %s and deleting all its content": [
"%s aygıtını kullanarak yükleyin ve tüm içeriğini silin"
],
@@ -987,6 +1005,12 @@ export default {
"Reboot": [
"Yeniden Başlat"
],
+ "Register": [
+ ""
+ ],
+ "Register it now": [
+ ""
+ ],
"Reload": [
"Yenile"
],
@@ -1162,9 +1186,6 @@ export default {
"Table with mount points": [
"Bağlantı noktaları olan tablo"
],
- "Take your time to check your configuration before starting the installation process.": [
- "Kurulum sürecine başlamadan önce yapılandırmanızı kontrol etmek için zaman ayırın."
- ],
"Target Password": [
"Hedef Şifre"
],
@@ -1255,6 +1276,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"Sistem henüz bir Wi-Fi ağına bağlanacak şekilde yapılandırılmadı."
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"Sistem varsayılan dil olarak %s dilini kullanacaktır."
],
@@ -1378,9 +1402,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "Bekleyin"
- ],
"Waiting for actions information...": [
"Eylem bilgileri bekleniyor..."
],
diff --git a/web/src/po/po.zh_Hans.js b/web/src/po/po.zh_Hans.js
index 721536656f..63d3205b03 100644
--- a/web/src/po/po.zh_Hans.js
+++ b/web/src/po/po.zh_Hans.js
@@ -33,6 +33,9 @@ export default {
"%s disk": [
"%s 磁盘"
],
+ "%s has been registered with below information.": [
+ ""
+ ],
"%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [
"%s 是具备原子更新特性的不可变系统。它使用只读的 Btrfs 文件系统并通过快照保持更新。"
],
@@ -42,6 +45,9 @@ export default {
"%s with %d partitions": [
"%s (包含 %d 个分区)"
],
+ "(optional)": [
+ ""
+ ],
", ": [
", "
],
@@ -423,6 +429,9 @@ export default {
"Encryption Password": [
"加密密码"
],
+ "Enter a registration code and optionally a valid email address for registering the product.": [
+ ""
+ ],
"Exact size": [
"准确大小"
],
@@ -507,6 +516,9 @@ export default {
"GiB": [
"GiB"
],
+ "Hide": [
+ ""
+ ],
"Hide %d subvolume action": [
"隐藏 %d 个子卷操作"
],
@@ -936,6 +948,15 @@ export default {
"Reboot": [
"重启"
],
+ "Register": [
+ "注册"
+ ],
+ "Registration": [
+ "注册"
+ ],
+ "Registration code": [
+ "注册码"
+ ],
"Reload": [
"重载"
],
@@ -1104,9 +1125,6 @@ export default {
"Table with mount points": [
"挂载点列表"
],
- "Take your time to check your configuration before starting the installation process.": [
- "开始安装进程前,请花些时间检查您的配置。"
- ],
"Target Password": [
"目标密码"
],
@@ -1194,6 +1212,9 @@ export default {
"The system has not been configured for connecting to a Wi-Fi network yet.": [
"系统尚未配置为连接到 WiFi 网络。"
],
+ "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [
+ ""
+ ],
"The system will use %s as its default language.": [
"系统会使用 %s 作为默认语言。"
],
@@ -1308,9 +1329,6 @@ export default {
"WWPN": [
"WWPN"
],
- "Waiting": [
- "正在等候"
- ],
"Waiting for actions information...": [
"正在等待操作信息……"
],
diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts
index 6030489bd7..72afd692e7 100644
--- a/web/src/queries/software.ts
+++ b/web/src/queries/software.ts
@@ -33,6 +33,7 @@ import {
Pattern,
PatternsSelection,
Product,
+ RegistrationInfo,
SelectedBy,
SoftwareConfig,
SoftwareProposal,
@@ -42,6 +43,8 @@ import {
fetchPatterns,
fetchProducts,
fetchProposal,
+ fetchRegistration,
+ register,
updateConfig,
} from "~/api/software";
import { QueryHookOptions } from "~/types/queries";
@@ -80,6 +83,14 @@ const selectedProductQuery = () => ({
queryFn: () => fetchConfig().then(({ product }) => product),
});
+/**
+ * Query to retrieve registration info
+ */
+const registrationQuery = () => ({
+ queryKey: ["software/registration"],
+ queryFn: fetchRegistration,
+});
+
/**
* Query to retrieve available patterns
*/
@@ -111,6 +122,25 @@ const useConfigMutation = () => {
return useMutation(query);
};
+/**
+ * Hook that builds a mutation for registering a product
+ *
+ * @note it would trigger a general probing as a side-effect when mutation
+ * includes a product.
+ */
+const useRegisterMutation = () => {
+ const queryClient = useQueryClient();
+
+ const query = {
+ mutationFn: register,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["software/registration"] });
+ startProbing();
+ },
+ };
+ return useMutation(query);
+};
+
/**
* Returns available products and selected one, if any
*/
@@ -172,6 +202,14 @@ const useProposal = (): SoftwareProposal => {
return proposal;
};
+/**
+ * Returns registration info
+ */
+const useRegistration = (): RegistrationInfo => {
+ const { data: registration } = useSuspenseQuery(registrationQuery());
+ return registration;
+};
+
/**
* Hook that returns a useEffect to listen for software proposal events
*
@@ -226,4 +264,6 @@ export {
useProductChanges,
useProposal,
useProposalChanges,
+ useRegistration,
+ useRegisterMutation,
};
diff --git a/web/src/queries/storage.ts b/web/src/queries/storage.ts
index 72fe11a17a..957bf4c956 100644
--- a/web/src/queries/storage.ts
+++ b/web/src/queries/storage.ts
@@ -28,7 +28,7 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import React from "react";
-import { fetchConfig, setConfig } from "~/api/storage";
+import { fetchConfig, refresh, setConfig } from "~/api/storage";
import { fetchDevices, fetchDevicesDirty } from "~/api/storage/devices";
import {
calculate,
@@ -314,6 +314,32 @@ const useDeprecatedChanges = () => {
});
};
+type RefreshHandler = {
+ onStart?: () => void;
+ onFinish?: () => void;
+};
+
+/**
+ * Hook that reprobes the devices and recalculates the proposal using the current settings.
+ */
+const useRefresh = (handler?: RefreshHandler) => {
+ const queryClient = useQueryClient();
+ const deprecated = useDeprecated();
+
+ handler ||= {};
+ handler.onStart ||= () => undefined;
+ handler.onFinish ||= () => undefined;
+
+ React.useEffect(() => {
+ if (!deprecated) return;
+
+ handler.onStart();
+ refresh()
+ .then(() => queryClient.invalidateQueries({ queryKey: ["storage"] }))
+ .then(() => handler.onFinish());
+ }, [handler, deprecated, queryClient]);
+};
+
export {
useConfig,
useConfigMutation,
@@ -326,6 +352,7 @@ export {
useProposalMutation,
useDeprecated,
useDeprecatedChanges,
+ useRefresh,
};
export * from "~/queries/storage/config-model";
diff --git a/web/src/router.js b/web/src/router.tsx
similarity index 97%
rename from web/src/router.js
rename to web/src/router.tsx
index f949a76652..55ec7f0ce2 100644
--- a/web/src/router.js
+++ b/web/src/router.tsx
@@ -30,6 +30,7 @@ import { OverviewPage } from "~/components/overview";
import l10nRoutes from "~/routes/l10n";
import networkRoutes from "~/routes/network";
import productsRoutes from "~/routes/products";
+import registrationRoutes from "~/routes/registration";
import storageRoutes from "~/routes/storage";
import softwareRoutes from "~/routes/software";
import usersRoutes from "~/routes/users";
@@ -43,6 +44,7 @@ const rootRoutes = () => [
element: ,
handle: { name: N_("Overview"), icon: "list_alt" },
},
+ registrationRoutes(),
l10nRoutes(),
networkRoutes(),
storageRoutes(),
@@ -99,7 +101,6 @@ const protectedRoutes = () => [
const router = () =>
createHashRouter([
{
- exact: true,
path: PATHS.login,
element: (
diff --git a/web/src/routes/paths.ts b/web/src/routes/paths.ts
index 65f1469527..14272d87e1 100644
--- a/web/src/routes/paths.ts
+++ b/web/src/routes/paths.ts
@@ -39,6 +39,10 @@ const PRODUCT = {
progress: "/products/progress",
};
+const REGISTRATION = {
+ root: "/registration",
+};
+
const ROOT = {
root: "/",
login: "/login",
@@ -78,4 +82,13 @@ const STORAGE = {
},
};
-export { L10N, NETWORK, PRODUCT, ROOT, SOFTWARE, STORAGE, USER };
+const SUPPORTIVE_PATHS = [
+ ROOT.login,
+ PRODUCT.changeProduct,
+ PRODUCT.progress,
+ ROOT.installationProgress,
+ ROOT.installationFinished,
+ USER.rootUser.edit,
+];
+
+export { L10N, NETWORK, PRODUCT, REGISTRATION, ROOT, SOFTWARE, STORAGE, USER, SUPPORTIVE_PATHS };
diff --git a/web/src/components/core/SectionSkeleton.jsx b/web/src/routes/registration.tsx
similarity index 61%
rename from web/src/components/core/SectionSkeleton.jsx
rename to web/src/routes/registration.tsx
index 59337e133b..db27ad5037 100644
--- a/web/src/components/core/SectionSkeleton.jsx
+++ b/web/src/routes/registration.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2023] SUSE LLC
+ * Copyright (c) [2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -21,22 +21,20 @@
*/
import React from "react";
-import { Skeleton } from "@patternfly/react-core";
-import { _ } from "~/i18n";
+import { ProductRegistrationPage } from "~/components/product";
+import { Route } from "~/types/routes";
+import { REGISTRATION as PATHS } from "~/routes/paths";
+import { N_ } from "~/i18n";
-const WaitingSkeleton = ({ width }) => {
- return ;
-};
+const routes = (): Route => ({
+ path: PATHS.root,
+ handle: { name: N_("Registration"), icon: "app_registration", needsRegistrableProduct: true },
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ ],
+});
-const SectionSkeleton = ({ numRows = 2 }) => {
- return (
- <>
- {Array.from({ length: numRows }, (_, i) => {
- const width = i % 2 === 0 ? "50%" : "25%";
- return ;
- })}
- >
- );
-};
-
-export default SectionSkeleton;
+export default routes;
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
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 91%
rename from web/src/test-utils.js
rename to web/src/test-utils.tsx
index 7580c0be32..39493e288e 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.
*
@@ -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
*
@@ -112,7 +114,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 +161,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) => void>] => {
const callbacks = [];
- const on = (callback) => {
+ const on = (callback: Function) => {
callbacks.push(callback);
return () => {
const position = callbacks.indexOf(callback);
@@ -176,10 +178,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;
diff --git a/web/src/types/registration.ts b/web/src/types/registration.ts
index 4e70d22974..bb31e80435 100644
--- a/web/src/types/registration.ts
+++ b/web/src/types/registration.ts
@@ -22,7 +22,7 @@
type Registration = {
/** Registration requirement (i.e., "not-required", "optional", "mandatory") */
- requirement: string;
+ requirement: "no" | "optional" | "mandatory";
/** Registration code, if any */
code?: string;
/** Registration email, if any */
diff --git a/web/src/types/routes.ts b/web/src/types/routes.ts
index 11f2135547..fe5b85ea71 100644
--- a/web/src/types/routes.ts
+++ b/web/src/types/routes.ts
@@ -29,6 +29,8 @@ type RouteHandle = {
title?: string;
/** Icon for representing the route in some places, like a menu entry */
icon?: string;
+ /** Whether the route link will be rendered for registrable products only */
+ needsRegistrableProduct?: boolean;
};
type Route = RouteObject & { handle?: RouteHandle };
diff --git a/web/src/types/software.ts b/web/src/types/software.ts
index 822d60855a..f35a9a609e 100644
--- a/web/src/types/software.ts
+++ b/web/src/types/software.ts
@@ -41,6 +41,8 @@ type Product = {
description?: string;
/** Product icon (e.g., "default.svg") */
icon?: string;
+ /** If product is registrable or not */
+ registration: "no" | "optional" | "mandatory";
};
type PatternsSelection = { [key: string]: SelectedBy };
@@ -76,5 +78,17 @@ type Pattern = {
selectedBy?: SelectedBy;
};
+type RegistrationInfo = {
+ key: string;
+ email?: string;
+};
+
export { SelectedBy };
-export type { Pattern, PatternsSelection, Product, SoftwareConfig, SoftwareProposal };
+export type {
+ Pattern,
+ PatternsSelection,
+ Product,
+ SoftwareConfig,
+ RegistrationInfo,
+ SoftwareProposal,
+};
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..81ab64c3f8 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 = [];
@@ -250,23 +245,23 @@ function useCancellablePromise() {
};
}, []);
- const cancellablePromise = useCallback((promise) => {
- const cancellableWrapper = makeCancellable(promise);
+ const cancellablePromise = useCallback((promise: Promise): 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,
@@ -420,6 +402,11 @@ const timezoneTime = (timezone, { date = new Date() }) => {
}
};
+const mask = (value, visible = 4, character = "*") => {
+ const regex = new RegExp(`.(?=(.{${visible}}))`, "g");
+ return value.replace(regex, character);
+};
+
export {
noop,
identity,
@@ -438,7 +425,7 @@ export {
locationReload,
setLocationSearch,
localConnection,
- remoteConnection,
slugify,
timezoneTime,
+ mask,
};
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,